imip-agent

Annotated imipweb/event.py

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