imip-agent

Annotated imipweb/event.py

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