imip-agent

imipweb/event.py

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