imip-agent

imipweb/event.py

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