imip-agent

imipweb/event.py

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