imip-agent

imipweb/event.py

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