imip-agent

imipweb/event.py

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