imip-agent

imipweb/event.py

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