imip-agent

Annotated imip_manager.py

331:dbb845a59834
2015-02-12 Paul Boddie Filter out cancelled events from incoming request objects.
paul@69 1
#!/usr/bin/env python
paul@69 2
paul@146 3
"""
paul@146 4
A Web interface to a user's calendar.
paul@146 5
paul@146 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@146 20
"""
paul@146 21
paul@146 22
# Edit this path to refer to the location of the imiptools libraries, if
paul@146 23
# necessary.
paul@146 24
paul@146 25
LIBRARY_PATH = "/var/lib/imip-agent"
paul@146 26
paul@232 27
from datetime import date, datetime, timedelta
paul@149 28
import babel.dates
paul@149 29
import cgi, os, sys
paul@69 30
paul@146 31
sys.path.append(LIBRARY_PATH)
paul@69 32
paul@213 33
from imiptools.content import Handler
paul@309 34
from imiptools.data import get_address, get_uri, make_freebusy, Object, to_part, \
paul@309 35
                           uri_dict, uri_item, uri_items, uri_values
paul@286 36
from imiptools.dates import format_datetime, format_time, get_date, get_datetime, \
paul@291 37
                            get_datetime_item, get_default_timezone, \
paul@241 38
                            get_end_of_day, get_start_of_day, get_start_of_next_day, \
paul@274 39
                            get_timestamp, ends_on_same_day, to_timezone
paul@83 40
from imiptools.mail import Messenger
paul@279 41
from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
paul@279 42
                             convert_periods, get_freebusy_details, \
paul@162 43
                             get_scale, have_conflict, get_slots, get_spans, \
paul@305 44
                             partition_by_day, remove_from_freebusy, update_freebusy, \
paul@305 45
                             _update_freebusy
paul@147 46
from imiptools.profile import Preferences
paul@213 47
import imip_store
paul@69 48
import markup
paul@69 49
paul@69 50
getenv = os.environ.get
paul@69 51
setenv = os.environ.__setitem__
paul@69 52
paul@69 53
class CGIEnvironment:
paul@69 54
paul@69 55
    "A CGI-compatible environment."
paul@69 56
paul@212 57
    def __init__(self, charset=None):
paul@212 58
        self.charset = charset
paul@69 59
        self.args = None
paul@69 60
        self.method = None
paul@69 61
        self.path = None
paul@69 62
        self.path_info = None
paul@69 63
        self.user = None
paul@69 64
paul@69 65
    def get_args(self):
paul@69 66
        if self.args is None:
paul@69 67
            if self.get_method() != "POST":
paul@69 68
                setenv("QUERY_STRING", "")
paul@212 69
            args = cgi.parse(keep_blank_values=True)
paul@212 70
paul@212 71
            if not self.charset:
paul@212 72
                self.args = args
paul@212 73
            else:
paul@212 74
                self.args = {}
paul@212 75
                for key, values in args.items():
paul@212 76
                    self.args[key] = [unicode(value, self.charset) for value in values]
paul@212 77
paul@69 78
        return self.args
paul@69 79
paul@69 80
    def get_method(self):
paul@69 81
        if self.method is None:
paul@69 82
            self.method = getenv("REQUEST_METHOD") or "GET"
paul@69 83
        return self.method
paul@69 84
paul@69 85
    def get_path(self):
paul@69 86
        if self.path is None:
paul@69 87
            self.path = getenv("SCRIPT_NAME") or ""
paul@69 88
        return self.path
paul@69 89
paul@69 90
    def get_path_info(self):
paul@69 91
        if self.path_info is None:
paul@69 92
            self.path_info = getenv("PATH_INFO") or ""
paul@69 93
        return self.path_info
paul@69 94
paul@69 95
    def get_user(self):
paul@69 96
        if self.user is None:
paul@69 97
            self.user = getenv("REMOTE_USER") or ""
paul@69 98
        return self.user
paul@69 99
paul@69 100
    def get_output(self):
paul@69 101
        return sys.stdout
paul@69 102
paul@69 103
    def get_url(self):
paul@69 104
        path = self.get_path()
paul@69 105
        path_info = self.get_path_info()
paul@69 106
        return "%s%s" % (path.rstrip("/"), path_info)
paul@69 107
paul@154 108
    def new_url(self, path_info):
paul@154 109
        path = self.get_path()
paul@154 110
        return "%s/%s" % (path.rstrip("/"), path_info.lstrip("/"))
paul@154 111
paul@305 112
class Common:
paul@305 113
paul@305 114
    "Common handler and manager methods."
paul@305 115
paul@305 116
    def __init__(self, user):
paul@305 117
        self.user = user
paul@305 118
        self.preferences = None
paul@305 119
paul@305 120
    def get_preferences(self):
paul@305 121
        if not self.preferences:
paul@305 122
            self.preferences = Preferences(self.user)
paul@305 123
        return self.preferences
paul@305 124
paul@305 125
    def get_tzid(self):
paul@305 126
        prefs = self.get_preferences()
paul@305 127
        return prefs.get("TZID") or get_default_timezone()
paul@305 128
paul@305 129
class ManagerHandler(Handler, Common):
paul@79 130
paul@121 131
    """
paul@121 132
    A content handler for use by the manager, as opposed to operating within the
paul@121 133
    mail processing pipeline.
paul@121 134
    """
paul@79 135
paul@121 136
    def __init__(self, obj, user, messenger):
paul@224 137
        Handler.__init__(self, messenger=messenger)
paul@305 138
        Common.__init__(self, user)
paul@305 139
paul@224 140
        self.set_object(obj)
paul@82 141
paul@79 142
    # Communication methods.
paul@79 143
paul@253 144
    def send_message(self, method, sender, for_organiser):
paul@79 145
paul@79 146
        """
paul@207 147
        Create a full calendar object employing the given 'method', and send it
paul@253 148
        to the appropriate recipients, also sending a copy to the 'sender'. The
paul@253 149
        'for_organiser' value indicates whether the organiser is sending this
paul@253 150
        message.
paul@79 151
        """
paul@79 152
paul@219 153
        parts = [self.obj.to_part(method)]
paul@207 154
paul@260 155
        # As organiser, send an invitation to attendees, excluding oneself if
paul@260 156
        # also attending. The updated event will be saved by the outgoing
paul@260 157
        # handler.
paul@260 158
paul@323 159
        organiser = get_uri(self.obj.get_value("ORGANIZER"))
paul@309 160
        attendees = uri_values(self.obj.get_values("ATTENDEE"))
paul@308 161
paul@253 162
        if for_organiser:
paul@308 163
            recipients = [get_address(attendee) for attendee in attendees if attendee != self.user]
paul@207 164
        else:
paul@308 165
            recipients = [get_address(organiser)]
paul@207 166
paul@219 167
        # Bundle free/busy information if appropriate.
paul@219 168
paul@219 169
        preferences = Preferences(self.user)
paul@219 170
paul@219 171
        if preferences.get("freebusy_sharing") == "share" and \
paul@219 172
           preferences.get("freebusy_bundling") == "always":
paul@219 173
paul@222 174
            # Invent a unique identifier.
paul@222 175
paul@222 176
            utcnow = get_timestamp()
paul@222 177
            uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
paul@222 178
paul@222 179
            freebusy = self.store.get_freebusy(self.user)
paul@305 180
paul@305 181
            # Replace the non-updated free/busy details for this event with
paul@305 182
            # newer details (since the outgoing handler updates this user's
paul@305 183
            # free/busy details).
paul@305 184
paul@305 185
            tzid = self.get_tzid()
paul@305 186
paul@305 187
            _update_freebusy(freebusy, self.obj.get_periods_for_freebusy(tzid),
paul@305 188
                self.obj.get_value("TRANSP") or "OPAQUE", self.obj.get_value("UID"))
paul@305 189
paul@292 190
            user_attr = self.messenger and self.messenger.sender != get_address(self.user) and \
paul@292 191
                {"SENT-BY" : get_uri(self.messenger.sender)} or {}
paul@292 192
paul@292 193
            parts.append(to_part("PUBLISH", [
paul@292 194
                make_freebusy(freebusy, uid, self.user, user_attr)
paul@292 195
                ]))
paul@219 196
paul@219 197
        message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
paul@207 198
        self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
paul@79 199
paul@79 200
    # Action methods.
paul@79 201
paul@266 202
    def process_received_request(self, update=False):
paul@79 203
paul@79 204
        """
paul@266 205
        Process the current request for the given 'user'. Return whether any
paul@79 206
        action was taken.
paul@155 207
paul@155 208
        If 'update' is given, the sequence number will be incremented in order
paul@155 209
        to override any previous response.
paul@79 210
        """
paul@79 211
paul@266 212
        # Reply only on behalf of this user.
paul@79 213
paul@309 214
        for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")):
paul@79 215
paul@79 216
            if attendee == self.user:
paul@266 217
                if attendee_attr.has_key("RSVP"):
paul@266 218
                    del attendee_attr["RSVP"]
paul@128 219
                if self.messenger and self.messenger.sender != get_address(attendee):
paul@128 220
                    attendee_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@213 221
                self.obj["ATTENDEE"] = [(attendee, attendee_attr)]
paul@273 222
paul@158 223
                self.update_dtstamp()
paul@273 224
                self.set_sequence(update)
paul@155 225
paul@253 226
                self.send_message("REPLY", get_address(attendee), for_organiser=False)
paul@79 227
paul@79 228
                return True
paul@79 229
paul@79 230
        return False
paul@79 231
paul@315 232
    def process_created_request(self, method, update=False, removed=None, added=None):
paul@207 233
paul@207 234
        """
paul@207 235
        Process the current request for the given 'user', sending a created
paul@255 236
        request of the given 'method' to attendees. Return whether any action
paul@255 237
        was taken.
paul@207 238
paul@207 239
        If 'update' is given, the sequence number will be incremented in order
paul@207 240
        to override any previous message.
paul@308 241
paul@308 242
        If 'removed' is specified, a list of participants to be removed is
paul@308 243
        provided.
paul@315 244
paul@315 245
        If 'added' is specified, a list of participants to be added is provided.
paul@207 246
        """
paul@207 247
paul@309 248
        organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER"))
paul@213 249
paul@213 250
        if self.messenger and self.messenger.sender != get_address(organiser):
paul@213 251
            organiser_attr["SENT-BY"] = get_uri(self.messenger.sender)
paul@273 252
paul@311 253
        to_cancel = []
paul@311 254
paul@315 255
        if added or removed:
paul@326 256
            attendees = uri_items(self.obj.get_items("ATTENDEE") or [])
paul@315 257
paul@315 258
            if removed:
paul@315 259
                remaining = []
paul@311 260
paul@315 261
                for attendee, attendee_attr in attendees:
paul@315 262
                    if attendee in removed:
paul@315 263
                        to_cancel.append((attendee, attendee_attr))
paul@315 264
                    else:
paul@315 265
                        remaining.append((attendee, attendee_attr))
paul@311 266
paul@315 267
                attendees = remaining
paul@315 268
paul@315 269
            if added:
paul@315 270
                for attendee in added:
paul@315 271
                    attendees.append((attendee, {"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}))
paul@315 272
paul@315 273
            self.obj["ATTENDEE"] = attendees
paul@308 274
paul@207 275
        self.update_dtstamp()
paul@273 276
        self.set_sequence(update)
paul@207 277
paul@308 278
        self.send_message(method, get_address(organiser), for_organiser=True)
paul@308 279
paul@311 280
        # When cancelling, replace the attendees with those for whom the event
paul@311 281
        # is now cancelled.
paul@311 282
paul@311 283
        if to_cancel:
paul@311 284
            self.obj["ATTENDEE"] = to_cancel
paul@311 285
            self.send_message("CANCEL", get_address(organiser), for_organiser=True)
paul@311 286
paul@311 287
            # Just in case more work is done with this event, the attendees are
paul@311 288
            # now restored.
paul@311 289
paul@311 290
            self.obj["ATTENDEE"] = remaining
paul@311 291
paul@207 292
        return True
paul@207 293
paul@305 294
class Manager(Common):
paul@69 295
paul@69 296
    "A simple manager application."
paul@69 297
paul@82 298
    def __init__(self, messenger=None):
paul@82 299
        self.messenger = messenger or Messenger()
paul@212 300
        self.encoding = "utf-8"
paul@212 301
        self.env = CGIEnvironment(self.encoding)
paul@212 302
paul@69 303
        user = self.env.get_user()
paul@305 304
        Common.__init__(self, user and get_uri(user) or None)
paul@305 305
paul@149 306
        self.locale = None
paul@121 307
        self.requests = None
paul@121 308
paul@69 309
        self.out = self.env.get_output()
paul@69 310
        self.page = markup.page()
paul@69 311
paul@77 312
        self.store = imip_store.FileStore()
paul@162 313
        self.objects = {}
paul@77 314
paul@77 315
        try:
paul@77 316
            self.publisher = imip_store.FilePublisher()
paul@77 317
        except OSError:
paul@77 318
            self.publisher = None
paul@77 319
paul@121 320
    def _get_uid(self, path_info):
paul@121 321
        return path_info.lstrip("/").split("/", 1)[0]
paul@121 322
paul@117 323
    def _get_object(self, uid):
paul@162 324
        if self.objects.has_key(uid):
paul@162 325
            return self.objects[uid]
paul@162 326
paul@301 327
        fragment = uid and self.store.get_event(self.user, uid) or None
paul@213 328
        obj = self.objects[uid] = fragment and Object(fragment)
paul@121 329
        return obj
paul@121 330
paul@121 331
    def _get_requests(self):
paul@121 332
        if self.requests is None:
paul@331 333
            cancellations = self.store.get_cancellations(self.user)
paul@331 334
            requests = set(self.store.get_requests(self.user))
paul@331 335
            self.requests = requests.difference(cancellations)
paul@121 336
        return self.requests
paul@117 337
paul@162 338
    def _get_request_summary(self):
paul@162 339
        summary = []
paul@162 340
        for uid in self._get_requests():
paul@162 341
            obj = self._get_object(uid)
paul@162 342
            if obj:
paul@319 343
                for start, end in obj.get_periods_for_freebusy(self.get_tzid()):
paul@319 344
                    summary.append((start, end, uid))
paul@162 345
        return summary
paul@162 346
paul@147 347
    # Preference methods.
paul@147 348
paul@149 349
    def get_user_locale(self):
paul@149 350
        if not self.locale:
paul@149 351
            self.locale = self.get_preferences().get("LANG", "C")
paul@149 352
        return self.locale
paul@147 353
paul@162 354
    # Prettyprinting of dates and times.
paul@162 355
paul@149 356
    def format_date(self, dt, format):
paul@149 357
        return self._format_datetime(babel.dates.format_date, dt, format)
paul@149 358
paul@149 359
    def format_time(self, dt, format):
paul@149 360
        return self._format_datetime(babel.dates.format_time, dt, format)
paul@149 361
paul@149 362
    def format_datetime(self, dt, format):
paul@232 363
        return self._format_datetime(
paul@232 364
            isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,
paul@232 365
            dt, format)
paul@232 366
paul@149 367
    def _format_datetime(self, fn, dt, format):
paul@149 368
        return fn(dt, format=format, locale=self.get_user_locale())
paul@149 369
paul@78 370
    # Data management methods.
paul@78 371
paul@78 372
    def remove_request(self, uid):
paul@105 373
        return self.store.dequeue_request(self.user, uid)
paul@78 374
paul@234 375
    def remove_event(self, uid):
paul@234 376
        return self.store.remove_event(self.user, uid)
paul@234 377
paul@296 378
    def update_freebusy(self, uid, obj):
paul@296 379
        tzid = self.get_tzid()
paul@296 380
        freebusy = self.store.get_freebusy(self.user)
paul@296 381
        update_freebusy(freebusy, self.user, obj.get_periods_for_freebusy(tzid),
paul@296 382
            obj.get_value("TRANSP"), uid, self.store)
paul@296 383
paul@296 384
    def remove_from_freebusy(self, uid):
paul@296 385
        freebusy = self.store.get_freebusy(self.user)
paul@296 386
        remove_from_freebusy(freebusy, self.user, uid, self.store)
paul@296 387
paul@78 388
    # Presentation methods.
paul@78 389
paul@69 390
    def new_page(self, title):
paul@192 391
        self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))
paul@69 392
paul@69 393
    def status(self, code, message):
paul@123 394
        self.header("Status", "%s %s" % (code, message))
paul@123 395
paul@123 396
    def header(self, header, value):
paul@123 397
        print >>self.out, "%s: %s" % (header, value)
paul@69 398
paul@69 399
    def no_user(self):
paul@69 400
        self.status(403, "Forbidden")
paul@69 401
        self.new_page(title="Forbidden")
paul@69 402
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 403
paul@70 404
    def no_page(self):
paul@70 405
        self.status(404, "Not Found")
paul@70 406
        self.new_page(title="Not Found")
paul@70 407
        self.page.p("No page is provided at the given address.")
paul@70 408
paul@123 409
    def redirect(self, url):
paul@123 410
        self.status(302, "Redirect")
paul@123 411
        self.header("Location", url)
paul@123 412
        self.new_page(title="Redirect")
paul@123 413
        self.page.p("Redirecting to: %s" % url)
paul@123 414
paul@246 415
    # Request logic methods.
paul@121 416
paul@202 417
    def handle_newevent(self):
paul@202 418
paul@207 419
        """
paul@207 420
        Handle any new event operation, creating a new event and redirecting to
paul@207 421
        the event page for further activity.
paul@207 422
        """
paul@202 423
paul@202 424
        # Handle a submitted form.
paul@202 425
paul@202 426
        args = self.env.get_args()
paul@202 427
paul@202 428
        if not args.has_key("newevent"):
paul@202 429
            return
paul@202 430
paul@202 431
        # Create a new event using the available information.
paul@202 432
paul@236 433
        slots = args.get("slot", [])
paul@202 434
        participants = args.get("participants", [])
paul@202 435
paul@236 436
        if not slots:
paul@202 437
            return
paul@202 438
paul@273 439
        # Obtain the user's timezone.
paul@273 440
paul@273 441
        tzid = self.get_tzid()
paul@273 442
paul@236 443
        # Coalesce the selected slots.
paul@236 444
paul@236 445
        slots.sort()
paul@236 446
        coalesced = []
paul@236 447
        last = None
paul@236 448
paul@236 449
        for slot in slots:
paul@236 450
            start, end = slot.split("-")
paul@273 451
            start = get_datetime(start, {"TZID" : tzid})
paul@273 452
            end = end and get_datetime(end, {"TZID" : tzid}) or get_start_of_next_day(start, tzid)
paul@248 453
paul@236 454
            if last:
paul@248 455
                last_start, last_end = last
paul@248 456
paul@248 457
                # Merge adjacent dates and datetimes.
paul@248 458
paul@273 459
                if start == last_end or get_start_of_day(last_end, tzid) == get_start_of_day(start, tzid):
paul@248 460
                    last = last_start, end
paul@236 461
                    continue
paul@248 462
paul@248 463
                # Handle datetimes within dates.
paul@248 464
                # Datetime periods are within single days and are therefore
paul@248 465
                # discarded.
paul@248 466
paul@273 467
                elif get_start_of_day(start, tzid) == get_start_of_day(last_start, tzid):
paul@248 468
                    continue
paul@248 469
paul@248 470
                # Add separate dates and datetimes.
paul@248 471
paul@236 472
                else:
paul@236 473
                    coalesced.append(last)
paul@248 474
paul@236 475
            last = start, end
paul@236 476
paul@236 477
        if last:
paul@236 478
            coalesced.append(last)
paul@202 479
paul@202 480
        # Invent a unique identifier.
paul@202 481
paul@222 482
        utcnow = get_timestamp()
paul@202 483
        uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
paul@202 484
paul@236 485
        # Define a single occurrence if only one coalesced slot exists.
paul@236 486
        # Otherwise, many occurrences are defined.
paul@202 487
paul@236 488
        for i, (start, end) in enumerate(coalesced):
paul@236 489
            this_uid = "%s-%s" % (uid, i)
paul@236 490
paul@252 491
            start_value, start_attr = get_datetime_item(start, tzid)
paul@252 492
            end_value, end_attr = get_datetime_item(end, tzid)
paul@239 493
paul@236 494
            # Create a calendar object and store it as a request.
paul@236 495
paul@236 496
            record = []
paul@236 497
            rwrite = record.append
paul@202 498
paul@236 499
            rwrite(("UID", {}, this_uid))
paul@236 500
            rwrite(("SUMMARY", {}, "New event at %s" % utcnow))
paul@236 501
            rwrite(("DTSTAMP", {}, utcnow))
paul@239 502
            rwrite(("DTSTART", start_attr, start_value))
paul@239 503
            rwrite(("DTEND", end_attr, end_value))
paul@236 504
            rwrite(("ORGANIZER", {}, self.user))
paul@202 505
paul@236 506
            for participant in participants:
paul@236 507
                if not participant:
paul@236 508
                    continue
paul@236 509
                participant = get_uri(participant)
paul@253 510
                rwrite(("ATTENDEE", {"RSVP" : "TRUE", "PARTSTAT" : "NEEDS-ACTION"}, participant))
paul@202 511
paul@236 512
            obj = ("VEVENT", {}, record)
paul@236 513
paul@236 514
            self.store.set_event(self.user, this_uid, obj)
paul@236 515
            self.store.queue_request(self.user, this_uid)
paul@202 516
paul@236 517
        # Redirect to the object (or the first of the objects), where instead of
paul@236 518
        # attendee controls, there will be organiser controls.
paul@236 519
paul@236 520
        self.redirect(self.env.new_url("%s-0" % uid))
paul@202 521
paul@286 522
    def handle_request(self, uid, obj):
paul@121 523
paul@299 524
        """
paul@299 525
        Handle actions involving the given 'uid' and 'obj' object, returning an
paul@299 526
        error if one occurred, or None if the request was successfully handled.
paul@299 527
        """
paul@121 528
paul@121 529
        # Handle a submitted form.
paul@121 530
paul@121 531
        args = self.env.get_args()
paul@299 532
paul@299 533
        # Get the possible actions.
paul@299 534
paul@299 535
        reply = args.has_key("reply")
paul@299 536
        discard = args.has_key("discard")
paul@299 537
        invite = args.has_key("invite")
paul@299 538
        cancel = args.has_key("cancel")
paul@299 539
        save = args.has_key("save")
paul@299 540
paul@299 541
        have_action = reply or discard or invite or cancel or save
paul@299 542
paul@299 543
        if not have_action:
paul@299 544
            return ["action"]
paul@121 545
paul@212 546
        # Update the object.
paul@212 547
paul@212 548
        if args.has_key("summary"):
paul@213 549
            obj["SUMMARY"] = [(args["summary"][0], {})]
paul@212 550
paul@309 551
        organisers = uri_dict(obj.get_value_map("ORGANIZER"))
paul@309 552
        attendees = uri_dict(obj.get_value_map("ATTENDEE"))
paul@308 553
paul@257 554
        if args.has_key("partstat"):
paul@286 555
            for d in attendees, organisers:
paul@286 556
                if d.has_key(self.user):
paul@286 557
                    d[self.user]["PARTSTAT"] = args["partstat"][0]
paul@286 558
                    if d[self.user].has_key("RSVP"):
paul@286 559
                        del d[self.user]["RSVP"]
paul@286 560
paul@309 561
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@286 562
paul@286 563
        # Obtain the user's timezone and process datetime values.
paul@286 564
paul@286 565
        update = False
paul@286 566
paul@286 567
        if is_organiser:
paul@300 568
            dtend_enabled = args.get("dtend-control", [None])[0] == "enable"
paul@300 569
            dttimes_enabled = args.get("dttimes-control", [None])[0] == "enable"
paul@300 570
paul@300 571
            t = self.handle_date_controls("dtstart", dttimes_enabled)
paul@286 572
            if t:
paul@290 573
                dtstart, attr = t
paul@300 574
                update = self.set_datetime_in_object(dtstart, attr.get("TZID"), "DTSTART", obj) or update
paul@286 575
            else:
paul@299 576
                return ["dtstart"]
paul@290 577
paul@290 578
            # Handle specified end datetimes.
paul@290 579
paul@300 580
            if dtend_enabled:
paul@300 581
                t = self.handle_date_controls("dtend", dttimes_enabled)
paul@290 582
                if t:
paul@290 583
                    dtend, attr = t
paul@290 584
paul@290 585
                    # Convert end dates to iCalendar "next day" dates.
paul@286 586
paul@290 587
                    if not isinstance(dtend, datetime):
paul@290 588
                        dtend += timedelta(1)
paul@300 589
                    update = self.set_datetime_in_object(dtend, attr.get("TZID"), "DTEND", obj) or update
paul@290 590
                else:
paul@299 591
                    return ["dtend"]
paul@290 592
paul@299 593
            # Otherwise, treat the end date as the start date. Datetimes are
paul@299 594
            # handled by making the event occupy the rest of the day.
paul@290 595
paul@286 596
            else:
paul@299 597
                dtend = dtstart + timedelta(1)
paul@290 598
                if isinstance(dtstart, datetime):
paul@299 599
                    dtend = get_start_of_day(dtend, attr["TZID"])
paul@300 600
                update = self.set_datetime_in_object(dtend, attr.get("TZID"), "DTEND", obj) or update
paul@286 601
paul@290 602
            if dtstart >= dtend:
paul@299 603
                return ["dtstart", "dtend"]
paul@257 604
paul@315 605
        # Obtain any participants to be added or removed.
paul@315 606
paul@315 607
        removed = args.get("remove")
paul@315 608
        added = args.get("added")
paul@315 609
paul@212 610
        # Process any action.
paul@212 611
paul@299 612
        handled = True
paul@121 613
paul@266 614
        if reply or invite or cancel:
paul@121 615
paul@212 616
            handler = ManagerHandler(obj, self.user, self.messenger)
paul@121 617
paul@212 618
            # Process the object and remove it from the list of requests.
paul@121 619
paul@266 620
            if reply and handler.process_received_request(update) or \
paul@308 621
               is_organiser and (invite or cancel) and \
paul@315 622
               handler.process_created_request(invite and "REQUEST" or "CANCEL", update, removed, added):
paul@121 623
paul@121 624
                self.remove_request(uid)
paul@121 625
paul@257 626
        # Save single user events.
paul@121 627
paul@257 628
        elif save:
paul@257 629
            self.store.set_event(self.user, uid, obj.to_node())
paul@296 630
            self.update_freebusy(uid, obj)
paul@257 631
            self.remove_request(uid)
paul@121 632
paul@257 633
        # Remove the request and the object.
paul@257 634
paul@257 635
        elif discard:
paul@296 636
            self.remove_from_freebusy(uid)
paul@234 637
            self.remove_event(uid)
paul@121 638
            self.remove_request(uid)
paul@121 639
paul@121 640
        else:
paul@123 641
            handled = False
paul@121 642
paul@212 643
        # Upon handling an action, redirect to the main page.
paul@212 644
paul@123 645
        if handled:
paul@123 646
            self.redirect(self.env.get_path())
paul@123 647
paul@299 648
        return None
paul@121 649
paul@300 650
    def handle_date_controls(self, name, with_time=True):
paul@155 651
paul@155 652
        """
paul@286 653
        Handle date control information for fields starting with 'name',
paul@290 654
        returning a (datetime, attr) tuple or None if the fields cannot be used
paul@286 655
        to construct a datetime object.
paul@155 656
        """
paul@155 657
paul@286 658
        args = self.env.get_args()
paul@286 659
paul@286 660
        if args.has_key("%s-date" % name):
paul@286 661
            date = args["%s-date" % name][0]
paul@300 662
paul@300 663
            if with_time:
paul@300 664
                hour = args.get("%s-hour" % name, [None])[0]
paul@300 665
                minute = args.get("%s-minute" % name, [None])[0]
paul@300 666
                second = args.get("%s-second" % name, [None])[0]
paul@300 667
                tzid = args.get("%s-tzid" % name, [self.get_tzid()])[0]
paul@286 668
paul@300 669
                time = (hour or minute or second) and "T%s%s%s" % (hour, minute, second) or ""
paul@300 670
                value = "%s%s" % (date, time)
paul@300 671
                attr = {"TZID" : tzid, "VALUE" : "DATE-TIME"}
paul@300 672
                dt = get_datetime(value, attr)
paul@300 673
            else:
paul@300 674
                attr = {"VALUE" : "DATE"}
paul@300 675
                dt = get_datetime(date)
paul@300 676
paul@286 677
            if dt:
paul@290 678
                return dt, attr
paul@286 679
paul@286 680
        return None
paul@286 681
paul@286 682
    def set_datetime_in_object(self, dt, tzid, property, obj):
paul@286 683
paul@286 684
        """
paul@286 685
        Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether
paul@286 686
        an update has occurred.
paul@286 687
        """
paul@286 688
paul@286 689
        if dt:
paul@286 690
            old_value = obj.get_value(property)
paul@286 691
            obj[property] = [get_datetime_item(dt, tzid)]
paul@286 692
            return format_datetime(dt) != old_value
paul@286 693
paul@286 694
        return False
paul@286 695
paul@286 696
    # Page fragment methods.
paul@286 697
paul@286 698
    def show_request_controls(self, obj):
paul@286 699
paul@286 700
        "Show form controls for a request concerning 'obj'."
paul@286 701
paul@212 702
        page = self.page
paul@326 703
        args = self.env.get_args()
paul@212 704
paul@309 705
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@207 706
paul@326 707
        attendees = uri_values((obj.get_values("ATTENDEE") or []) + args.get("attendee", []))
paul@326 708
        is_attendee = self.user in attendees
paul@121 709
paul@276 710
        is_request = obj.get_value("UID") in self._get_requests()
paul@276 711
paul@257 712
        have_other_attendees = len(attendees) > (is_attendee and 1 or 0)
paul@257 713
paul@257 714
        # Show appropriate options depending on the role of the user.
paul@257 715
paul@257 716
        if is_attendee and not is_organiser:
paul@286 717
            page.p("An action is required for this request:")
paul@253 718
paul@255 719
            page.p()
paul@266 720
            page.input(name="reply", type="submit", value="Reply")
paul@255 721
            page.add(" ")
paul@255 722
            page.input(name="discard", type="submit", value="Discard")
paul@255 723
            page.p.close()
paul@207 724
paul@255 725
        if is_organiser:
paul@257 726
            if have_other_attendees:
paul@286 727
                page.p("As organiser, you can perform the following:")
paul@255 728
paul@257 729
                page.p()
paul@257 730
                page.input(name="invite", type="submit", value="Invite")
paul@257 731
                page.add(" ")
paul@276 732
                if is_request:
paul@276 733
                    page.input(name="discard", type="submit", value="Discard")
paul@276 734
                else:
paul@276 735
                    page.input(name="cancel", type="submit", value="Cancel")
paul@257 736
                page.p.close()
paul@257 737
            else:
paul@326 738
                page.p("As attendee, you can perform the following:")
paul@326 739
paul@257 740
                page.p()
paul@257 741
                page.input(name="save", type="submit", value="Save")
paul@276 742
                page.add(" ")
paul@276 743
                page.input(name="discard", type="submit", value="Discard")
paul@257 744
                page.p.close()
paul@207 745
paul@287 746
    property_items = [
paul@287 747
        ("SUMMARY", "Summary"),
paul@287 748
        ("DTSTART", "Start"),
paul@287 749
        ("DTEND", "End"),
paul@287 750
        ("ORGANIZER", "Organiser"),
paul@287 751
        ("ATTENDEE", "Attendee"),
paul@287 752
        ]
paul@210 753
paul@257 754
    partstat_items = [
paul@257 755
        ("NEEDS-ACTION", "Not confirmed"),
paul@257 756
        ("ACCEPTED", "Attending"),
paul@259 757
        ("TENTATIVE", "Tentatively attending"),
paul@257 758
        ("DECLINED", "Not attending"),
paul@277 759
        ("DELEGATED", "Delegated"),
paul@257 760
        ]
paul@257 761
paul@299 762
    def show_object_on_page(self, uid, obj, error=None):
paul@121 763
paul@121 764
        """
paul@121 765
        Show the calendar object with the given 'uid' and representation 'obj'
paul@299 766
        on the current page. If 'error' is given, show a suitable message.
paul@121 767
        """
paul@121 768
paul@210 769
        page = self.page
paul@212 770
        page.form(method="POST")
paul@210 771
paul@154 772
        # Obtain the user's timezone.
paul@154 773
paul@244 774
        tzid = self.get_tzid()
paul@121 775
paul@290 776
        # Provide controls to change the displayed object.
paul@290 777
paul@290 778
        args = self.env.get_args()
paul@315 779
paul@315 780
        # Add or remove new attendees.
paul@315 781
        # This does not affect the stored object.
paul@315 782
paul@326 783
        existing_attendees = uri_values(obj.get_values("ATTENDEE") or [])
paul@315 784
        new_attendees = args.get("added", [])
paul@315 785
        new_attendee = args.get("attendee", [""])[0]
paul@315 786
paul@315 787
        if args.has_key("add"):
paul@315 788
            if new_attendee.strip():
paul@315 789
                new_attendee = get_uri(new_attendee.strip())
paul@315 790
                if new_attendee not in new_attendees and new_attendee not in existing_attendees:
paul@315 791
                    new_attendees.append(new_attendee)
paul@315 792
                new_attendee = ""
paul@315 793
paul@315 794
        if args.has_key("removenew"):
paul@315 795
            removed_attendee = args["removenew"][0]
paul@315 796
            if removed_attendee in new_attendees:
paul@315 797
                new_attendees.remove(removed_attendee)
paul@315 798
paul@315 799
        # Configure the start and end datetimes.
paul@315 800
paul@300 801
        dtend_control = args.get("dtend-control", [None])[0]
paul@300 802
        dttimes_control = args.get("dttimes-control", [None])[0]
paul@300 803
        with_time = dttimes_control == "enable"
paul@290 804
paul@300 805
        t = self.handle_date_controls("dtstart", with_time)
paul@290 806
        if t:
paul@290 807
            dtstart, dtstart_attr = t
paul@290 808
        else:
paul@290 809
            dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")
paul@290 810
paul@297 811
        if dtend_control == "enable":
paul@300 812
            t = self.handle_date_controls("dtend", with_time)
paul@290 813
            if t:
paul@290 814
                dtend, dtend_attr = t
paul@297 815
            else:
paul@297 816
                dtend, dtend_attr = None, {}
paul@297 817
        elif dtend_control == "disable":
paul@297 818
            dtend, dtend_attr = None, {}
paul@297 819
        else:
paul@290 820
            dtend, dtend_attr = obj.get_datetime_item("DTEND")
paul@290 821
paul@290 822
        # Change end dates to refer to the actual dates, not the iCalendar
paul@290 823
        # "next day" dates.
paul@290 824
paul@290 825
        if dtend and not isinstance(dtend, datetime):
paul@290 826
            dtend -= timedelta(1)
paul@290 827
paul@297 828
        # Show the end datetime controls if already active or if an object needs
paul@297 829
        # them.
paul@297 830
paul@300 831
        dtend_enabled = dtend_control == "enable" or isinstance(dtend, datetime) or dtstart != dtend
paul@300 832
        dttimes_enabled = dttimes_control == "enable" or isinstance(dtstart, datetime) or isinstance(dtend, datetime)
paul@297 833
paul@300 834
        if dtend_enabled:
paul@290 835
            page.input(name="dtend-control", type="radio", value="enable", id="dtend-enable", checked="checked")
paul@290 836
            page.input(name="dtend-control", type="radio", value="disable", id="dtend-disable")
paul@290 837
        else:
paul@290 838
            page.input(name="dtend-control", type="radio", value="enable", id="dtend-enable")
paul@290 839
            page.input(name="dtend-control", type="radio", value="disable", id="dtend-disable", checked="checked")
paul@290 840
paul@300 841
        if dttimes_enabled:
paul@300 842
            page.input(name="dttimes-control", type="radio", value="enable", id="dttimes-enable", checked="checked")
paul@300 843
            page.input(name="dttimes-control", type="radio", value="disable", id="dttimes-disable")
paul@300 844
        else:
paul@300 845
            page.input(name="dttimes-control", type="radio", value="enable", id="dttimes-enable")
paul@300 846
            page.input(name="dttimes-control", type="radio", value="disable", id="dttimes-disable", checked="checked")
paul@300 847
paul@121 848
        # Provide a summary of the object.
paul@121 849
paul@230 850
        page.table(class_="object", cellspacing=5, cellpadding=5)
paul@212 851
        page.thead()
paul@212 852
        page.tr()
paul@286 853
        page.th("Event", class_="mainheading", colspan=2)
paul@212 854
        page.tr.close()
paul@212 855
        page.thead.close()
paul@212 856
        page.tbody()
paul@121 857
paul@309 858
        is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
paul@269 859
paul@287 860
        for name, label in self.property_items:
paul@210 861
            page.tr()
paul@210 862
paul@210 863
            # Handle datetimes specially.
paul@210 864
paul@147 865
            if name in ["DTSTART", "DTEND"]:
paul@299 866
                field = name.lower()
paul@290 867
paul@299 868
                page.th(label, class_="objectheading %s%s" % (field, error and field in error and " error" or ""))
paul@290 869
paul@297 870
                # Obtain the datetime.
paul@297 871
paul@290 872
                if name == "DTSTART":
paul@290 873
                    dt, attr, event_tzid = dtstart, dtstart_attr, dtstart_attr.get("TZID", tzid)
paul@297 874
paul@297 875
                # Where no end datetime exists, use the start datetime as the
paul@297 876
                # basis of any potential datetime specified if dt-control is
paul@297 877
                # set.
paul@297 878
paul@290 879
                else:
paul@293 880
                    dt, attr, event_tzid = dtend or dtstart, dtend_attr or dtstart_attr, (dtend_attr or dtstart_attr).get("TZID", tzid)
paul@293 881
paul@300 882
                # Show controls for editing as organiser.
paul@286 883
paul@286 884
                if is_organiser:
paul@300 885
                    value = format_datetime(dt)
paul@300 886
paul@299 887
                    page.td(class_="objectvalue %s" % field)
paul@290 888
                    if name == "DTEND":
paul@300 889
                        page.div(class_="dt disabled")
paul@290 890
                        page.label("Specify end date", for_="dtend-enable", class_="enable")
paul@290 891
                        page.div.close()
paul@290 892
paul@300 893
                    page.div(class_="dt enabled")
paul@299 894
                    self._show_date_controls(field, value, attr, tzid)
paul@300 895
                    if name == "DTSTART":
paul@300 896
                        page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
paul@300 897
                        page.label("Specify dates only", for_="dttimes-disable", class_="time enabled disable")
paul@300 898
                    elif name == "DTEND":
paul@290 899
                        page.label("End on same day", for_="dtend-disable", class_="disable")
paul@290 900
                    page.div.close()
paul@290 901
paul@286 902
                    page.td.close()
paul@300 903
paul@300 904
                # Show a label as attendee.
paul@300 905
paul@286 906
                else:
paul@300 907
                    page.td(self.format_datetime(dt, "full"))
paul@286 908
paul@210 909
                page.tr.close()
paul@210 910
paul@212 911
            # Handle the summary specially.
paul@212 912
paul@212 913
            elif name == "SUMMARY":
paul@290 914
                value = args.get("summary", [obj.get_value(name)])[0]
paul@290 915
paul@212 916
                page.th(label, class_="objectheading")
paul@286 917
                page.td()
paul@269 918
                if is_organiser:
paul@269 919
                    page.input(name="summary", type="text", value=value, size=80)
paul@269 920
                else:
paul@269 921
                    page.add(value)
paul@212 922
                page.td.close()
paul@212 923
                page.tr.close()
paul@212 924
paul@210 925
            # Handle potentially many values.
paul@210 926
paul@147 927
            else:
paul@326 928
                items = obj.get_items(name) or []
paul@315 929
                rowspan = len(items)
paul@315 930
paul@315 931
                if name == "ATTENDEE":
paul@315 932
                    rowspan += len(new_attendees) + 1
paul@326 933
                elif not items:
paul@326 934
                    continue
paul@315 935
paul@315 936
                page.th(label, class_="objectheading", rowspan=rowspan)
paul@210 937
paul@210 938
                first = True
paul@210 939
paul@308 940
                for i, (value, attr) in enumerate(items):
paul@210 941
                    if not first:
paul@210 942
                        page.tr()
paul@210 943
                    else:
paul@210 944
                        first = False
paul@121 945
paul@277 946
                    if name in ("ATTENDEE", "ORGANIZER"):
paul@309 947
                        value = get_uri(value)
paul@309 948
paul@326 949
                        page.td(class_="objectvalue")
paul@265 950
                        page.add(value)
paul@286 951
                        page.add(" ")
paul@210 952
paul@210 953
                        partstat = attr.get("PARTSTAT")
paul@286 954
                        if value == self.user and (not is_organiser or name == "ORGANIZER"):
paul@315 955
                            self._show_menu("partstat", partstat, self.partstat_items, "partstat")
paul@265 956
                        else:
paul@286 957
                            page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")
paul@308 958
paul@308 959
                        if is_organiser and name == "ATTENDEE":
paul@315 960
                            if value in args.get("remove", []):
paul@315 961
                                page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove", checked="checked")
paul@315 962
                            else:
paul@315 963
                                page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove")
paul@308 964
                            page.label("Remove", for_="remove-%d" % i, class_="remove")
paul@308 965
                            page.label("Uninvited", for_="remove-%d" % i, class_="removed")
paul@308 966
paul@265 967
                    else:
paul@326 968
                        page.td(class_="objectvalue")
paul@265 969
                        page.add(value)
paul@210 970
paul@210 971
                    page.td.close()
paul@210 972
                    page.tr.close()
paul@210 973
paul@315 974
                # Allow more attendees to be specified.
paul@315 975
paul@315 976
                if is_organiser and name == "ATTENDEE":
paul@315 977
                    for i, attendee in enumerate(new_attendees):
paul@326 978
                        if not first:
paul@326 979
                            page.tr()
paul@326 980
                        else:
paul@326 981
                            first = False
paul@326 982
paul@315 983
                        page.td()
paul@315 984
                        page.input(name="added", type="value", value=attendee)
paul@315 985
                        page.input(name="removenew", type="submit", value=attendee, id="removenew-%d" % i, class_="remove")
paul@315 986
                        page.label("Remove", for_="removenew-%d" % i, class_="remove")
paul@315 987
                        page.td.close()
paul@315 988
                        page.tr.close()
paul@326 989
paul@326 990
                    if not first:
paul@326 991
                        page.tr()
paul@326 992
paul@315 993
                    page.td()
paul@315 994
                    page.input(name="attendee", type="value", value=new_attendee)
paul@315 995
                    page.input(name="add", type="submit", value="add", id="add-%d" % i, class_="add")
paul@315 996
                    page.label("Add", for_="add-%d" % i, class_="add")
paul@315 997
                    page.td.close()
paul@315 998
                    page.tr.close()
paul@315 999
paul@212 1000
        page.tbody.close()
paul@210 1001
        page.table.close()
paul@121 1002
paul@321 1003
        self.show_recurrences(obj)
paul@307 1004
        self.show_conflicting_events(uid, obj)
paul@307 1005
        self.show_request_controls(obj)
paul@307 1006
paul@307 1007
        page.form.close()
paul@307 1008
paul@321 1009
    def show_recurrences(self, obj):
paul@321 1010
paul@321 1011
        "Show recurrences for the object having the given representation 'obj'."
paul@321 1012
paul@321 1013
        page = self.page
paul@321 1014
paul@321 1015
        # Obtain the user's timezone.
paul@321 1016
paul@321 1017
        tzid = self.get_tzid()
paul@321 1018
paul@321 1019
        window_size = 100
paul@321 1020
paul@321 1021
        periods = obj.get_periods(self.get_tzid(), window_size)
paul@321 1022
paul@321 1023
        if len(periods) == 1:
paul@321 1024
            return
paul@321 1025
paul@321 1026
        page.p("This event occurs on the following occasions within the next %d days:" % window_size)
paul@321 1027
paul@321 1028
        page.table(cellspacing=5, cellpadding=5, class_="conflicts")
paul@321 1029
        page.thead()
paul@321 1030
        page.tr()
paul@321 1031
        page.th("Start")
paul@321 1032
        page.th("End")
paul@321 1033
        page.tr.close()
paul@321 1034
        page.thead.close()
paul@321 1035
        page.tbody()
paul@321 1036
paul@321 1037
        for start, end in periods:
paul@321 1038
            page.tr()
paul@321 1039
            page.td(self.format_datetime(start, "long"))
paul@321 1040
            page.td(self.format_datetime(end, "long"))
paul@321 1041
            page.tr.close()
paul@321 1042
paul@321 1043
        page.tbody.close()
paul@321 1044
        page.table.close()
paul@321 1045
paul@307 1046
    def show_conflicting_events(self, uid, obj):
paul@307 1047
paul@307 1048
        """
paul@307 1049
        Show conflicting events for the object having the given 'uid' and
paul@307 1050
        representation 'obj'.
paul@307 1051
        """
paul@307 1052
paul@307 1053
        page = self.page
paul@307 1054
paul@307 1055
        # Obtain the user's timezone.
paul@307 1056
paul@307 1057
        tzid = self.get_tzid()
paul@307 1058
paul@213 1059
        dtstart = format_datetime(obj.get_utc_datetime("DTSTART"))
paul@213 1060
        dtend = format_datetime(obj.get_utc_datetime("DTEND"))
paul@121 1061
paul@121 1062
        # Indicate whether there are conflicting events.
paul@121 1063
paul@121 1064
        freebusy = self.store.get_freebusy(self.user)
paul@121 1065
paul@121 1066
        if freebusy:
paul@121 1067
paul@121 1068
            # Obtain any time zone details from the suggested event.
paul@121 1069
paul@213 1070
            _dtstart, attr = obj.get_item("DTSTART")
paul@154 1071
            tzid = attr.get("TZID", tzid)
paul@121 1072
paul@121 1073
            # Show any conflicts.
paul@121 1074
paul@302 1075
            conflicts = [t for t in have_conflict(freebusy, [(dtstart, dtend)], True) if t[2] != uid]
paul@154 1076
paul@302 1077
            if conflicts:
paul@302 1078
                page.p("This event conflicts with others:")
paul@154 1079
paul@302 1080
                page.table(cellspacing=5, cellpadding=5, class_="conflicts")
paul@302 1081
                page.thead()
paul@302 1082
                page.tr()
paul@302 1083
                page.th("Event")
paul@302 1084
                page.th("Start")
paul@302 1085
                page.th("End")
paul@302 1086
                page.tr.close()
paul@302 1087
                page.thead.close()
paul@302 1088
                page.tbody()
paul@302 1089
paul@302 1090
                for t in conflicts:
paul@302 1091
                    start, end, found_uid = t[:3]
paul@302 1092
paul@302 1093
                    # Provide details of any conflicting event.
paul@302 1094
paul@302 1095
                    start = self.format_datetime(to_timezone(get_datetime(start), tzid), "long")
paul@302 1096
                    end = self.format_datetime(to_timezone(get_datetime(end), tzid), "long")
paul@302 1097
paul@302 1098
                    page.tr()
paul@154 1099
paul@154 1100
                    # Show the event summary for the conflicting event.
paul@154 1101
paul@302 1102
                    page.td()
paul@302 1103
paul@154 1104
                    found_obj = self._get_object(found_uid)
paul@154 1105
                    if found_obj:
paul@213 1106
                        page.a(found_obj.get_value("SUMMARY"), href=self.env.new_url(found_uid))
paul@302 1107
                    else:
paul@302 1108
                        page.add("No details available")
paul@302 1109
paul@302 1110
                    page.td.close()
paul@302 1111
paul@302 1112
                    page.td(start)
paul@302 1113
                    page.td(end)
paul@302 1114
paul@302 1115
                    page.tr.close()
paul@302 1116
paul@302 1117
                page.tbody.close()
paul@302 1118
                page.table.close()
paul@121 1119
paul@121 1120
    def show_requests_on_page(self):
paul@69 1121
paul@69 1122
        "Show requests for the current user."
paul@69 1123
paul@69 1124
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 1125
        # NOTE: the requests would be visited directly anyway.
paul@69 1126
paul@121 1127
        requests = self._get_requests()
paul@70 1128
paul@185 1129
        self.page.div(id="pending-requests")
paul@185 1130
paul@80 1131
        if requests:
paul@114 1132
            self.page.p("Pending requests:")
paul@114 1133
paul@80 1134
            self.page.ul()
paul@69 1135
paul@80 1136
            for request in requests:
paul@165 1137
                obj = self._get_object(request)
paul@165 1138
                if obj:
paul@165 1139
                    self.page.li()
paul@213 1140
                    self.page.a(obj.get_value("SUMMARY"), href="#request-%s" % request)
paul@165 1141
                    self.page.li.close()
paul@80 1142
paul@80 1143
            self.page.ul.close()
paul@80 1144
paul@80 1145
        else:
paul@80 1146
            self.page.p("There are no pending requests.")
paul@69 1147
paul@185 1148
        self.page.div.close()
paul@185 1149
paul@185 1150
    def show_participants_on_page(self):
paul@185 1151
paul@185 1152
        "Show participants for scheduling purposes."
paul@185 1153
paul@185 1154
        args = self.env.get_args()
paul@185 1155
        participants = args.get("participants", [])
paul@185 1156
paul@185 1157
        try:
paul@185 1158
            for name, value in args.items():
paul@185 1159
                if name.startswith("remove-participant-"):
paul@185 1160
                    i = int(name[len("remove-participant-"):])
paul@185 1161
                    del participants[i]
paul@185 1162
                    break
paul@185 1163
        except ValueError:
paul@185 1164
            pass
paul@185 1165
paul@185 1166
        # Trim empty participants.
paul@185 1167
paul@185 1168
        while participants and not participants[-1].strip():
paul@185 1169
            participants.pop()
paul@185 1170
paul@185 1171
        # Show any specified participants together with controls to remove and
paul@185 1172
        # add participants.
paul@185 1173
paul@185 1174
        self.page.div(id="participants")
paul@185 1175
paul@185 1176
        self.page.p("Participants for scheduling:")
paul@185 1177
paul@185 1178
        for i, participant in enumerate(participants):
paul@185 1179
            self.page.p()
paul@185 1180
            self.page.input(name="participants", type="text", value=participant)
paul@185 1181
            self.page.input(name="remove-participant-%d" % i, type="submit", value="Remove")
paul@185 1182
            self.page.p.close()
paul@185 1183
paul@185 1184
        self.page.p()
paul@185 1185
        self.page.input(name="participants", type="text")
paul@185 1186
        self.page.input(name="add-participant", type="submit", value="Add")
paul@185 1187
        self.page.p.close()
paul@185 1188
paul@185 1189
        self.page.div.close()
paul@185 1190
paul@185 1191
        return participants
paul@185 1192
paul@121 1193
    # Full page output methods.
paul@70 1194
paul@121 1195
    def show_object(self, path_info):
paul@70 1196
paul@121 1197
        "Show an object request using the given 'path_info' for the current user."
paul@70 1198
paul@121 1199
        uid = self._get_uid(path_info)
paul@121 1200
        obj = self._get_object(uid)
paul@121 1201
paul@121 1202
        if not obj:
paul@70 1203
            return False
paul@70 1204
paul@299 1205
        error = self.handle_request(uid, obj)
paul@77 1206
paul@299 1207
        if not error:
paul@123 1208
            return True
paul@73 1209
paul@123 1210
        self.new_page(title="Event")
paul@299 1211
        self.show_object_on_page(uid, obj, error)
paul@73 1212
paul@70 1213
        return True
paul@70 1214
paul@114 1215
    def show_calendar(self):
paul@114 1216
paul@114 1217
        "Show the calendar for the current user."
paul@114 1218
paul@202 1219
        handled = self.handle_newevent()
paul@202 1220
paul@114 1221
        self.new_page(title="Calendar")
paul@162 1222
        page = self.page
paul@162 1223
paul@196 1224
        # Form controls are used in various places on the calendar page.
paul@196 1225
paul@196 1226
        page.form(method="POST")
paul@196 1227
paul@121 1228
        self.show_requests_on_page()
paul@185 1229
        participants = self.show_participants_on_page()
paul@114 1230
paul@196 1231
        # Show a button for scheduling a new event.
paul@196 1232
paul@230 1233
        page.p(class_="controls")
paul@313 1234
        page.input(name="newevent", type="submit", value="New event", id="newevent", accesskey="N")
paul@258 1235
        page.input(name="reset", type="submit", value="Clear selections", id="reset")
paul@196 1236
        page.p.close()
paul@196 1237
paul@280 1238
        # Show controls for hiding empty days and busy slots.
paul@203 1239
        # The positioning of the control, paragraph and table are important here.
paul@203 1240
paul@288 1241
        page.input(name="showdays", type="checkbox", value="show", id="showdays", accesskey="D")
paul@282 1242
        page.input(name="hidebusy", type="checkbox", value="hide", id="hidebusy", accesskey="B")
paul@203 1243
paul@230 1244
        page.p(class_="controls")
paul@237 1245
        page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable")
paul@237 1246
        page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable")
paul@288 1247
        page.label("Show empty days", for_="showdays", class_="showdays disable")
paul@288 1248
        page.label("Hide empty days", for_="showdays", class_="showdays enable")
paul@203 1249
        page.p.close()
paul@203 1250
paul@114 1251
        freebusy = self.store.get_freebusy(self.user)
paul@114 1252
paul@114 1253
        if not freebusy:
paul@114 1254
            page.p("No events scheduled.")
paul@114 1255
            return
paul@114 1256
paul@154 1257
        # Obtain the user's timezone.
paul@147 1258
paul@244 1259
        tzid = self.get_tzid()
paul@147 1260
paul@114 1261
        # Day view: start at the earliest known day and produce days until the
paul@114 1262
        # latest known day, perhaps with expandable sections of empty days.
paul@114 1263
paul@114 1264
        # Month view: start at the earliest known month and produce months until
paul@114 1265
        # the latest known month, perhaps with expandable sections of empty
paul@114 1266
        # months.
paul@114 1267
paul@114 1268
        # Details of users to invite to new events could be superimposed on the
paul@114 1269
        # calendar.
paul@114 1270
paul@185 1271
        # Requests are listed and linked to their tentative positions in the
paul@185 1272
        # calendar. Other participants are also shown.
paul@185 1273
paul@185 1274
        request_summary = self._get_request_summary()
paul@185 1275
paul@185 1276
        period_groups = [request_summary, freebusy]
paul@185 1277
        period_group_types = ["request", "freebusy"]
paul@185 1278
        period_group_sources = ["Pending requests", "Your schedule"]
paul@185 1279
paul@187 1280
        for i, participant in enumerate(participants):
paul@185 1281
            period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant)))
paul@187 1282
            period_group_types.append("freebusy-part%d" % i)
paul@185 1283
            period_group_sources.append(participant)
paul@114 1284
paul@162 1285
        groups = []
paul@162 1286
        group_columns = []
paul@185 1287
        group_types = period_group_types
paul@185 1288
        group_sources = period_group_sources
paul@162 1289
        all_points = set()
paul@162 1290
paul@162 1291
        # Obtain time point information for each group of periods.
paul@162 1292
paul@185 1293
        for periods in period_groups:
paul@162 1294
            periods = convert_periods(periods, tzid)
paul@162 1295
paul@162 1296
            # Get the time scale with start and end points.
paul@162 1297
paul@162 1298
            scale = get_scale(periods)
paul@162 1299
paul@162 1300
            # Get the time slots for the periods.
paul@162 1301
paul@162 1302
            slots = get_slots(scale)
paul@162 1303
paul@162 1304
            # Add start of day time points for multi-day periods.
paul@162 1305
paul@244 1306
            add_day_start_points(slots, tzid)
paul@162 1307
paul@162 1308
            # Record the slots and all time points employed.
paul@162 1309
paul@162 1310
            groups.append(slots)
paul@201 1311
            all_points.update([point for point, active in slots])
paul@162 1312
paul@162 1313
        # Partition the groups into days.
paul@162 1314
paul@162 1315
        days = {}
paul@162 1316
        partitioned_groups = []
paul@171 1317
        partitioned_group_types = []
paul@185 1318
        partitioned_group_sources = []
paul@162 1319
paul@185 1320
        for slots, group_type, group_source in zip(groups, group_types, group_sources):
paul@162 1321
paul@162 1322
            # Propagate time points to all groups of time slots.
paul@162 1323
paul@162 1324
            add_slots(slots, all_points)
paul@162 1325
paul@162 1326
            # Count the number of columns employed by the group.
paul@162 1327
paul@162 1328
            columns = 0
paul@162 1329
paul@162 1330
            # Partition the time slots by day.
paul@162 1331
paul@162 1332
            partitioned = {}
paul@162 1333
paul@162 1334
            for day, day_slots in partition_by_day(slots).items():
paul@201 1335
                intervals = []
paul@201 1336
                last = None
paul@201 1337
paul@201 1338
                for point, active in day_slots:
paul@201 1339
                    columns = max(columns, len(active))
paul@201 1340
                    if last:
paul@201 1341
                        intervals.append((last, point))
paul@201 1342
                    last = point
paul@201 1343
paul@201 1344
                if last:
paul@201 1345
                    intervals.append((last, None))
paul@162 1346
paul@162 1347
                if not days.has_key(day):
paul@162 1348
                    days[day] = set()
paul@162 1349
paul@162 1350
                # Convert each partition to a mapping from points to active
paul@162 1351
                # periods.
paul@162 1352
paul@201 1353
                partitioned[day] = dict(day_slots)
paul@201 1354
paul@201 1355
                # Record the divisions or intervals within each day.
paul@201 1356
paul@201 1357
                days[day].update(intervals)
paul@162 1358
paul@194 1359
            if group_type != "request" or columns:
paul@194 1360
                group_columns.append(columns)
paul@194 1361
                partitioned_groups.append(partitioned)
paul@194 1362
                partitioned_group_types.append(group_type)
paul@194 1363
                partitioned_group_sources.append(group_source)
paul@114 1364
paul@279 1365
        # Add empty days.
paul@279 1366
paul@283 1367
        add_empty_days(days, tzid)
paul@279 1368
paul@279 1369
        # Show the controls permitting day selection.
paul@279 1370
paul@243 1371
        self.show_calendar_day_controls(days)
paul@243 1372
paul@279 1373
        # Show the calendar itself.
paul@279 1374
paul@230 1375
        page.table(cellspacing=5, cellpadding=5, class_="calendar")
paul@188 1376
        self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)
paul@171 1377
        self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns)
paul@162 1378
        page.table.close()
paul@114 1379
paul@196 1380
        # End the form region.
paul@196 1381
paul@196 1382
        page.form.close()
paul@196 1383
paul@246 1384
    # More page fragment methods.
paul@246 1385
paul@243 1386
    def show_calendar_day_controls(self, days):
paul@243 1387
paul@243 1388
        "Show controls for the given 'days' in the calendar."
paul@243 1389
paul@243 1390
        page = self.page
paul@243 1391
        slots = self.env.get_args().get("slot", [])
paul@243 1392
paul@243 1393
        for day in days:
paul@243 1394
            value, identifier = self._day_value_and_identifier(day)
paul@243 1395
            self._slot_selector(value, identifier, slots)
paul@243 1396
paul@243 1397
        # Generate a dynamic stylesheet to allow day selections to colour
paul@243 1398
        # specific days.
paul@243 1399
        # NOTE: The style details need to be coordinated with the static
paul@243 1400
        # NOTE: stylesheet.
paul@243 1401
paul@243 1402
        page.style(type="text/css")
paul@243 1403
paul@243 1404
        for day in days:
paul@243 1405
            daystr = format_datetime(day)
paul@243 1406
            page.add("""\
paul@249 1407
input.newevent.selector#day-%s-:checked ~ table label.day.day-%s,
paul@249 1408
input.newevent.selector#day-%s-:checked ~ table label.timepoint.day-%s {
paul@243 1409
    background-color: #5f4;
paul@243 1410
    text-decoration: underline;
paul@243 1411
}
paul@243 1412
""" % (daystr, daystr, daystr, daystr))
paul@243 1413
paul@243 1414
        page.style.close()
paul@243 1415
paul@188 1416
    def show_calendar_participant_headings(self, group_types, group_sources, group_columns):
paul@186 1417
paul@186 1418
        """
paul@186 1419
        Show headings for the participants and other scheduling contributors,
paul@188 1420
        defined by 'group_types', 'group_sources' and 'group_columns'.
paul@186 1421
        """
paul@186 1422
paul@185 1423
        page = self.page
paul@185 1424
paul@188 1425
        page.colgroup(span=1, id="columns-timeslot")
paul@186 1426
paul@188 1427
        for group_type, columns in zip(group_types, group_columns):
paul@191 1428
            page.colgroup(span=max(columns, 1), id="columns-%s" % group_type)
paul@186 1429
paul@185 1430
        page.thead()
paul@185 1431
        page.tr()
paul@185 1432
        page.th("", class_="emptyheading")
paul@185 1433
paul@193 1434
        for group_type, source, columns in zip(group_types, group_sources, group_columns):
paul@193 1435
            page.th(source,
paul@193 1436
                class_=(group_type == "request" and "requestheading" or "participantheading"),
paul@193 1437
                colspan=max(columns, 1))
paul@185 1438
paul@185 1439
        page.tr.close()
paul@185 1440
        page.thead.close()
paul@185 1441
paul@171 1442
    def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns):
paul@186 1443
paul@186 1444
        """
paul@186 1445
        Show calendar days, defined by a collection of 'days', the contributing
paul@186 1446
        period information as 'partitioned_groups' (partitioned by day), the
paul@186 1447
        'partitioned_group_types' indicating the kind of contribution involved,
paul@186 1448
        and the 'group_columns' defining the number of columns in each group.
paul@186 1449
        """
paul@186 1450
paul@162 1451
        page = self.page
paul@162 1452
paul@191 1453
        # Determine the number of columns required. Where participants provide
paul@191 1454
        # no columns for events, one still needs to be provided for the
paul@191 1455
        # participant itself.
paul@147 1456
paul@191 1457
        all_columns = sum([max(columns, 1) for columns in group_columns])
paul@191 1458
paul@191 1459
        # Determine the days providing time slots.
paul@191 1460
paul@162 1461
        all_days = days.items()
paul@162 1462
        all_days.sort()
paul@162 1463
paul@162 1464
        # Produce a heading and time points for each day.
paul@162 1465
paul@201 1466
        for day, intervals in all_days:
paul@279 1467
            groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
paul@279 1468
            is_empty = True
paul@279 1469
paul@279 1470
            for slots in groups_for_day:
paul@279 1471
                if not slots:
paul@279 1472
                    continue
paul@279 1473
paul@279 1474
                for active in slots.values():
paul@279 1475
                    if active:
paul@279 1476
                        is_empty = False
paul@279 1477
                        break
paul@279 1478
paul@282 1479
            page.thead(class_="separator%s" % (is_empty and " empty" or ""))
paul@282 1480
            page.tr()
paul@243 1481
            page.th(class_="dayheading container", colspan=all_columns+1)
paul@239 1482
            self._day_heading(day)
paul@114 1483
            page.th.close()
paul@153 1484
            page.tr.close()
paul@186 1485
            page.thead.close()
paul@114 1486
paul@282 1487
            page.tbody(class_="points%s" % (is_empty and " empty" or ""))
paul@280 1488
            self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns)
paul@186 1489
            page.tbody.close()
paul@185 1490
paul@280 1491
    def show_calendar_points(self, intervals, groups, group_types, group_columns):
paul@186 1492
paul@186 1493
        """
paul@201 1494
        Show the time 'intervals' along with period information from the given
paul@186 1495
        'groups', having the indicated 'group_types', each with the number of
paul@186 1496
        columns given by 'group_columns'.
paul@186 1497
        """
paul@186 1498
paul@162 1499
        page = self.page
paul@162 1500
paul@244 1501
        # Obtain the user's timezone.
paul@244 1502
paul@244 1503
        tzid = self.get_tzid()
paul@244 1504
paul@203 1505
        # Produce a row for each interval.
paul@162 1506
paul@201 1507
        intervals = list(intervals)
paul@201 1508
        intervals.sort()
paul@162 1509
paul@201 1510
        for point, endpoint in intervals:
paul@244 1511
            continuation = point == get_start_of_day(point, tzid)
paul@153 1512
paul@203 1513
            # Some rows contain no period details and are marked as such.
paul@203 1514
paul@283 1515
            have_active = reduce(lambda x, y: x or y, [slots and slots.get(point) for slots in groups], None)
paul@203 1516
paul@203 1517
            css = " ".join(
paul@203 1518
                ["slot"] +
paul@231 1519
                (have_active and ["busy"] or ["empty"]) +
paul@203 1520
                (continuation and ["daystart"] or [])
paul@203 1521
                )
paul@203 1522
paul@203 1523
            page.tr(class_=css)
paul@162 1524
            page.th(class_="timeslot")
paul@201 1525
            self._time_point(point, endpoint)
paul@162 1526
            page.th.close()
paul@162 1527
paul@162 1528
            # Obtain slots for the time point from each group.
paul@162 1529
paul@171 1530
            for columns, slots, group_type in zip(group_columns, groups, group_types):
paul@162 1531
                active = slots and slots.get(point)
paul@162 1532
paul@191 1533
                # Where no periods exist for the given time interval, generate
paul@191 1534
                # an empty cell. Where a participant provides no periods at all,
paul@191 1535
                # the colspan is adjusted to be 1, not 0.
paul@191 1536
paul@162 1537
                if not active:
paul@196 1538
                    page.td(class_="empty container", colspan=max(columns, 1))
paul@201 1539
                    self._empty_slot(point, endpoint)
paul@196 1540
                    page.td.close()
paul@162 1541
                    continue
paul@162 1542
paul@162 1543
                slots = slots.items()
paul@162 1544
                slots.sort()
paul@162 1545
                spans = get_spans(slots)
paul@162 1546
paul@278 1547
                empty = 0
paul@278 1548
paul@162 1549
                # Show a column for each active period.
paul@117 1550
paul@153 1551
                for t in active:
paul@185 1552
                    if t and len(t) >= 2:
paul@278 1553
paul@278 1554
                        # Flush empty slots preceding this one.
paul@278 1555
paul@278 1556
                        if empty:
paul@278 1557
                            page.td(class_="empty container", colspan=empty)
paul@278 1558
                            self._empty_slot(point, endpoint)
paul@278 1559
                            page.td.close()
paul@278 1560
                            empty = 0
paul@278 1561
paul@185 1562
                        start, end, uid, key = get_freebusy_details(t)
paul@185 1563
                        span = spans[key]
paul@171 1564
paul@171 1565
                        # Produce a table cell only at the start of the period
paul@171 1566
                        # or when continued at the start of a day.
paul@171 1567
paul@153 1568
                        if point == start or continuation:
paul@153 1569
paul@275 1570
                            obj = self._get_object(uid)
paul@275 1571
paul@195 1572
                            has_continued = continuation and point != start
paul@244 1573
                            will_continue = not ends_on_same_day(point, end, tzid)
paul@309 1574
                            is_organiser = obj and get_uri(obj.get_value("ORGANIZER")) == self.user
paul@275 1575
paul@195 1576
                            css = " ".join(
paul@195 1577
                                ["event"] +
paul@195 1578
                                (has_continued and ["continued"] or []) +
paul@275 1579
                                (will_continue and ["continues"] or []) +
paul@275 1580
                                (is_organiser and ["organising"] or ["attending"])
paul@195 1581
                                )
paul@195 1582
paul@189 1583
                            # Only anchor the first cell of events.
paul@189 1584
paul@189 1585
                            if point == start:
paul@195 1586
                                page.td(class_=css, rowspan=span, id="%s-%s" % (group_type, uid))
paul@189 1587
                            else:
paul@195 1588
                                page.td(class_=css, rowspan=span)
paul@171 1589
paul@185 1590
                            if not obj:
paul@291 1591
                                page.span("(Participant is busy)")
paul@185 1592
                            else:
paul@213 1593
                                summary = obj.get_value("SUMMARY")
paul@171 1594
paul@171 1595
                                # Only link to events if they are not being
paul@171 1596
                                # updated by requests.
paul@171 1597
paul@171 1598
                                if uid in self._get_requests() and group_type != "request":
paul@189 1599
                                    page.span(summary)
paul@164 1600
                                else:
paul@171 1601
                                    href = "%s/%s" % (self.env.get_url().rstrip("/"), uid)
paul@189 1602
                                    page.a(summary, href=href)
paul@171 1603
paul@153 1604
                            page.td.close()
paul@153 1605
                    else:
paul@278 1606
                        empty += 1
paul@114 1607
paul@166 1608
                # Pad with empty columns.
paul@166 1609
paul@278 1610
                empty = columns - len(active)
paul@278 1611
paul@278 1612
                if empty:
paul@278 1613
                    page.td(class_="empty container", colspan=empty)
paul@201 1614
                    self._empty_slot(point, endpoint)
paul@196 1615
                    page.td.close()
paul@166 1616
paul@162 1617
            page.tr.close()
paul@114 1618
paul@239 1619
    def _day_heading(self, day):
paul@243 1620
paul@243 1621
        """
paul@243 1622
        Generate a heading for 'day' of the following form:
paul@243 1623
paul@243 1624
        <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label>
paul@243 1625
        """
paul@243 1626
paul@239 1627
        page = self.page
paul@243 1628
        daystr = format_datetime(day)
paul@239 1629
        value, identifier = self._day_value_and_identifier(day)
paul@243 1630
        page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier)
paul@239 1631
paul@201 1632
    def _time_point(self, point, endpoint):
paul@243 1633
paul@243 1634
        """
paul@243 1635
        Generate headings for the 'point' to 'endpoint' period of the following
paul@243 1636
        form:
paul@243 1637
paul@243 1638
        <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>
paul@243 1639
        <span class="endpoint">10:00:00 CET</span>
paul@243 1640
        """
paul@243 1641
paul@201 1642
        page = self.page
paul@244 1643
        tzid = self.get_tzid()
paul@243 1644
        daystr = format_datetime(point.date())
paul@201 1645
        value, identifier = self._slot_value_and_identifier(point, endpoint)
paul@238 1646
        slots = self.env.get_args().get("slot", [])
paul@239 1647
        self._slot_selector(value, identifier, slots)
paul@243 1648
        page.label(self.format_time(point, "long"), class_="timepoint day-%s" % daystr, for_=identifier)
paul@244 1649
        page.span(self.format_time(endpoint or get_end_of_day(point, tzid), "long"), class_="endpoint")
paul@239 1650
paul@239 1651
    def _slot_selector(self, value, identifier, slots):
paul@258 1652
        reset = self.env.get_args().has_key("reset")
paul@239 1653
        page = self.page
paul@258 1654
        if not reset and value in slots:
paul@249 1655
            page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector", checked="checked")
paul@202 1656
        else:
paul@249 1657
            page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector")
paul@201 1658
paul@201 1659
    def _empty_slot(self, point, endpoint):
paul@197 1660
        page = self.page
paul@201 1661
        value, identifier = self._slot_value_and_identifier(point, endpoint)
paul@236 1662
        page.label("Select/deselect period", class_="newevent popup", for_=identifier)
paul@196 1663
paul@239 1664
    def _day_value_and_identifier(self, day):
paul@239 1665
        value = "%s-" % format_datetime(day)
paul@239 1666
        identifier = "day-%s" % value
paul@239 1667
        return value, identifier
paul@239 1668
paul@201 1669
    def _slot_value_and_identifier(self, point, endpoint):
paul@202 1670
        value = "%s-%s" % (format_datetime(point), endpoint and format_datetime(endpoint) or "")
paul@201 1671
        identifier = "slot-%s" % value
paul@201 1672
        return value, identifier
paul@196 1673
paul@315 1674
    def _show_menu(self, name, default, items, class_=""):
paul@257 1675
        page = self.page
paul@286 1676
        values = self.env.get_args().get(name, [default])
paul@324 1677
        page.select(name=name, class_=class_)
paul@257 1678
        for v, label in items:
paul@257 1679
            if v in values:
paul@324 1680
                page.option(label, value=v, selected="selected")
paul@257 1681
            else:
paul@324 1682
                page.option(label, value=v)
paul@257 1683
        page.select.close()
paul@257 1684
paul@286 1685
    def _show_date_controls(self, name, default, attr, tzid):
paul@286 1686
paul@286 1687
        """
paul@286 1688
        Show date controls for a field with the given 'name' and 'default' value
paul@286 1689
        and 'attr', with the given 'tzid' being used if no other time regime
paul@286 1690
        information is provided.
paul@286 1691
        """
paul@286 1692
paul@286 1693
        page = self.page
paul@286 1694
        args = self.env.get_args()
paul@286 1695
paul@286 1696
        event_tzid = attr.get("TZID", tzid)
paul@286 1697
        dt = get_datetime(default, attr)
paul@286 1698
paul@286 1699
        # Show dates for up to one week around the current date.
paul@286 1700
paul@286 1701
        base = get_date(dt)
paul@286 1702
        items = []
paul@286 1703
        for i in range(-7, 8):
paul@286 1704
            d = base + timedelta(i)
paul@286 1705
            items.append((format_datetime(d), self.format_date(d, "full")))
paul@286 1706
paul@286 1707
        self._show_menu("%s-date" % name, format_datetime(base), items)
paul@286 1708
paul@286 1709
        # Show time details.
paul@286 1710
paul@300 1711
        dt_time = isinstance(dt, datetime) and dt or None
paul@300 1712
        hour = args.get("%s-hour" % name, "%02d" % (dt_time and dt_time.hour or 0))
paul@300 1713
        minute = args.get("%s-minute" % name, "%02d" % (dt_time and dt_time.minute or 0))
paul@300 1714
        second = args.get("%s-second" % name, "%02d" % (dt_time and dt_time.second or 0))
paul@300 1715
paul@300 1716
        page.span(class_="time enabled")
paul@300 1717
        page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2)
paul@300 1718
        page.add(":")
paul@300 1719
        page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2)
paul@300 1720
        page.add(":")
paul@300 1721
        page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2)
paul@300 1722
        page.add(" ")
paul@300 1723
        self._show_menu("%s-tzid" % name, event_tzid,
paul@300 1724
            [(event_tzid, event_tzid)] + (
paul@300 1725
            event_tzid != tzid and [(tzid, tzid)] or []
paul@300 1726
            ))
paul@300 1727
        page.span.close()
paul@286 1728
paul@246 1729
    # Incoming HTTP request direction.
paul@246 1730
paul@69 1731
    def select_action(self):
paul@69 1732
paul@69 1733
        "Select the desired action and show the result."
paul@69 1734
paul@121 1735
        path_info = self.env.get_path_info().strip("/")
paul@121 1736
paul@69 1737
        if not path_info:
paul@114 1738
            self.show_calendar()
paul@121 1739
        elif self.show_object(path_info):
paul@70 1740
            pass
paul@70 1741
        else:
paul@70 1742
            self.no_page()
paul@69 1743
paul@82 1744
    def __call__(self):
paul@69 1745
paul@69 1746
        "Interpret a request and show an appropriate response."
paul@69 1747
paul@69 1748
        if not self.user:
paul@69 1749
            self.no_user()
paul@69 1750
        else:
paul@69 1751
            self.select_action()
paul@69 1752
paul@70 1753
        # Write the headers and actual content.
paul@70 1754
paul@69 1755
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 1756
        print >>self.out
paul@69 1757
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 1758
paul@69 1759
if __name__ == "__main__":
paul@128 1760
    Manager()()
paul@69 1761
paul@69 1762
# vim: tabstop=4 expandtab shiftwidth=4