imip-agent

imipweb/event.py

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