imip-agent

Annotated imipweb/event.py

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