imip-agent

imipweb/event.py

783:2c7a4ff3b8a8
2015-09-28 Paul Boddie Fixed recurrence cancellation support. imipweb-client-simplification
     1 #!/usr/bin/env python     2      3 """     4 A Web interface to a calendar event.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from imiptools.data import get_uri, uri_dict, uri_items, uri_values    23 from imiptools.dates import format_datetime, to_timezone    24 from imiptools.mail import Messenger    25 from imiptools.period import have_conflict    26 from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError    27 from imipweb.client import ManagerClient    28 from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject    29     30 class EventPageFragment(ResourceClientForObject, DateTimeFormUtilities, FormUtilities):    31     32     "A resource presenting the details of an event."    33     34     def __init__(self, resource=None):    35         ResourceClientForObject.__init__(self, resource)    36     37     # Various property values and labels.    38     39     property_items = [    40         ("SUMMARY", "Summary"),    41         ("DTSTART", "Start"),    42         ("DTEND", "End"),    43         ("ORGANIZER", "Organiser"),    44         ("ATTENDEE", "Attendee"),    45         ]    46     47     partstat_items = [    48         ("NEEDS-ACTION", "Not confirmed"),    49         ("ACCEPTED", "Attending"),    50         ("TENTATIVE", "Tentatively attending"),    51         ("DECLINED", "Not attending"),    52         ("DELEGATED", "Delegated"),    53         (None, "Not indicated"),    54         ]    55     56     def can_remove_recurrence(self, recurrence):    57     58         """    59         Return whether the 'recurrence' can be removed from the current object    60         without notification.    61         """    62     63         return self.recurrence_is_new(recurrence) or not self.obj.is_shared()    64     65     def recurrence_is_new(self, recurrence):    66     67         "Return whether 'recurrence' is new to the current object."    68     69         return recurrence not in self.get_stored_recurrences()    70     71     def can_remove_attendee(self, attendee):    72     73         """    74         Return whether 'attendee' can be removed from the current object without    75         notification.    76         """    77     78         return self.can_edit_attendee(attendee) or attendee == self.user    79     80     def can_edit_attendee(self, attendee):    81     82         "Return whether 'attendee' can be edited by an organiser."    83     84         return self.attendee_is_new(attendee) or not self.obj.is_shared()    85     86     def attendee_is_new(self, attendee):    87     88         "Return whether 'attendee' is new to the current object."    89     90         return attendee not in self.get_stored_attendees()    91     92     # Access to stored object information.    93     94     def is_organiser(self):    95         return get_uri(self.obj.get_value("ORGANIZER")) == self.user    96     97     def get_stored_attendees(self):    98         return uri_values(self.obj.get_values("ATTENDEE") or [])    99    100     def get_stored_main_period(self):   101    102         "Return the main event period for the current object."   103    104         (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid())   105         return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr)   106    107     def get_stored_recurrences(self):   108    109         "Return recurrences computed using the current object."   110    111         recurrences = []   112         for period in self.get_periods(self.obj):   113             if period.origin != "DTSTART":   114                 recurrences.append(period)   115         return recurrences   116    117     # Access to current object information.   118    119     def get_current_main_period(self):   120         return self.get_stored_main_period()   121    122     def get_current_recurrences(self):   123         return self.get_stored_recurrences()   124    125     def get_current_attendees(self):   126         return self.get_stored_attendees()   127    128     # Page fragment methods.   129    130     def show_request_controls(self):   131    132         "Show form controls for a request."   133    134         page = self.page   135         args = self.env.get_args()   136    137         attendees = self.get_current_attendees()   138         is_attendee = self.user in attendees   139         is_request = self._have_request(self.uid, self.recurrenceid)   140    141         # Show appropriate options depending on the role of the user.   142    143         if is_attendee and not self.is_organiser():   144             page.p("An action is required for this request:")   145    146             page.p()   147             self.control("reply", "submit", "Send reply")   148             page.add(" ")   149             self.control("discard", "submit", "Discard event")   150             page.add(" ")   151             self.control("ignore", "submit", "Do nothing for now")   152             page.p.close()   153    154         if self.is_organiser():   155             page.p("As organiser, you can perform the following:")   156    157             page.p()   158             self.control("create", "submit", not self.obj.is_shared() and "Create event" or "Update event")   159             page.add(" ")   160    161             if self.obj.is_shared() and not is_request:   162                 self.control("cancel", "submit", "Cancel event")   163             else:   164                 self.control("discard", "submit", "Discard event")   165    166             page.add(" ")   167             self.control("save", "submit", "Save without sending")   168             page.p.close()   169    170     def show_object_on_page(self, errors=None):   171    172         """   173         Show the calendar object on the current page. If 'errors' is given, show   174         a suitable message for the different errors provided.   175         """   176    177         page = self.page   178         page.form(method="POST")   179    180         # Add a hidden control to help determine whether editing has already begun.   181    182         self.control("editing", "hidden", "true")   183    184         args = self.env.get_args()   185    186         # Obtain basic event information, generating any necessary editing controls.   187    188         attendees = self.update_current_attendees()   189         period = self.get_current_main_period()   190         self.show_object_datetime_controls(period)   191    192         # Obtain any separate recurrences for this event.   193    194         recurrenceids = self._get_active_recurrences(self.uid)   195         replaced = not self.recurrenceid and period.is_replaced(recurrenceids)   196    197         # Provide a summary of the object.   198    199         page.table(class_="object", cellspacing=5, cellpadding=5)   200         page.thead()   201         page.tr()   202         page.th("Event", class_="mainheading", colspan=2)   203         page.tr.close()   204         page.thead.close()   205         page.tbody()   206    207         for name, label in self.property_items:   208             field = name.lower()   209    210             items = uri_items(self.obj.get_items(name) or [])   211             rowspan = len(items)   212    213             if name == "ATTENDEE":   214                 rowspan = len(attendees) + 1 # for the add button   215             elif not items:   216                 continue   217    218             page.tr()   219             page.th(label, class_="objectheading %s%s" % (field, errors and field in errors and " error" or ""), rowspan=rowspan)   220    221             # Handle datetimes specially.   222    223             if name in ["DTSTART", "DTEND"]:   224                 if not replaced:   225    226                     # Obtain the datetime.   227    228                     is_start = name == "DTSTART"   229    230                     # Where no end datetime exists, use the start datetime as the   231                     # basis of any potential datetime specified if dt-control is   232                     # set.   233    234                     self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start)   235    236                 elif name == "DTSTART":   237                     page.td(class_="objectvalue %s replaced" % field, rowspan=2)   238                     page.a("First occurrence replaced by a separate event", href=self.link_to(self.uid, replaced))   239                     page.td.close()   240    241                 page.tr.close()   242    243             # Handle the summary specially.   244    245             elif name == "SUMMARY":   246                 value = args.get("summary", [self.obj.get_value(name)])[0]   247    248                 page.td(class_="objectvalue summary")   249                 if self.is_organiser():   250                     self.control("summary", "text", value, size=80)   251                 else:   252                     page.add(value)   253                 page.td.close()   254                 page.tr.close()   255    256             # Handle attendees specially.   257    258             elif name == "ATTENDEE":   259                 attendee_map = dict(items)   260                 first = True   261    262                 for i, value in enumerate(attendees):   263                     if not first:   264                         page.tr()   265                     else:   266                         first = False   267    268                     # Obtain details of attendees to supply attributes.   269    270                     self.show_attendee(i, value, attendee_map.get(value))   271                     page.tr.close()   272    273                 # Allow more attendees to be specified.   274    275                 if self.is_organiser():   276                     if not first:   277                         page.tr()   278    279                     page.td()   280                     self.control("add", "submit", "add", id="add", class_="add")   281                     page.label("Add attendee", for_="add", class_="add")   282                     page.td.close()   283                     page.tr.close()   284    285             # Handle potentially many values of other kinds.   286    287             else:   288                 first = True   289    290                 for i, (value, attr) in enumerate(items):   291                     if not first:   292                         page.tr()   293                     else:   294                         first = False   295    296                     page.td(class_="objectvalue %s" % field)   297                     page.add(value)   298                     page.td.close()   299                     page.tr.close()   300    301         page.tbody.close()   302         page.table.close()   303    304         self.show_recurrences(errors)   305         self.show_counters()   306         self.show_conflicting_events()   307         self.show_request_controls()   308    309         page.form.close()   310    311     def show_attendee(self, i, attendee, attendee_attr):   312    313         """   314         For the current object, show the attendee in position 'i' with the given   315         'attendee' value, having 'attendee_attr' as any stored attributes.   316         """   317    318         page = self.page   319         args = self.env.get_args()   320    321         partstat = attendee_attr and attendee_attr.get("PARTSTAT")   322    323         page.td(class_="objectvalue")   324    325         # Show a form control as organiser for new attendees.   326    327         if self.is_organiser() and self.can_edit_attendee(attendee):   328             self.control("attendee", "value", attendee, size="40")   329         else:   330             self.control("attendee", "hidden", attendee)   331             page.add(attendee)   332         page.add(" ")   333    334         # Show participation status, editable for the current user.   335    336         if attendee == self.user:   337             self.menu("partstat", partstat, self.partstat_items, "partstat")   338    339         # Allow the participation indicator to act as a submit   340         # button in order to refresh the page and show a control for   341         # the current user, if indicated.   342    343         elif self.is_organiser() and self.attendee_is_new(attendee):   344             self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh")   345             page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat")   346    347         # Otherwise, just show a label with the participation status.   348    349         else:   350             page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")   351    352         # Permit organisers to remove attendees.   353    354         if self.is_organiser():   355    356             # Permit the removal of newly-added attendees.   357    358             remove_type = self.can_remove_attendee(attendee) and "submit" or "checkbox"   359             self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove")   360    361             page.label("Remove", for_="remove-%d" % i, class_="remove")   362             page.label(for_="remove-%d" % i, class_="removed")   363             page.add("(Uninvited)")   364             page.span("Re-invite", class_="action")   365             page.label.close()   366    367         page.td.close()   368    369     def show_recurrences(self, errors=None):   370    371         """   372         Show recurrences for the current object. If 'errors' is given, show a   373         suitable message for the different errors provided.   374         """   375    376         page = self.page   377    378         # Obtain any parent object if this object is a specific recurrence.   379    380         if self.recurrenceid:   381             parent = self.get_stored_object(self.uid, None)   382             if not parent:   383                 return   384    385             page.p()   386             page.a("This event modifies a recurring event.", href=self.link_to(self.uid))   387             page.p.close()   388    389         # Obtain the periods associated with the event.   390         # NOTE: Add a control to add recurrences here.   391    392         recurrences = self.update_current_recurrences()   393    394         if len(recurrences) < 1:   395             return   396    397         recurrenceids = self._get_recurrences(self.uid)   398    399         page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())   400    401         # Show each recurrence in a separate table if editable.   402    403         if self.is_organiser() and recurrences:   404    405             for index, period in enumerate(recurrences):   406                 self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors)   407    408         # Otherwise, use a compact single table.   409    410         else:   411             page.table(cellspacing=5, cellpadding=5, class_="recurrence")   412             page.caption("Occurrences")   413             page.thead()   414             page.tr()   415             page.th("Start", class_="objectheading start")   416             page.th("End", class_="objectheading end")   417             page.tr.close()   418             page.thead.close()   419             page.tbody()   420    421             for index, period in enumerate(recurrences):   422                 page.tr()   423                 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True)   424                 self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False)   425                 page.tr.close()   426    427             page.tbody.close()   428             page.table.close()   429    430     def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None):   431    432         """   433         Show recurrence controls for a recurrence provided by the current object   434         with the given 'index' position in the list of periods, the given   435         'period' details, where a 'recurrenceid' indicates any specific   436         recurrence, and where 'recurrenceids' indicates all known additional   437         recurrences for the object.   438    439         If 'errors' is given, show a suitable message for the different errors   440         provided.   441         """   442    443         page = self.page   444         args = self.env.get_args()   445    446         p = event_period_from_period(period)   447         replaced = not recurrenceid and p.is_replaced(recurrenceids)   448    449         # Isolate the controls from neighbouring tables.   450    451         page.div()   452    453         self.show_object_datetime_controls(period, index)   454    455         page.table(cellspacing=5, cellpadding=5, class_="recurrence")   456         page.caption(period.origin == "RRULE" and "Occurrence from rule" or "Occurrence")   457         page.tbody()   458    459         page.tr()   460         error = errors and ("dtstart", index) in errors and " error" or ""   461         page.th("Start", class_="objectheading start%s" % error)   462         self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True)   463         page.tr.close()   464         page.tr()   465         error = errors and ("dtend", index) in errors and " error" or ""   466         page.th("End", class_="objectheading end%s" % error)   467         self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False)   468         page.tr.close()   469    470         # Permit the removal of recurrences.   471    472         if not replaced:   473             page.tr()   474             page.th("")   475             page.td()   476    477             remove_type = (not self.obj.is_shared() or not period.origin) and "submit" or "checkbox"   478             self.control("recur-remove", remove_type, str(index),   479                 str(index) in args.get("recur-remove", []),   480                 id="recur-remove-%d" % index, class_="remove")   481    482             page.label("Remove", for_="recur-remove-%d" % index, class_="remove")   483             page.label(for_="recur-remove-%d" % index, class_="removed")   484             page.add("(Removed)")   485             page.span("Re-add", class_="action")   486             page.label.close()   487    488             page.td.close()   489             page.tr.close()   490    491         page.tbody.close()   492         page.table.close()   493    494         page.div.close()   495    496     def show_counters(self):   497    498         "Show any counter-proposals for the current object."   499    500         page = self.page   501         query = self.env.get_query()   502         counter = query.get("counter", [None])[0]   503    504         attendees = self._get_counters(self.uid, self.recurrenceid)   505         tzid = self.get_tzid()   506    507         if not attendees:   508             return   509    510         page.p("The following counter-proposals have been received for this event:")   511    512         page.table(cellspacing=5, cellpadding=5, class_="counters")   513         page.thead()   514         page.tr()   515         page.th("Attendee", rowspan=2)   516         page.th("Periods", colspan=2)   517         page.tr.close()   518         page.tr()   519         page.th("Start")   520         page.th("End")   521         page.tr.close()   522         page.thead.close()   523         page.tbody()   524    525         for attendee in attendees:   526             obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)   527             periods = self.get_periods(obj)   528    529             first = True   530             for p in periods:   531                 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point()))   532                 css = identifier == counter and "selected" or ""   533    534                 if first:   535                     page.tr(rowspan=len(periods), class_=css)   536                     page.td(attendee)   537                     first = False   538                 else:   539                     page.tr(class_=css)   540    541                 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long")   542                 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long")   543    544                 page.td(start)   545                 page.td(end)   546    547                 page.tr.close()   548    549         page.tbody.close()   550         page.table.close()   551    552     def show_conflicting_events(self):   553    554         "Show conflicting events for the current object."   555    556         page = self.page   557         recurrenceids = self._get_active_recurrences(self.uid)   558    559         # Obtain the user's timezone.   560    561         tzid = self.get_tzid()   562         periods = self.get_periods(self.obj)   563    564         # Indicate whether there are conflicting events.   565    566         conflicts = []   567         attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))   568    569         for participant in self.get_current_attendees():   570             if participant == self.user:   571                 freebusy = self.store.get_freebusy(participant)   572             else:   573                 freebusy = self.store.get_freebusy_for_other(self.user, participant)   574    575             if not freebusy:   576                 continue   577    578             # Obtain any time zone details from the suggested event.   579    580             _dtstart, attr = self.obj.get_item("DTSTART")   581             tzid = attr.get("TZID", tzid)   582    583             # Show any conflicts with periods of actual attendance.   584    585             participant_attr = attendee_map.get(participant)   586             partstat = participant_attr and participant_attr.get("PARTSTAT")   587             recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid)   588    589             for p in have_conflict(freebusy, periods, True):   590                 if not self.recurrenceid and p.is_replaced(recurrences):   591                     continue   592    593                 if ( # Unidentified or different event   594                      (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and   595                      # Different period or unclear participation with the same period   596                      (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and   597                      # Participant not limited to organising   598                      p.transp != "ORG"   599                    ):   600    601                     conflicts.append(p)   602    603         conflicts.sort()   604    605         # Show any conflicts with periods of actual attendance.   606    607         if conflicts:   608             page.p("This event conflicts with others:")   609    610             page.table(cellspacing=5, cellpadding=5, class_="conflicts")   611             page.thead()   612             page.tr()   613             page.th("Event")   614             page.th("Start")   615             page.th("End")   616             page.tr.close()   617             page.thead.close()   618             page.tbody()   619    620             for p in conflicts:   621    622                 # Provide details of any conflicting event.   623    624                 start = self.format_datetime(to_timezone(p.get_start(), tzid), "long")   625                 end = self.format_datetime(to_timezone(p.get_end(), tzid), "long")   626    627                 page.tr()   628    629                 # Show the event summary for the conflicting event.   630    631                 page.td()   632                 if p.summary:   633                     page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid))   634                 else:   635                     page.add("(Unspecified event)")   636                 page.td.close()   637    638                 page.td(start)   639                 page.td(end)   640    641                 page.tr.close()   642    643             page.tbody.close()   644             page.table.close()   645    646 class EventPage(EventPageFragment):   647    648     "A request handler for the event page."   649    650     def __init__(self, resource=None, messenger=None):   651         ResourceClientForObject.__init__(self, resource)   652         self.messenger = messenger or Messenger()   653    654     # Request logic methods.   655    656     def is_initial_load(self):   657    658         "Return whether the event is being loaded and shown for the first time."   659    660         return not self.env.get_args().has_key("editing")   661    662     def handle_request(self):   663    664         """   665         Handle actions involving the current object, returning an error if one   666         occurred, or None if the request was successfully handled.   667         """   668    669         # Handle a submitted form.   670    671         args = self.env.get_args()   672    673         # Get the possible actions.   674    675         reply = args.has_key("reply")   676         discard = args.has_key("discard")   677         create = args.has_key("create")   678         cancel = args.has_key("cancel")   679         ignore = args.has_key("ignore")   680         save = args.has_key("save")   681    682         have_action = reply or discard or create or cancel or ignore or save   683    684         if not have_action:   685             return ["action"]   686    687         # If ignoring the object, return to the calendar.   688    689         if ignore:   690             self.redirect(self.env.get_path())   691             return None   692    693         # Update the object.   694    695         single_user = False   696    697         if reply or create or cancel or save:   698    699             # Update principal event details if organiser.   700    701             if self.is_organiser():   702    703                 # Update time periods (main and recurring).   704    705                 try:   706                     period = self.handle_main_period()   707                 except PeriodError, exc:   708                     return exc.args   709    710                 try:   711                     periods = self.handle_recurrence_periods()   712                 except PeriodError, exc:   713                     return exc.args   714    715                 # Set the periods in the object, first obtaining removed and   716                 # modified period information.   717    718                 to_unschedule = self.get_removed_periods(periods)   719    720                 self.obj.set_period(period)   721                 self.obj.set_periods(periods)   722    723                 # Update summary.   724    725                 if args.has_key("summary"):   726                     self.obj["SUMMARY"] = [(args["summary"][0], {})]   727    728                 # Obtain any participants and those to be removed.   729    730                 attendees = self.get_attendees_from_page()   731                 removed = [attendees[int(i)] for i in args.get("remove", [])]   732                 to_cancel = self.update_attendees(self.obj, attendees, removed)   733                 single_user = not attendees or attendees == [self.user]   734    735             # Update attendee participation for the current user.   736    737             if args.has_key("partstat"):   738                 self.update_participation(self.obj, args["partstat"][0])   739    740         # Process any action.   741    742         invite = not save and create and not single_user   743         save = save or create and single_user   744    745         handled = True   746    747         if reply or invite or cancel:   748    749             client = ManagerClient(self.obj, self.user, self.messenger)   750    751             # Process the object and remove it from the list of requests.   752    753             if reply and client.process_received_request():   754                 self.remove_request(self.uid, self.recurrenceid)   755    756             elif self.is_organiser() and (invite or cancel):   757    758                 # Invitation, uninvitation and unscheduling...   759    760                 if client.process_created_request(   761                     invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule):   762    763                     self.remove_request(self.uid, self.recurrenceid)   764    765         # Save single user events.   766    767         elif save:   768             self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node())   769             self.update_event_in_freebusy()   770             self.remove_request(self.uid, self.recurrenceid)   771    772         # Remove the request and the object.   773    774         elif discard:   775             self.remove_event_from_freebusy()   776             self.remove_event(self.uid, self.recurrenceid)   777             self.remove_request(self.uid, self.recurrenceid)   778    779         else:   780             handled = False   781    782         # Upon handling an action, redirect to the main page.   783    784         if handled:   785             self.redirect(self.env.get_path())   786    787         return None   788    789     def handle_main_period(self):   790    791         "Return period details for the main start/end period in an event."   792    793         return self.get_main_period_from_page().as_event_period()   794    795     def handle_recurrence_periods(self):   796    797         "Return period details for the recurrences specified for an event."   798    799         return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())]   800    801     # Access to form-originating object information.   802    803     def get_main_period_from_page(self):   804    805         "Return the main period defined in the event form."   806    807         args = self.env.get_args()   808    809         dtend_enabled = args.get("dtend-control", [None])[0]   810         dttimes_enabled = args.get("dttimes-control", [None])[0]   811         start = self.get_date_control_values("dtstart")   812         end = self.get_date_control_values("dtend")   813    814         return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid())   815    816     def get_recurrences_from_page(self):   817    818         "Return the recurrences defined in the event form."   819    820         args = self.env.get_args()   821    822         all_dtend_enabled = args.get("dtend-control-recur", [])   823         all_dttimes_enabled = args.get("dttimes-control-recur", [])   824         all_starts = self.get_date_control_values("dtstart-recur", multiple=True)   825         all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")   826         all_origins = args.get("recur-origin", [])   827    828         periods = []   829    830         for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \   831             enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)):   832    833             dtend_enabled = str(index) in all_dtend_enabled   834             dttimes_enabled = str(index) in all_dttimes_enabled   835             period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin)   836             periods.append(period)   837    838         return periods   839    840     def get_removed_periods(self, periods):   841    842         """   843         Return those from the recurrence 'periods' to remove upon updating an   844         event.   845         """   846    847         to_unschedule = []   848         args = self.env.get_args()   849         for i in args.get("recur-remove", []):   850             to_unschedule.append(periods[int(i)])   851         return to_unschedule   852    853     def get_attendees_from_page(self):   854    855         """   856         Return attendees from the request, normalised for iCalendar purposes,   857         and without duplicates.   858         """   859    860         args = self.env.get_args()   861    862         attendees = args.get("attendee", [])   863         unique_attendees = set()   864         ordered_attendees = []   865    866         for attendee in attendees:   867             if not attendee.strip():   868                 continue   869             attendee = get_uri(attendee)   870             if attendee not in unique_attendees:   871                 unique_attendees.add(attendee)   872                 ordered_attendees.append(attendee)   873    874         return ordered_attendees   875    876     def update_attendees_from_page(self):   877    878         "Add or remove attendees. This does not affect the stored object."   879    880         args = self.env.get_args()   881    882         attendees = self.get_attendees_from_page()   883    884         if args.has_key("add"):   885             attendees.append("")   886    887         # Only actually remove attendees if the event is unsent, if the attendee   888         # is new, or if it is the current user being removed.   889    890         if args.has_key("remove"):   891             still_to_remove = []   892    893             for i in args["remove"]:   894                 try:   895                     attendee = attendees[int(i)]   896                 except IndexError:   897                     continue   898    899                 if self.can_remove_attendee(attendee):   900                     attendees.remove(attendee)   901                 else:   902                     still_to_remove.append(i)   903    904             args["remove"] = still_to_remove   905    906         return attendees   907    908     def update_recurrences_from_page(self):   909    910         "Add or remove recurrences. This does not affect the stored object."   911    912         args = self.env.get_args()   913    914         recurrences = self.get_recurrences_from_page()   915    916         # NOTE: Addition of recurrences to be supported.   917    918         # Only actually remove recurrences if the event is unsent, or if the   919         # recurrence is new.   920    921         if args.has_key("recur-remove"):   922             still_to_remove = []   923    924             for i in args["recur-remove"]:   925                 try:   926                     recurrence = recurrences[int(i)]   927                 except IndexError:   928                     continue   929    930                 if self.can_remove_recurrence(recurrence):   931                     recurrences.remove(recurrence)   932                 else:   933                     still_to_remove.append(i)   934    935             args["recur-remove"] = still_to_remove   936    937         return recurrences   938    939     # Access to current object information.   940    941     def get_current_main_period(self):   942    943         """   944         Return the currently active main period for the current object depending   945         on whether editing has begun or whether the object has just been loaded.   946         """   947    948         if self.is_initial_load() or not self.is_organiser():   949             return self.get_stored_main_period()   950         else:   951             return self.get_main_period_from_page()   952    953     def get_current_recurrences(self):   954    955         """   956         Return recurrences for the current object using the original object   957         details where no editing is in progress, using form data otherwise.   958         """   959    960         if self.is_initial_load() or not self.is_organiser():   961             return self.get_stored_recurrences()   962         else:   963             return self.get_recurrences_from_page()   964    965     def update_current_recurrences(self):   966    967         "Return an updated collection of recurrences for the current object."   968    969         if self.is_initial_load() or not self.is_organiser():   970             return self.get_stored_recurrences()   971         else:   972             return self.update_recurrences_from_page()   973    974     def get_current_attendees(self):   975    976         """   977         Return attendees for the current object depending on whether the object   978         has been edited or instead provides such information from its stored   979         form.   980         """   981    982         if self.is_initial_load() or not self.is_organiser():   983             return self.get_stored_attendees()   984         else:   985             return self.get_attendees_from_page()   986    987     def update_current_attendees(self):   988    989         "Return an updated collection of attendees for the current object."   990    991         if self.is_initial_load() or not self.is_organiser():   992             return self.get_stored_attendees()   993         else:   994             return self.update_attendees_from_page()   995    996     # Full page output methods.   997    998     def show(self, path_info):   999   1000         "Show an object request using the given 'path_info' for the current user."  1001   1002         uid, recurrenceid = self.get_identifiers(path_info)  1003         obj = self.get_stored_object(uid, recurrenceid)  1004         self.set_object(obj)  1005   1006         if not obj:  1007             return False  1008   1009         errors = self.handle_request()  1010   1011         if not errors:  1012             return True  1013   1014         self.new_page(title="Event")  1015         self.show_object_on_page(errors)  1016   1017         return True  1018   1019 # vim: tabstop=4 expandtab shiftwidth=4