imip-agent

Annotated imipweb/event.py

508:b317a8937754
2015-04-07 Paul Boddie Store the final state of a cancelled event.
paul@446 1
#!/usr/bin/env python
paul@446 2
paul@446 3
"""
paul@446 4
A Web interface to a calendar event.
paul@446 5
paul@446 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@446 7
paul@446 8
This program is free software; you can redistribute it and/or modify it under
paul@446 9
the terms of the GNU General Public License as published by the Free Software
paul@446 10
Foundation; either version 3 of the License, or (at your option) any later
paul@446 11
version.
paul@446 12
paul@446 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@446 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@446 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@446 16
details.
paul@446 17
paul@446 18
You should have received a copy of the GNU General Public License along with
paul@446 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@446 20
"""
paul@446 21
paul@498 22
from datetime import date, datetime, timedelta
paul@473 23
from imiptools.client import update_attendees, update_participation
paul@446 24
from imiptools.data import get_uri, uri_dict, uri_values
paul@446 25
from imiptools.dates import format_datetime, to_date, get_datetime, \
paul@446 26
                            get_datetime_item, get_period_item, \
paul@497 27
                            to_timezone
paul@446 28
from imiptools.mail import Messenger
paul@497 29
from imiptools.period import have_conflict
paul@498 30
from imipweb.data import EventPeriod, \
paul@499 31
                         event_period_from_period, form_period_from_period, \
paul@498 32
                         FormDate, FormPeriod, PeriodError
paul@446 33
from imipweb.handler import ManagerHandler
paul@446 34
from imipweb.resource import Resource
paul@446 35
import pytz
paul@446 36
paul@446 37
class EventPage(Resource):
paul@446 38
paul@446 39
    "A request handler for the event page."
paul@446 40
paul@446 41
    def __init__(self, resource=None, messenger=None):
paul@446 42
        Resource.__init__(self, resource)
paul@446 43
        self.messenger = messenger or Messenger()
paul@446 44
paul@474 45
    # Various property values and labels.
paul@474 46
paul@474 47
    property_items = [
paul@474 48
        ("SUMMARY", "Summary"),
paul@474 49
        ("DTSTART", "Start"),
paul@474 50
        ("DTEND", "End"),
paul@474 51
        ("ORGANIZER", "Organiser"),
paul@474 52
        ("ATTENDEE", "Attendee"),
paul@474 53
        ]
paul@474 54
paul@474 55
    partstat_items = [
paul@474 56
        ("NEEDS-ACTION", "Not confirmed"),
paul@474 57
        ("ACCEPTED", "Attending"),
paul@474 58
        ("TENTATIVE", "Tentatively attending"),
paul@474 59
        ("DECLINED", "Not attending"),
paul@474 60
        ("DELEGATED", "Delegated"),
paul@474 61
        (None, "Not indicated"),
paul@474 62
        ]
paul@474 63
paul@484 64
    def is_organiser(self, obj):
paul@484 65
        return get_uri(obj.get_value("ORGANIZER")) == self.user
paul@484 66
paul@446 67
    # Request logic methods.
paul@446 68
paul@446 69
    def handle_request(self, uid, recurrenceid, obj):
paul@446 70
paul@446 71
        """
paul@446 72
        Handle actions involving the given 'uid', 'recurrenceid', and 'obj' as
paul@446 73
        the object's representation, returning an error if one occurred, or None
paul@446 74
        if the request was successfully handled.
paul@446 75
        """
paul@446 76
paul@446 77
        # Handle a submitted form.
paul@446 78
paul@446 79
        args = self.env.get_args()
paul@446 80
paul@446 81
        # Get the possible actions.
paul@446 82
paul@446 83
        reply = args.has_key("reply")
paul@446 84
        discard = args.has_key("discard")
paul@446 85
        invite = args.has_key("invite")
paul@446 86
        cancel = args.has_key("cancel")
paul@446 87
        save = args.has_key("save")
paul@446 88
        ignore = args.has_key("ignore")
paul@446 89
paul@446 90
        have_action = reply or discard or invite or cancel or save or ignore
paul@446 91
paul@446 92
        if not have_action:
paul@446 93
            return ["action"]
paul@446 94
paul@446 95
        # If ignoring the object, return to the calendar.
paul@446 96
paul@446 97
        if ignore:
paul@446 98
            self.redirect(self.env.get_path())
paul@446 99
            return None
paul@446 100
paul@446 101
        update = False
paul@472 102
paul@472 103
        # Update the object.
paul@472 104
paul@472 105
        if reply or invite or cancel or save:
paul@472 106
paul@473 107
            # Update principal event details if organiser.
paul@473 108
paul@484 109
            if self.is_organiser(obj):
paul@473 110
paul@473 111
                # Update time periods (main and recurring).
paul@473 112
paul@498 113
                try:
paul@498 114
                    period = self.handle_main_period()
paul@498 115
                except PeriodError, exc:
paul@498 116
                    return exc.args
paul@495 117
paul@498 118
                try:
paul@498 119
                    periods = self.handle_recurrence_periods()
paul@498 120
                except PeriodError, exc:
paul@498 121
                    return exc.args
paul@495 122
paul@495 123
                self.set_period_in_object(obj, period)
paul@495 124
                self.set_periods_in_object(obj, periods)
paul@472 125
paul@473 126
                # Update summary.
paul@473 127
paul@473 128
                if args.has_key("summary"):
paul@473 129
                    obj["SUMMARY"] = [(args["summary"][0], {})]
paul@446 130
paul@473 131
                # Obtain any participants and those to be removed.
paul@472 132
paul@478 133
                attendees = self.get_attendees()
paul@485 134
                removed = [attendees[int(i)] for i in args.get("remove", [])]
paul@473 135
                to_cancel = update_attendees(obj, attendees, removed)
paul@472 136
paul@472 137
            # Update attendee participation.
paul@472 138
paul@472 139
            if args.has_key("partstat"):
paul@473 140
                update_participation(obj, self.user, args["partstat"][0])
paul@446 141
paul@446 142
        # Process any action.
paul@446 143
paul@446 144
        handled = True
paul@446 145
paul@446 146
        if reply or invite or cancel:
paul@446 147
paul@446 148
            handler = ManagerHandler(obj, self.user, self.messenger)
paul@446 149
paul@446 150
            # Process the object and remove it from the list of requests.
paul@446 151
paul@473 152
            if reply and handler.process_received_request(update):
paul@473 153
                self.remove_request(uid, recurrenceid)
paul@473 154
paul@484 155
            elif self.is_organiser(obj) and (invite or cancel):
paul@446 156
paul@473 157
                if handler.process_created_request(
paul@473 158
                    invite and "REQUEST" or "CANCEL", update, to_cancel):
paul@473 159
paul@473 160
                    self.remove_request(uid, recurrenceid)
paul@446 161
paul@446 162
        # Save single user events.
paul@446 163
paul@446 164
        elif save:
paul@446 165
            self.store.set_event(self.user, uid, recurrenceid, node=obj.to_node())
paul@446 166
            self.update_freebusy(uid, recurrenceid, obj)
paul@446 167
            self.remove_request(uid, recurrenceid)
paul@446 168
paul@446 169
        # Remove the request and the object.
paul@446 170
paul@446 171
        elif discard:
paul@446 172
            self.remove_from_freebusy(uid, recurrenceid)
paul@446 173
            self.remove_event(uid, recurrenceid)
paul@446 174
            self.remove_request(uid, recurrenceid)
paul@446 175
paul@446 176
        else:
paul@446 177
            handled = False
paul@446 178
paul@446 179
        # Upon handling an action, redirect to the main page.
paul@446 180
paul@446 181
        if handled:
paul@446 182
            self.redirect(self.env.get_path())
paul@446 183
paul@446 184
        return None
paul@446 185
paul@498 186
    def set_period_in_object(self, obj, period):
paul@498 187
paul@498 188
        "Set in the given 'obj' the given 'period' as the main start and end."
paul@498 189
paul@499 190
        p = event_period_from_period(period)
paul@498 191
        result = self.set_datetime_in_object(p.start, p.start_attr and p.start_attr.get("TZID"), "DTSTART", obj)
paul@499 192
        result = self.set_datetime_in_object(p.end, p.end_attr and p.end_attr.get("TZID"), "DTEND", obj) or result
paul@498 193
        return result
paul@498 194
paul@498 195
    def set_periods_in_object(self, obj, periods):
paul@498 196
paul@498 197
        "Set in the given 'obj' the given 'periods'."
paul@498 198
paul@498 199
        update = False
paul@498 200
paul@498 201
        old_values = obj.get_values("RDATE")
paul@498 202
        new_rdates = []
paul@498 203
paul@498 204
        if obj.has_key("RDATE"):
paul@498 205
            del obj["RDATE"]
paul@498 206
paul@498 207
        for period in periods:
paul@499 208
            p = event_period_from_period(period)
paul@498 209
            tzid = p.start_attr and p.start_attr.get("TZID") or p.end_attr and p.end_attr.get("TZID")
paul@499 210
            new_rdates.append(get_period_item(p.start, p.end, tzid))
paul@498 211
paul@498 212
        obj["RDATE"] = new_rdates
paul@498 213
paul@498 214
        # NOTE: To do: calculate the update status.
paul@498 215
        return update
paul@498 216
paul@498 217
    def set_datetime_in_object(self, dt, tzid, property, obj):
paul@498 218
paul@498 219
        """
paul@498 220
        Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether
paul@498 221
        an update has occurred.
paul@498 222
        """
paul@498 223
paul@498 224
        if dt:
paul@498 225
            old_value = obj.get_value(property)
paul@498 226
            obj[property] = [get_datetime_item(dt, tzid)]
paul@498 227
            return format_datetime(dt) != old_value
paul@498 228
paul@498 229
        return False
paul@498 230
paul@495 231
    def handle_main_period(self):
paul@446 232
paul@494 233
        "Return period details for the main start/end period in an event."
paul@494 234
paul@498 235
        return self.get_main_period().as_event_period()
paul@446 236
paul@495 237
    def handle_recurrence_periods(self):
paul@446 238
paul@494 239
        "Return period details for the recurrences specified for an event."
paul@494 240
paul@498 241
        return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences())]
paul@446 242
paul@461 243
    def get_date_control_values(self, name, multiple=False, tzid_name=None):
paul@446 244
paul@446 245
        """
paul@446 246
        Return a dictionary containing date, time and tzid entries for fields
paul@461 247
        starting with 'name'. If 'multiple' is set to a true value, many
paul@461 248
        dictionaries will be returned corresponding to a collection of
paul@461 249
        datetimes. If 'tzid_name' is specified, the time zone information will
paul@461 250
        be acquired from a field starting with 'tzid_name' instead of 'name'.
paul@446 251
        """
paul@446 252
paul@446 253
        args = self.env.get_args()
paul@446 254
paul@446 255
        dates = args.get("%s-date" % name, [])
paul@446 256
        hours = args.get("%s-hour" % name, [])
paul@446 257
        minutes = args.get("%s-minute" % name, [])
paul@446 258
        seconds = args.get("%s-second" % name, [])
paul@461 259
        tzids = args.get("%s-tzid" % (tzid_name or name), [])
paul@446 260
paul@446 261
        # Handle absent values by employing None values.
paul@446 262
paul@446 263
        field_values = map(None, dates, hours, minutes, seconds, tzids)
paul@446 264
paul@498 265
        if not field_values and not multiple:
paul@498 266
            all_values = FormDate()
paul@498 267
        else:
paul@498 268
            all_values = []
paul@498 269
            for date, hour, minute, second, tzid in field_values:
paul@498 270
                value = FormDate(date, hour, minute, second, tzid or self.get_tzid())
paul@446 271
paul@498 272
                # Return a single value or append to a collection of all values.
paul@446 273
paul@498 274
                if not multiple:
paul@498 275
                    return value
paul@498 276
                else:
paul@498 277
                    all_values.append(value)
paul@446 278
paul@446 279
        return all_values
paul@446 280
paul@498 281
    def get_current_main_period(self, obj):
paul@498 282
        args = self.env.get_args()
paul@498 283
        initial_load = not args.has_key("editing")
paul@446 284
paul@498 285
        if initial_load or not self.is_organiser(obj):
paul@498 286
            return self.get_existing_main_period(obj)
paul@498 287
        else:
paul@498 288
            return self.get_main_period()
paul@446 289
paul@498 290
    def get_existing_main_period(self, obj):
paul@446 291
paul@446 292
        """
paul@498 293
        Return the main event period for the given 'obj'.
paul@446 294
        """
paul@446 295
paul@446 296
        dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")
paul@498 297
paul@446 298
        if obj.has_key("DTEND"):
paul@446 299
            dtend, dtend_attr = obj.get_datetime_item("DTEND")
paul@446 300
        elif obj.has_key("DURATION"):
paul@446 301
            duration = obj.get_duration("DURATION")
paul@446 302
            dtend = dtstart + duration
paul@446 303
            dtend_attr = dtstart_attr
paul@446 304
        else:
paul@446 305
            dtend, dtend_attr = dtstart, dtstart_attr
paul@498 306
paul@499 307
        return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr)
paul@498 308
paul@498 309
    def get_main_period(self):
paul@498 310
paul@498 311
        "Return the main period defined in the event form."
paul@498 312
paul@498 313
        args = self.env.get_args()
paul@498 314
paul@498 315
        dtend_enabled = args.get("dtend-control", [None])[0]
paul@498 316
        dttimes_enabled = args.get("dttimes-control", [None])[0]
paul@498 317
        start = self.get_date_control_values("dtstart")
paul@498 318
        end = self.get_date_control_values("dtend")
paul@498 319
paul@498 320
        return FormPeriod(start, end, dtend_enabled, dttimes_enabled)
paul@498 321
paul@498 322
    def get_current_recurrences(self, obj):
paul@498 323
        args = self.env.get_args()
paul@498 324
        initial_load = not args.has_key("editing")
paul@498 325
paul@498 326
        if initial_load or not self.is_organiser(obj):
paul@498 327
            return self.get_existing_recurrences(obj)
paul@498 328
        else:
paul@498 329
            return self.get_recurrences()
paul@498 330
paul@498 331
    def get_existing_recurrences(self, obj):
paul@498 332
        recurrences = []
paul@498 333
        for period in obj.get_periods(self.get_tzid(), self.get_window_end()):
paul@499 334
            if period.origin != "DTSTART":
paul@499 335
                recurrences.append(period)
paul@498 336
        return recurrences
paul@498 337
paul@498 338
    def get_recurrences(self):
paul@498 339
paul@498 340
        "Return the recurrences defined in the event form."
paul@498 341
paul@498 342
        args = self.env.get_args()
paul@498 343
paul@498 344
        all_dtend_enabled = args.get("dtend-control-recur", [])
paul@498 345
        all_dttimes_enabled = args.get("dttimes-control-recur", [])
paul@498 346
        all_starts = self.get_date_control_values("dtstart-recur", multiple=True)
paul@498 347
        all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")
paul@498 348
paul@498 349
        periods = []
paul@498 350
paul@498 351
        for index, (start, end, dtend_enabled, dttimes_enabled) in \
paul@498 352
            enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled)):
paul@498 353
paul@498 354
            dtend_enabled = str(index) in all_dtend_enabled
paul@498 355
            dttimes_enabled = str(index) in all_dttimes_enabled
paul@498 356
            period = FormPeriod(start, end, dtend_enabled, dttimes_enabled)
paul@498 357
            periods.append(period)
paul@498 358
paul@498 359
        return periods
paul@446 360
paul@484 361
    def get_current_attendees(self, obj):
paul@484 362
paul@484 363
        """
paul@484 364
        Return attendees for 'obj' depending on whether the object is being
paul@484 365
        edited.
paul@484 366
        """
paul@484 367
paul@484 368
        args = self.env.get_args()
paul@484 369
        initial_load = not args.has_key("editing")
paul@484 370
paul@484 371
        if initial_load or not self.is_organiser(obj):
paul@484 372
            return self.get_existing_attendees(obj)
paul@484 373
        else:
paul@484 374
            return self.get_attendees()
paul@484 375
paul@484 376
    def get_existing_attendees(self, obj):
paul@484 377
        return uri_values(obj.get_values("ATTENDEE") or [])
paul@484 378
paul@478 379
    def get_attendees(self):
paul@478 380
paul@478 381
        """
paul@478 382
        Return attendees from the request, normalised for iCalendar purposes,
paul@478 383
        and without duplicates.
paul@478 384
        """
paul@478 385
paul@478 386
        args = self.env.get_args()
paul@478 387
paul@478 388
        attendees = args.get("attendee", [])
paul@478 389
        unique_attendees = set()
paul@478 390
        ordered_attendees = []
paul@478 391
paul@478 392
        for attendee in attendees:
paul@484 393
            if not attendee.strip():
paul@484 394
                continue
paul@478 395
            attendee = get_uri(attendee)
paul@478 396
            if attendee not in unique_attendees:
paul@478 397
                unique_attendees.add(attendee)
paul@478 398
                ordered_attendees.append(attendee)
paul@478 399
paul@478 400
        return ordered_attendees
paul@478 401
paul@478 402
    def update_attendees(self, obj):
paul@477 403
paul@477 404
        "Add or remove attendees. This does not affect the stored object."
paul@477 405
paul@477 406
        args = self.env.get_args()
paul@477 407
paul@478 408
        attendees = self.get_attendees()
paul@485 409
        existing_attendees = self.get_existing_attendees(obj)
paul@485 410
        sequence = obj.get_value("SEQUENCE")
paul@477 411
paul@477 412
        if args.has_key("add"):
paul@477 413
            attendees.append("")
paul@477 414
paul@494 415
        # Only actually remove attendees if the event is unsent, if the attendee
paul@494 416
        # is new, or if it is the current user being removed.
paul@485 417
paul@477 418
        if args.has_key("remove"):
paul@485 419
            for i in args["remove"]:
paul@496 420
                try:
paul@496 421
                    attendee = attendees[int(i)]
paul@496 422
                except IndexError:
paul@496 423
                    continue
paul@496 424
paul@485 425
                existing = attendee in existing_attendees
paul@485 426
paul@494 427
                if not existing or sequence is None or attendee == self.user:
paul@485 428
                    attendees.remove(attendee)
paul@477 429
paul@477 430
        return attendees
paul@477 431
paul@446 432
    # Page fragment methods.
paul@446 433
paul@446 434
    def show_request_controls(self, obj):
paul@446 435
paul@446 436
        "Show form controls for a request concerning 'obj'."
paul@446 437
paul@446 438
        page = self.page
paul@446 439
        args = self.env.get_args()
paul@446 440
paul@484 441
        attendees = self.get_current_attendees(obj)
paul@446 442
        is_attendee = self.user in attendees
paul@446 443
paul@446 444
        is_request = (obj.get_value("UID"), obj.get_value("RECURRENCE-ID")) in self._get_requests()
paul@446 445
paul@446 446
        have_other_attendees = len(attendees) > (is_attendee and 1 or 0)
paul@446 447
paul@446 448
        # Show appropriate options depending on the role of the user.
paul@446 449
paul@484 450
        if is_attendee and not self.is_organiser(obj):
paul@446 451
            page.p("An action is required for this request:")
paul@446 452
paul@446 453
            page.p()
paul@446 454
            page.input(name="reply", type="submit", value="Send reply")
paul@446 455
            page.add(" ")
paul@446 456
            page.input(name="discard", type="submit", value="Discard event")
paul@446 457
            page.add(" ")
paul@446 458
            page.input(name="ignore", type="submit", value="Do nothing for now")
paul@446 459
            page.p.close()
paul@446 460
paul@484 461
        if self.is_organiser(obj):
paul@446 462
            page.p("As organiser, you can perform the following:")
paul@446 463
paul@446 464
            if have_other_attendees:
paul@446 465
                page.p()
paul@446 466
                page.input(name="invite", type="submit", value="Invite/notify attendees")
paul@446 467
                page.add(" ")
paul@446 468
                if is_request:
paul@446 469
                    page.input(name="discard", type="submit", value="Discard event")
paul@446 470
                else:
paul@446 471
                    page.input(name="cancel", type="submit", value="Cancel event")
paul@446 472
                page.add(" ")
paul@446 473
                page.input(name="ignore", type="submit", value="Do nothing for now")
paul@446 474
                page.p.close()
paul@446 475
            else:
paul@446 476
                page.p()
paul@446 477
                page.input(name="save", type="submit", value="Save event")
paul@446 478
                page.add(" ")
paul@446 479
                page.input(name="discard", type="submit", value="Discard event")
paul@446 480
                page.add(" ")
paul@446 481
                page.input(name="ignore", type="submit", value="Do nothing for now")
paul@446 482
                page.p.close()
paul@446 483
paul@492 484
    def show_object_on_page(self, uid, obj, errors=None):
paul@446 485
paul@446 486
        """
paul@446 487
        Show the calendar object with the given 'uid' and representation 'obj'
paul@492 488
        on the current page. If 'errors' is given, show a suitable message for
paul@492 489
        the different errors provided.
paul@446 490
        """
paul@446 491
paul@446 492
        page = self.page
paul@446 493
        page.form(method="POST")
paul@446 494
paul@446 495
        page.input(name="editing", type="hidden", value="true")
paul@446 496
paul@446 497
        args = self.env.get_args()
paul@446 498
paul@487 499
        # Obtain basic event information, generating any necessary editing controls.
paul@446 500
paul@473 501
        initial_load = not args.has_key("editing")
paul@446 502
paul@484 503
        if initial_load or not self.is_organiser(obj):
paul@484 504
            attendees = self.get_existing_attendees(obj)
paul@484 505
        else:
paul@484 506
            attendees = self.update_attendees(obj)
paul@446 507
paul@498 508
        p = self.get_current_main_period(obj)
paul@498 509
        self.show_object_datetime_controls(p)
paul@446 510
paul@487 511
        # Obtain any separate recurrences for this event.
paul@487 512
paul@487 513
        recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
paul@487 514
        recurrenceids = self._get_recurrences(uid)
paul@498 515
        start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
paul@487 516
        replaced = not recurrenceid and recurrenceids and start_utc in recurrenceids
paul@487 517
paul@446 518
        # Provide a summary of the object.
paul@446 519
paul@446 520
        page.table(class_="object", cellspacing=5, cellpadding=5)
paul@446 521
        page.thead()
paul@446 522
        page.tr()
paul@446 523
        page.th("Event", class_="mainheading", colspan=2)
paul@446 524
        page.tr.close()
paul@446 525
        page.thead.close()
paul@446 526
        page.tbody()
paul@446 527
paul@446 528
        for name, label in self.property_items:
paul@446 529
            field = name.lower()
paul@446 530
paul@446 531
            items = obj.get_items(name) or []
paul@446 532
            rowspan = len(items)
paul@446 533
paul@446 534
            if name == "ATTENDEE":
paul@473 535
                rowspan = len(attendees) + 1 # for the add button
paul@446 536
            elif not items:
paul@446 537
                continue
paul@446 538
paul@446 539
            page.tr()
paul@492 540
            page.th(label, class_="objectheading %s%s" % (field, errors and field in errors and " error" or ""), rowspan=rowspan)
paul@446 541
paul@446 542
            # Handle datetimes specially.
paul@446 543
paul@446 544
            if name in ["DTSTART", "DTEND"]:
paul@487 545
                if not replaced:
paul@446 546
paul@487 547
                    # Obtain the datetime.
paul@487 548
paul@498 549
                    is_start = name == "DTSTART"
paul@446 550
paul@487 551
                    # Where no end datetime exists, use the start datetime as the
paul@487 552
                    # basis of any potential datetime specified if dt-control is
paul@487 553
                    # set.
paul@487 554
paul@498 555
                    self.show_datetime_controls(obj, is_start and p.get_form_start() or p.get_form_end(), is_start)
paul@446 556
paul@487 557
                elif name == "DTSTART":
paul@487 558
                    page.td(class_="objectvalue %s replaced" % field, rowspan=2)
paul@487 559
                    page.a("First occurrence replaced by a separate event", href=self.link_to(uid, start_utc))
paul@487 560
                    page.td.close()
paul@446 561
paul@446 562
                page.tr.close()
paul@446 563
paul@446 564
            # Handle the summary specially.
paul@446 565
paul@446 566
            elif name == "SUMMARY":
paul@446 567
                value = args.get("summary", [obj.get_value(name)])[0]
paul@446 568
paul@490 569
                page.td(class_="objectvalue summary")
paul@484 570
                if self.is_organiser(obj):
paul@446 571
                    page.input(name="summary", type="text", value=value, size=80)
paul@446 572
                else:
paul@446 573
                    page.add(value)
paul@446 574
                page.td.close()
paul@446 575
                page.tr.close()
paul@446 576
paul@473 577
            # Handle attendees specially.
paul@473 578
paul@473 579
            elif name == "ATTENDEE":
paul@473 580
                attendee_map = dict(items)
paul@473 581
                first = True
paul@473 582
paul@473 583
                for i, value in enumerate(attendees):
paul@473 584
                    if not first:
paul@473 585
                        page.tr()
paul@473 586
                    else:
paul@473 587
                        first = False
paul@473 588
paul@479 589
                    # Obtain details of attendees to supply attributes.
paul@473 590
paul@479 591
                    self.show_attendee(obj, i, value, attendee_map.get(value))
paul@473 592
                    page.tr.close()
paul@473 593
paul@473 594
                # Allow more attendees to be specified.
paul@473 595
paul@484 596
                if self.is_organiser(obj):
paul@473 597
                    if not first:
paul@473 598
                        page.tr()
paul@473 599
paul@473 600
                    page.td()
paul@496 601
                    page.input(name="add", type="submit", value="add", id="add", class_="add")
paul@496 602
                    page.label("Add attendee", for_="add", class_="add")
paul@473 603
                    page.td.close()
paul@473 604
                    page.tr.close()
paul@473 605
paul@473 606
            # Handle potentially many values of other kinds.
paul@446 607
paul@446 608
            else:
paul@446 609
                first = True
paul@446 610
paul@446 611
                for i, (value, attr) in enumerate(items):
paul@446 612
                    if not first:
paul@446 613
                        page.tr()
paul@446 614
                    else:
paul@446 615
                        first = False
paul@446 616
paul@490 617
                    page.td(class_="objectvalue %s" % field)
paul@473 618
                    page.add(value)
paul@446 619
                    page.td.close()
paul@446 620
                    page.tr.close()
paul@446 621
paul@446 622
        page.tbody.close()
paul@446 623
        page.table.close()
paul@446 624
paul@492 625
        self.show_recurrences(obj, errors)
paul@446 626
        self.show_conflicting_events(uid, obj)
paul@446 627
        self.show_request_controls(obj)
paul@446 628
paul@446 629
        page.form.close()
paul@446 630
paul@479 631
    def show_attendee(self, obj, i, attendee, attendee_attr):
paul@479 632
paul@479 633
        """
paul@479 634
        For the given object 'obj', show the attendee in position 'i' with the
paul@479 635
        given 'attendee' value, having 'attendee_attr' as any stored attributes.
paul@479 636
        """
paul@479 637
paul@479 638
        page = self.page
paul@479 639
        args = self.env.get_args()
paul@479 640
paul@479 641
        existing = attendee_attr is not None
paul@479 642
        partstat = attendee_attr and attendee_attr.get("PARTSTAT")
paul@485 643
        sequence = obj.get_value("SEQUENCE")
paul@479 644
paul@479 645
        page.td(class_="objectvalue")
paul@479 646
paul@479 647
        # Show a form control as organiser for new attendees.
paul@479 648
paul@485 649
        if self.is_organiser(obj) and (not existing or sequence is None):
paul@479 650
            page.input(name="attendee", type="value", value=attendee, size="40")
paul@479 651
        else:
paul@479 652
            page.input(name="attendee", type="hidden", value=attendee)
paul@479 653
            page.add(attendee)
paul@479 654
        page.add(" ")
paul@479 655
paul@479 656
        # Show participation status, editable for the current user.
paul@479 657
paul@479 658
        if attendee == self.user:
paul@479 659
            self._show_menu("partstat", partstat, self.partstat_items, "partstat")
paul@479 660
paul@479 661
        # Allow the participation indicator to act as a submit
paul@479 662
        # button in order to refresh the page and show a control for
paul@479 663
        # the current user, if indicated.
paul@479 664
paul@484 665
        elif self.is_organiser(obj) and not existing:
paul@479 666
            page.input(name="partstat-refresh", type="submit", value="refresh", id="partstat-%d" % i, class_="refresh")
paul@479 667
            page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat")
paul@479 668
        else:
paul@479 669
            page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")
paul@479 670
paul@479 671
        # Permit organisers to remove attendees.
paul@479 672
paul@484 673
        if self.is_organiser(obj):
paul@479 674
paul@479 675
            # Permit the removal of newly-added attendees.
paul@479 676
paul@485 677
            remove_type = (not existing or sequence is None or attendee == self.user) and "submit" or "checkbox"
paul@479 678
paul@485 679
            self._control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove")
paul@479 680
paul@479 681
            page.label("Remove", for_="remove-%d" % i, class_="remove")
paul@479 682
            page.label("Uninvited", for_="remove-%d" % i, class_="removed")
paul@479 683
paul@479 684
        page.td.close()
paul@479 685
paul@492 686
    def show_recurrences(self, obj, errors=None):
paul@446 687
paul@492 688
        """
paul@492 689
        Show recurrences for the object having the given representation 'obj'.
paul@492 690
        If 'errors' is given, show a suitable message for the different errors
paul@492 691
        provided.
paul@492 692
        """
paul@446 693
paul@446 694
        page = self.page
paul@446 695
paul@446 696
        # Obtain any parent object if this object is a specific recurrence.
paul@446 697
paul@446 698
        uid = obj.get_value("UID")
paul@446 699
        recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
paul@446 700
paul@446 701
        if recurrenceid:
paul@489 702
            parent = self._get_object(uid)
paul@489 703
            if not parent:
paul@446 704
                return
paul@446 705
paul@489 706
            page.p()
paul@489 707
            page.a("This event modifies a recurring event.", href=self.link_to(uid))
paul@489 708
            page.p.close()
paul@446 709
paul@499 710
        # Obtain the periods associated with the event.
paul@446 711
paul@498 712
        recurrences = self.get_current_recurrences(obj)
paul@446 713
paul@499 714
        if len(recurrences) < 1:
paul@446 715
            return
paul@446 716
paul@498 717
        recurrenceids = self._get_recurrences(uid)
paul@446 718
paul@499 719
        page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
paul@499 720
paul@446 721
        # Show each recurrence in a separate table if editable.
paul@446 722
paul@498 723
        if self.is_organiser(obj) and recurrences:
paul@446 724
paul@498 725
            for index, p in enumerate(recurrences):
paul@446 726
paul@446 727
                # Isolate the controls from neighbouring tables.
paul@446 728
paul@446 729
                page.div()
paul@446 730
paul@498 731
                self.show_object_datetime_controls(p, index)
paul@446 732
paul@446 733
                page.table(cellspacing=5, cellpadding=5, class_="recurrence")
paul@446 734
                page.caption("Occurrence")
paul@446 735
                page.tbody()
paul@499 736
paul@446 737
                page.tr()
paul@492 738
                error = errors and ("dtstart", index) in errors and " error" or ""
paul@492 739
                page.th("Start", class_="objectheading start%s" % error)
paul@494 740
                self.show_recurrence_controls(obj, index, p, recurrenceid, recurrenceids, True)
paul@446 741
                page.tr.close()
paul@446 742
                page.tr()
paul@492 743
                error = errors and ("dtend", index) in errors and " error" or ""
paul@492 744
                page.th("End", class_="objectheading end%s" % error)
paul@494 745
                self.show_recurrence_controls(obj, index, p, recurrenceid, recurrenceids, False)
paul@446 746
                page.tr.close()
paul@499 747
paul@446 748
                page.tbody.close()
paul@446 749
                page.table.close()
paul@446 750
paul@446 751
                page.div.close()
paul@446 752
paul@446 753
        # Otherwise, use a compact single table.
paul@446 754
paul@499 755
        else:
paul@499 756
            page.table(cellspacing=5, cellpadding=5, class_="recurrence")
paul@499 757
            page.caption("Occurrences")
paul@499 758
            page.thead()
paul@499 759
            page.tr()
paul@499 760
            page.th("Start", class_="objectheading start")
paul@499 761
            page.th("End", class_="objectheading end")
paul@499 762
            page.tr.close()
paul@499 763
            page.thead.close()
paul@499 764
            page.tbody()
paul@446 765
paul@499 766
            for index, p in enumerate(recurrences):
paul@499 767
                page.tr()
paul@499 768
                self.show_recurrence_label(p, recurrenceid, recurrenceids, True)
paul@499 769
                self.show_recurrence_label(p, recurrenceid, recurrenceids, False)
paul@499 770
                page.tr.close()
paul@446 771
paul@499 772
            page.tbody.close()
paul@499 773
            page.table.close()
paul@446 774
paul@446 775
    def show_conflicting_events(self, uid, obj):
paul@446 776
paul@446 777
        """
paul@446 778
        Show conflicting events for the object having the given 'uid' and
paul@446 779
        representation 'obj'.
paul@446 780
        """
paul@446 781
paul@446 782
        page = self.page
paul@484 783
        recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
paul@446 784
paul@446 785
        # Obtain the user's timezone.
paul@446 786
paul@446 787
        tzid = self.get_tzid()
paul@446 788
        periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())
paul@446 789
paul@446 790
        # Indicate whether there are conflicting events.
paul@446 791
paul@484 792
        conflicts = []
paul@446 793
paul@484 794
        for participant in self.get_current_attendees(obj):
paul@484 795
            if participant == self.user:
paul@484 796
                freebusy = self.store.get_freebusy(participant)
paul@484 797
            else:
paul@484 798
                freebusy = self.store.get_freebusy_for_other(self.user, participant)
paul@484 799
paul@484 800
            if not freebusy:
paul@484 801
                continue
paul@446 802
paul@446 803
            # Obtain any time zone details from the suggested event.
paul@446 804
paul@446 805
            _dtstart, attr = obj.get_item("DTSTART")
paul@446 806
            tzid = attr.get("TZID", tzid)
paul@446 807
paul@484 808
            # Show any conflicts with periods of actual attendance.
paul@446 809
paul@484 810
            for p in have_conflict(freebusy, periods, True):
paul@500 811
                if (p.uid != uid or (recurrenceid and p.recurrenceid) and p.recurrenceid != recurrenceid) and p.transp != "ORG":
paul@484 812
                    conflicts.append(p)
paul@446 813
paul@484 814
        conflicts.sort()
paul@484 815
paul@484 816
        # Show any conflicts with periods of actual attendance.
paul@446 817
paul@484 818
        if conflicts:
paul@484 819
            page.p("This event conflicts with others:")
paul@446 820
paul@484 821
            page.table(cellspacing=5, cellpadding=5, class_="conflicts")
paul@484 822
            page.thead()
paul@484 823
            page.tr()
paul@484 824
            page.th("Event")
paul@484 825
            page.th("Start")
paul@484 826
            page.th("End")
paul@484 827
            page.tr.close()
paul@484 828
            page.thead.close()
paul@484 829
            page.tbody()
paul@446 830
paul@484 831
            for p in conflicts:
paul@484 832
paul@484 833
                # Provide details of any conflicting event.
paul@446 834
paul@484 835
                start = self.format_datetime(to_timezone(get_datetime(p.start), tzid), "long")
paul@484 836
                end = self.format_datetime(to_timezone(get_datetime(p.end), tzid), "long")
paul@446 837
paul@484 838
                page.tr()
paul@446 839
paul@484 840
                # Show the event summary for the conflicting event.
paul@446 841
paul@484 842
                page.td()
paul@484 843
                if p.summary:
paul@488 844
                    page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid))
paul@484 845
                else:
paul@484 846
                    page.add("(Unspecified event)")
paul@484 847
                page.td.close()
paul@446 848
paul@484 849
                page.td(start)
paul@484 850
                page.td(end)
paul@446 851
paul@484 852
                page.tr.close()
paul@446 853
paul@484 854
            page.tbody.close()
paul@484 855
            page.table.close()
paul@446 856
paul@474 857
    # Generation of controls within page fragments.
paul@474 858
paul@498 859
    def show_object_datetime_controls(self, period, index=None):
paul@474 860
paul@474 861
        """
paul@474 862
        Show datetime-related controls if already active or if an object needs
paul@498 863
        them for the given 'period'. The given 'index' is used to parameterise
paul@498 864
        individual controls for dynamic manipulation.
paul@474 865
        """
paul@474 866
paul@499 867
        p = form_period_from_period(period)
paul@498 868
paul@474 869
        page = self.page
paul@474 870
        args = self.env.get_args()
paul@474 871
        sn = self._suffixed_name
paul@474 872
        ssn = self._simple_suffixed_name
paul@474 873
paul@474 874
        # Add a dynamic stylesheet to permit the controls to modify the display.
paul@474 875
        # NOTE: The style details need to be coordinated with the static
paul@474 876
        # NOTE: stylesheet.
paul@474 877
paul@474 878
        if index is not None:
paul@474 879
            page.style(type="text/css")
paul@474 880
paul@474 881
            # Unlike the rules for object properties, these affect recurrence
paul@474 882
            # properties.
paul@474 883
paul@474 884
            page.add("""\
paul@474 885
input#dttimes-enable-%(index)d,
paul@474 886
input#dtend-enable-%(index)d,
paul@474 887
input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
paul@474 888
input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
paul@474 889
input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
paul@474 890
input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
paul@474 891
    display: none;
paul@474 892
}""" % {"index" : index})
paul@474 893
paul@474 894
            page.style.close()
paul@474 895
paul@474 896
        self._control(
paul@474 897
            ssn("dtend-control", "recur", index), "checkbox",
paul@498 898
            index is not None and str(index) or "enable", p.end_enabled,
paul@474 899
            id=sn("dtend-enable", index)
paul@474 900
            )
paul@474 901
paul@474 902
        self._control(
paul@474 903
            ssn("dttimes-control", "recur", index), "checkbox",
paul@498 904
            index is not None and str(index) or "enable", p.times_enabled,
paul@474 905
            id=sn("dttimes-enable", index)
paul@474 906
            )
paul@474 907
paul@498 908
    def show_datetime_controls(self, obj, formdate, show_start):
paul@474 909
paul@474 910
        """
paul@498 911
        Show datetime details from the given 'obj' for the 'formdate', showing
paul@498 912
        start details if 'show_start' is set to a true value. Details will
paul@498 913
        appear as controls for organisers and labels for attendees.
paul@474 914
        """
paul@474 915
paul@474 916
        page = self.page
paul@474 917
paul@474 918
        # Show controls for editing as organiser.
paul@474 919
paul@484 920
        if self.is_organiser(obj):
paul@474 921
            page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@474 922
paul@474 923
            if show_start:
paul@474 924
                page.div(class_="dt enabled")
paul@498 925
                self._show_date_controls("dtstart", formdate)
paul@474 926
                page.br()
paul@474 927
                page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
paul@474 928
                page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
paul@474 929
                page.div.close()
paul@474 930
paul@474 931
            else:
paul@474 932
                page.div(class_="dt disabled")
paul@474 933
                page.label("Specify end date", for_="dtend-enable", class_="enable")
paul@474 934
                page.div.close()
paul@474 935
                page.div(class_="dt enabled")
paul@498 936
                self._show_date_controls("dtend", formdate)
paul@474 937
                page.br()
paul@474 938
                page.label("End on same day", for_="dtend-enable", class_="disable")
paul@474 939
                page.div.close()
paul@474 940
paul@474 941
            page.td.close()
paul@474 942
paul@474 943
        # Show a label as attendee.
paul@474 944
paul@474 945
        else:
paul@498 946
            t = formdate.as_datetime_item()
paul@498 947
            if t:
paul@498 948
                dt, attr = t
paul@498 949
                page.td(self.format_datetime(dt, "full"))
paul@498 950
            else:
paul@498 951
                page.td("(Unrecognised date)")
paul@474 952
paul@494 953
    def show_recurrence_controls(self, obj, index, period, recurrenceid, recurrenceids, show_start):
paul@474 954
paul@474 955
        """
paul@474 956
        Show datetime details from the given 'obj' for the recurrence having the
paul@494 957
        given 'index', with the recurrence period described by 'period',
paul@494 958
        indicating a start, end and origin of the period from the event details,
paul@494 959
        employing any 'recurrenceid' and 'recurrenceids' for the object to
paul@494 960
        configure the displayed information.
paul@474 961
paul@474 962
        If 'show_start' is set to a true value, the start details will be shown;
paul@474 963
        otherwise, the end details will be shown.
paul@474 964
        """
paul@474 965
paul@474 966
        page = self.page
paul@474 967
        sn = self._suffixed_name
paul@474 968
        ssn = self._simple_suffixed_name
paul@499 969
paul@499 970
        p = event_period_from_period(period)
paul@474 971
paul@498 972
        start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
paul@474 973
        replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
paul@474 974
paul@474 975
        # Show controls for editing as organiser.
paul@474 976
paul@498 977
        if self.is_organiser(obj) and not replaced:
paul@474 978
            page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@474 979
paul@474 980
            if show_start:
paul@474 981
                page.div(class_="dt enabled")
paul@498 982
                self._show_date_controls(ssn("dtstart", "recur", index), p.get_form_start(), index=index)
paul@474 983
                page.br()
paul@474 984
                page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable")
paul@474 985
                page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable")
paul@474 986
                page.div.close()
paul@474 987
paul@474 988
            else:
paul@474 989
                page.div(class_="dt disabled")
paul@474 990
                page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable")
paul@474 991
                page.div.close()
paul@474 992
                page.div(class_="dt enabled")
paul@498 993
                self._show_date_controls(ssn("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False)
paul@474 994
                page.br()
paul@474 995
                page.label("End on same day", for_=sn("dtend-enable", index), class_="disable")
paul@474 996
                page.div.close()
paul@474 997
paul@474 998
            page.td.close()
paul@474 999
paul@474 1000
        # Show label as attendee.
paul@474 1001
paul@474 1002
        else:
paul@498 1003
            self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start)
paul@498 1004
paul@499 1005
    def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start):
paul@498 1006
paul@498 1007
        """
paul@499 1008
        Show datetime details for the given 'period', employing any
paul@498 1009
        'recurrenceid' and 'recurrenceids' for the object to configure the
paul@498 1010
        displayed information.
paul@498 1011
paul@498 1012
        If 'show_start' is set to a true value, the start details will be shown;
paul@498 1013
        otherwise, the end details will be shown.
paul@498 1014
        """
paul@498 1015
paul@498 1016
        page = self.page
paul@498 1017
paul@499 1018
        p = event_period_from_period(period)
paul@499 1019
paul@498 1020
        start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
paul@498 1021
        replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
paul@498 1022
        css = " ".join([
paul@498 1023
            replaced,
paul@498 1024
            recurrenceid and start_utc == recurrenceid and "affected" or ""
paul@498 1025
            ])
paul@498 1026
paul@498 1027
        formdate = show_start and p.get_form_start() or p.get_form_end()
paul@498 1028
        t = formdate.as_datetime_item()
paul@498 1029
        if t:
paul@498 1030
            dt, attr = t
paul@498 1031
            page.td(self.format_datetime(dt, "long"), class_=css)
paul@498 1032
        else:
paul@498 1033
            page.td("(Unrecognised date)")
paul@474 1034
paul@446 1035
    # Full page output methods.
paul@446 1036
paul@446 1037
    def show(self, path_info):
paul@446 1038
paul@446 1039
        "Show an object request using the given 'path_info' for the current user."
paul@446 1040
paul@446 1041
        uid, recurrenceid = self._get_identifiers(path_info)
paul@446 1042
        obj = self._get_object(uid, recurrenceid)
paul@446 1043
paul@446 1044
        if not obj:
paul@446 1045
            return False
paul@446 1046
paul@492 1047
        errors = self.handle_request(uid, recurrenceid, obj)
paul@446 1048
paul@492 1049
        if not errors:
paul@446 1050
            return True
paul@446 1051
paul@446 1052
        self.new_page(title="Event")
paul@492 1053
        self.show_object_on_page(uid, obj, errors)
paul@446 1054
paul@446 1055
        return True
paul@446 1056
paul@446 1057
    # Utility methods.
paul@446 1058
paul@474 1059
    def _control(self, name, type, value, selected, **kw):
paul@474 1060
paul@474 1061
        """
paul@474 1062
        Show a control with the given 'name', 'type' and 'value', with
paul@474 1063
        'selected' indicating whether it should be selected (checked or
paul@474 1064
        equivalent), and with keyword arguments setting other properties.
paul@474 1065
        """
paul@474 1066
paul@474 1067
        page = self.page
paul@474 1068
        if selected:
paul@474 1069
            page.input(name=name, type=type, value=value, checked=selected, **kw)
paul@474 1070
        else:
paul@474 1071
            page.input(name=name, type=type, value=value, **kw)
paul@474 1072
paul@446 1073
    def _show_menu(self, name, default, items, class_="", index=None):
paul@446 1074
paul@446 1075
        """
paul@446 1076
        Show a select menu having the given 'name', set to the given 'default',
paul@446 1077
        providing the given (value, label) 'items', and employing the given CSS
paul@446 1078
        'class_' if specified.
paul@446 1079
        """
paul@446 1080
paul@446 1081
        page = self.page
paul@446 1082
        values = self.env.get_args().get(name, [default])
paul@446 1083
        if index is not None:
paul@446 1084
            values = values[index:]
paul@446 1085
            values = values and values[0:1] or [default]
paul@446 1086
paul@446 1087
        page.select(name=name, class_=class_)
paul@446 1088
        for v, label in items:
paul@446 1089
            if v is None:
paul@446 1090
                continue
paul@446 1091
            if v in values:
paul@446 1092
                page.option(label, value=v, selected="selected")
paul@446 1093
            else:
paul@446 1094
                page.option(label, value=v)
paul@446 1095
        page.select.close()
paul@446 1096
paul@498 1097
    def _show_date_controls(self, name, default, index=None, show_tzid=True):
paul@446 1098
paul@446 1099
        """
paul@498 1100
        Show date controls for a field with the given 'name' and 'default' form
paul@498 1101
        date value.
paul@498 1102
paul@498 1103
        If 'index' is specified, default field values will be overridden by the
paul@498 1104
        element from a collection of existing form values with the specified
paul@498 1105
        index; otherwise, field values will be overridden by a single form
paul@498 1106
        value.
paul@461 1107
paul@461 1108
        If 'show_tzid' is set to a false value, the time zone menu will not be
paul@461 1109
        provided.
paul@446 1110
        """
paul@446 1111
paul@446 1112
        page = self.page
paul@446 1113
paul@446 1114
        # Show dates for up to one week around the current date.
paul@446 1115
paul@498 1116
        t = default.as_datetime_item()
paul@498 1117
        if t:
paul@498 1118
            dt, attr = t
paul@498 1119
        else:
paul@498 1120
            dt = date.today()
paul@498 1121
paul@498 1122
        base = to_date(dt)
paul@446 1123
        items = []
paul@446 1124
        for i in range(-7, 8):
paul@446 1125
            d = base + timedelta(i)
paul@446 1126
            items.append((format_datetime(d), self.format_date(d, "full")))
paul@446 1127
paul@446 1128
        self._show_menu("%s-date" % name, format_datetime(base), items, index=index)
paul@446 1129
paul@446 1130
        # Show time details.
paul@446 1131
paul@498 1132
        page.span(class_="time enabled")
paul@498 1133
        page.input(name="%s-hour" % name, type="text", value=default.get_hour(), maxlength=2, size=2)
paul@498 1134
        page.add(":")
paul@498 1135
        page.input(name="%s-minute" % name, type="text", value=default.get_minute(), maxlength=2, size=2)
paul@498 1136
        page.add(":")
paul@498 1137
        page.input(name="%s-second" % name, type="text", value=default.get_second(), maxlength=2, size=2)
paul@446 1138
paul@461 1139
        if show_tzid:
paul@461 1140
            page.add(" ")
paul@498 1141
            tzid = default.get_tzid() or self.get_tzid()
paul@461 1142
            self._show_timezone_menu("%s-tzid" % name, tzid, index)
paul@498 1143
paul@446 1144
        page.span.close()
paul@446 1145
paul@446 1146
    def _show_timezone_menu(self, name, default, index=None):
paul@446 1147
paul@446 1148
        """
paul@446 1149
        Show timezone controls using a menu with the given 'name', set to the
paul@446 1150
        given 'default' unless a field of the given 'name' provides a value.
paul@446 1151
        """
paul@446 1152
paul@446 1153
        entries = [(tzid, tzid) for tzid in pytz.all_timezones]
paul@446 1154
        self._show_menu(name, default, entries, index=index)
paul@446 1155
paul@446 1156
# vim: tabstop=4 expandtab shiftwidth=4