imip-agent

imipweb/event.py

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