imip-agent

imipweb/event.py

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