imip-agent

imipweb/event.py

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