imip-agent

imipweb/event.py

475:5f6011c50e61
2015-04-04 Paul Boddie Fixed attendee loading (where no attendees are present) and removal (where an existing attendee is present but has not responded, or where the organiser is removing themself).
     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 = args.get("attendee")   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 handle_attendees(self, obj):   393    394         "Add or remove attendees. This does not affect the stored object."   395    396         args = self.env.get_args()   397    398         attendees = args.get("attendee", [])   399    400         if args.has_key("add"):   401             attendees.append("")   402    403         if args.has_key("remove"):   404             removed_attendee = args["remove"][0]   405             if removed_attendee in attendees:   406                 attendees.remove(removed_attendee)   407    408         return attendees   409    410     def get_event_period(self, obj):   411    412         """   413         Return (dtstart, dtstart attributes), (dtend, dtend attributes) for   414         'obj'.   415         """   416    417         dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")   418         if obj.has_key("DTEND"):   419             dtend, dtend_attr = obj.get_datetime_item("DTEND")   420         elif obj.has_key("DURATION"):   421             duration = obj.get_duration("DURATION")   422             dtend = dtstart + duration   423             dtend_attr = dtstart_attr   424         else:   425             dtend, dtend_attr = dtstart, dtstart_attr   426         return (dtstart, dtstart_attr), (dtend, dtend_attr)   427    428     # Page fragment methods.   429    430     def show_request_controls(self, obj):   431    432         "Show form controls for a request concerning 'obj'."   433    434         page = self.page   435         args = self.env.get_args()   436    437         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   438    439         attendees = uri_values((obj.get_values("ATTENDEE") or []) + filter(None, args.get("attendee", [])))   440         is_attendee = self.user in attendees   441    442         is_request = (obj.get_value("UID"), obj.get_value("RECURRENCE-ID")) in self._get_requests()   443    444         have_other_attendees = len(attendees) > (is_attendee and 1 or 0)   445    446         # Show appropriate options depending on the role of the user.   447    448         if is_attendee and not is_organiser:   449             page.p("An action is required for this request:")   450    451             page.p()   452             page.input(name="reply", type="submit", value="Send reply")   453             page.add(" ")   454             page.input(name="discard", type="submit", value="Discard event")   455             page.add(" ")   456             page.input(name="ignore", type="submit", value="Do nothing for now")   457             page.p.close()   458    459         if is_organiser:   460             page.p("As organiser, you can perform the following:")   461    462             if have_other_attendees:   463                 page.p()   464                 page.input(name="invite", type="submit", value="Invite/notify attendees")   465                 page.add(" ")   466                 if is_request:   467                     page.input(name="discard", type="submit", value="Discard event")   468                 else:   469                     page.input(name="cancel", type="submit", value="Cancel event")   470                 page.add(" ")   471                 page.input(name="ignore", type="submit", value="Do nothing for now")   472                 page.p.close()   473             else:   474                 page.p()   475                 page.input(name="save", type="submit", value="Save event")   476                 page.add(" ")   477                 page.input(name="discard", type="submit", value="Discard event")   478                 page.add(" ")   479                 page.input(name="ignore", type="submit", value="Do nothing for now")   480                 page.p.close()   481    482     def show_object_on_page(self, uid, obj, error=None):   483    484         """   485         Show the calendar object with the given 'uid' and representation 'obj'   486         on the current page. If 'error' is given, show a suitable message.   487         """   488    489         page = self.page   490         page.form(method="POST")   491    492         page.input(name="editing", type="hidden", value="true")   493    494         args = self.env.get_args()   495    496         # Obtain the user's timezone.   497    498         tzid = self.get_tzid()   499    500         # Obtain basic event information, showing any necessary editing controls.   501    502         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   503         initial_load = not args.has_key("editing")   504    505         existing_attendees = uri_values(obj.get_values("ATTENDEE") or [])   506         attendees = is_organiser and self.handle_attendees(obj) or \   507             (initial_load or not is_organiser) and existing_attendees or []   508    509         (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_event_period(obj)   510         self.show_object_datetime_controls(dtstart, dtend)   511    512         # Provide a summary of the object.   513    514         page.table(class_="object", cellspacing=5, cellpadding=5)   515         page.thead()   516         page.tr()   517         page.th("Event", class_="mainheading", colspan=2)   518         page.tr.close()   519         page.thead.close()   520         page.tbody()   521    522         for name, label in self.property_items:   523             field = name.lower()   524    525             items = obj.get_items(name) or []   526             rowspan = len(items)   527    528             if name == "ATTENDEE":   529                 rowspan = len(attendees) + 1 # for the add button   530             elif not items:   531                 continue   532    533             page.tr()   534             page.th(label, class_="objectheading %s%s" % (field, error and field in error and " error" or ""), rowspan=rowspan)   535    536             # Handle datetimes specially.   537    538             if name in ["DTSTART", "DTEND"]:   539    540                 # Obtain the datetime.   541    542                 if name == "DTSTART":   543                     dt, attr = dtstart, dtstart_attr   544    545                 # Where no end datetime exists, use the start datetime as the   546                 # basis of any potential datetime specified if dt-control is   547                 # set.   548    549                 else:   550                     dt, attr = dtend or dtstart, dtend_attr or dtstart_attr   551    552                 self.show_datetime_controls(obj, dt, attr, name == "DTSTART")   553    554                 page.tr.close()   555    556             # Handle the summary specially.   557    558             elif name == "SUMMARY":   559                 value = args.get("summary", [obj.get_value(name)])[0]   560    561                 page.td()   562                 if is_organiser:   563                     page.input(name="summary", type="text", value=value, size=80)   564                 else:   565                     page.add(value)   566                 page.td.close()   567                 page.tr.close()   568    569             # Handle attendees specially.   570    571             elif name == "ATTENDEE":   572                 attendee_map = dict(items)   573                 first = True   574    575                 for i, value in enumerate(attendees):   576                     if not first:   577                         page.tr()   578                     else:   579                         first = False   580    581                     page.td(class_="objectvalue")   582    583                     # Obtain details of existing attendees.   584    585                     attr = attendee_map.get(value)   586                     partstat = attr and attr.get("PARTSTAT")   587    588                     # Show a form control as organiser for new attendees.   589    590                     if is_organiser and not partstat:   591                         page.input(name="attendee", type="value", value=value, size="40")   592                     else:   593                         page.input(name="attendee", type="hidden", value=value)   594                         page.add(value)   595                     page.add(" ")   596    597                     # Show participation status, editable for the current user.   598    599                     if value == self.user:   600                         self._show_menu("partstat", partstat, self.partstat_items, "partstat")   601    602                     # Allow the participation indicator to act as a submit   603                     # button in order to refresh the page and show a control for   604                     # the current user, if indicated.   605    606                     elif is_organiser:   607                         page.input(name="partstat-refresh", type="submit", value="refresh", id="partstat-%d" % i, class_="refresh")   608                         page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat")   609                     else:   610                         page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")   611    612                     # Permit organisers to remove attendees.   613    614                     if is_organiser:   615    616                         # Permit the removal of newly-added attendees.   617    618                         remove_type = (value in existing_attendees and value != self.user) and "checkbox" or "submit"   619    620                         self._control("remove", remove_type, value, value in args.get("remove", []), id="remove-%d" % i, class_="remove")   621    622                         page.label("Remove", for_="remove-%d" % i, class_="remove")   623                         page.label("Uninvited", for_="remove-%d" % i, class_="removed")   624    625                     page.td.close()   626                     page.tr.close()   627    628                 # Allow more attendees to be specified.   629    630                 if is_organiser:   631                     i = len(attendees)   632    633                     if not first:   634                         page.tr()   635    636                     page.td()   637                     page.input(name="add", type="submit", value="add", id="add-%d" % i, class_="add")   638                     page.label("Add attendee", for_="add-%d" % i, class_="add")   639                     page.td.close()   640                     page.tr.close()   641    642             # Handle potentially many values of other kinds.   643    644             else:   645                 first = True   646    647                 for i, (value, attr) in enumerate(items):   648                     if not first:   649                         page.tr()   650                     else:   651                         first = False   652    653                     page.td(class_="objectvalue")   654                     page.add(value)   655                     page.td.close()   656                     page.tr.close()   657    658         page.tbody.close()   659         page.table.close()   660    661         self.show_recurrences(obj)   662         self.show_conflicting_events(uid, obj)   663         self.show_request_controls(obj)   664    665         page.form.close()   666    667     def show_recurrences(self, obj):   668    669         "Show recurrences for the object having the given representation 'obj'."   670    671         page = self.page   672         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   673    674         # Obtain any parent object if this object is a specific recurrence.   675    676         uid = obj.get_value("UID")   677         recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))   678    679         if recurrenceid:   680             obj = self._get_object(uid)   681             if not obj:   682                 return   683    684             page.p("This event modifies a recurring event.")   685    686         # Obtain the periods associated with the event in the user's time zone.   687    688         periods = obj.get_periods(self.get_tzid(), self.get_window_end())   689         recurrenceids = self._get_recurrences(uid)   690    691         if len(periods) == 1:   692             return   693    694         if is_organiser:   695             page.p("This event recurs on the following occasions within the next %d days:" % self.get_window_size())   696         else:   697             page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())   698    699         # Determine whether any periods are explicitly created or are part of a   700         # rule.   701    702         explicit_periods = filter(lambda p: p.origin != "RRULE", periods)   703    704         # Show each recurrence in a separate table if editable.   705    706         if is_organiser and explicit_periods:   707    708             for index, p in enumerate(periods[1:]):   709    710                 # Isolate the controls from neighbouring tables.   711    712                 page.div()   713    714                 self.show_object_datetime_controls(p.start, p.end, index)   715    716                 # NOTE: Need to customise the TH classes according to errors and   717                 # NOTE: index information.   718    719                 page.table(cellspacing=5, cellpadding=5, class_="recurrence")   720                 page.caption("Occurrence")   721                 page.tbody()   722                 page.tr()   723                 page.th("Start", class_="objectheading start")   724                 self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, True)   725                 page.tr.close()   726                 page.tr()   727                 page.th("End", class_="objectheading end")   728                 self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, False)   729                 page.tr.close()   730                 page.tbody.close()   731                 page.table.close()   732    733                 page.div.close()   734    735         # Otherwise, use a compact single table.   736    737         else:   738             page.table(cellspacing=5, cellpadding=5, class_="recurrence")   739             page.caption("Occurrences")   740             page.thead()   741             page.tr()   742             page.th("Start", class_="objectheading start")   743             page.th("End", class_="objectheading end")   744             page.tr.close()   745             page.thead.close()   746             page.tbody()   747    748             # Show only subsequent periods if organiser, since the principal   749             # period will be the start and end datetimes.   750    751             for index, p in enumerate(is_organiser and periods[1:] or periods):   752                 page.tr()   753                 self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, True)   754                 self.show_recurrence_controls(obj, index, p.start, p.end, p.origin, recurrenceid, recurrenceids, False)   755                 page.tr.close()   756             page.tbody.close()   757             page.table.close()   758    759     def show_conflicting_events(self, uid, obj):   760    761         """   762         Show conflicting events for the object having the given 'uid' and   763         representation 'obj'.   764         """   765    766         page = self.page   767    768         # Obtain the user's timezone.   769    770         tzid = self.get_tzid()   771         periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())   772    773         # Indicate whether there are conflicting events.   774    775         freebusy = self.store.get_freebusy(self.user)   776    777         if freebusy:   778    779             # Obtain any time zone details from the suggested event.   780    781             _dtstart, attr = obj.get_item("DTSTART")   782             tzid = attr.get("TZID", tzid)   783    784             # Show any conflicts.   785    786             conflicts = list([p for p in have_conflict(freebusy, periods, True) if p.uid != uid])   787             conflicts.sort()   788    789             if conflicts:   790                 page.p("This event conflicts with others:")   791    792                 page.table(cellspacing=5, cellpadding=5, class_="conflicts")   793                 page.thead()   794                 page.tr()   795                 page.th("Event")   796                 page.th("Start")   797                 page.th("End")   798                 page.tr.close()   799                 page.thead.close()   800                 page.tbody()   801    802                 for p in conflicts:   803    804                     # Provide details of any conflicting event.   805    806                     start = self.format_datetime(to_timezone(get_datetime(p.start), tzid), "long")   807                     end = self.format_datetime(to_timezone(get_datetime(p.end), tzid), "long")   808    809                     page.tr()   810    811                     # Show the event summary for the conflicting event.   812    813                     page.td()   814                     page.a(p.summary, href=self.link_to(p.uid))   815                     page.td.close()   816    817                     page.td(start)   818                     page.td(end)   819    820                     page.tr.close()   821    822                 page.tbody.close()   823                 page.table.close()   824    825     # Generation of controls within page fragments.   826    827     def show_object_datetime_controls(self, start, end, index=None):   828    829         """   830         Show datetime-related controls if already active or if an object needs   831         them for the given 'start' to 'end' period. The given 'index' is used to   832         parameterise individual controls for dynamic manipulation.   833         """   834    835         page = self.page   836         args = self.env.get_args()   837         sn = self._suffixed_name   838         ssn = self._simple_suffixed_name   839    840         # Add a dynamic stylesheet to permit the controls to modify the display.   841         # NOTE: The style details need to be coordinated with the static   842         # NOTE: stylesheet.   843    844         if index is not None:   845             page.style(type="text/css")   846    847             # Unlike the rules for object properties, these affect recurrence   848             # properties.   849    850             page.add("""\   851 input#dttimes-enable-%(index)d,   852 input#dtend-enable-%(index)d,   853 input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,   854 input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,   855 input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,   856 input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {   857     display: none;   858 }""" % {"index" : index})   859    860             page.style.close()   861    862         dtend_control = args.get(ssn("dtend-control", "recur", index), [])   863         dttimes_control = args.get(ssn("dttimes-control", "recur", index), [])   864    865         dtend_enabled = index is not None and str(index) in dtend_control or index is None and dtend_control   866         dttimes_enabled = index is not None and str(index) in dttimes_control or index is None and dttimes_control   867    868         initial_load = not args.has_key("editing")   869    870         dtend_enabled = dtend_enabled or initial_load and (isinstance(end, datetime) or start != end - timedelta(1))   871         dttimes_enabled = dttimes_enabled or initial_load and (isinstance(start, datetime) or isinstance(end, datetime))   872    873         self._control(   874             ssn("dtend-control", "recur", index), "checkbox",   875             index is not None and str(index) or "enable", dtend_enabled,   876             id=sn("dtend-enable", index)   877             )   878    879         self._control(   880             ssn("dttimes-control", "recur", index), "checkbox",   881             index is not None and str(index) or "enable", dttimes_enabled,   882             id=sn("dttimes-enable", index)   883             )   884    885     def show_datetime_controls(self, obj, dt, attr, show_start):   886    887         """   888         Show datetime details from the given 'obj' for the datetime 'dt' and   889         attributes 'attr', showing start details if 'show_start' is set   890         to a true value. Details will appear as controls for organisers and   891         labels for attendees.   892         """   893    894         page = self.page   895         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   896    897         # Change end dates to refer to the actual dates, not the iCalendar   898         # "next day" dates.   899    900         if not show_start and not isinstance(dt, datetime):   901             dt -= timedelta(1)   902    903         # Show controls for editing as organiser.   904    905         if is_organiser:   906             page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))   907    908             if show_start:   909                 page.div(class_="dt enabled")   910                 self._show_date_controls("dtstart", dt, attr.get("TZID"))   911                 page.br()   912                 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")   913                 page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")   914                 page.div.close()   915    916             else:   917                 page.div(class_="dt disabled")   918                 page.label("Specify end date", for_="dtend-enable", class_="enable")   919                 page.div.close()   920                 page.div(class_="dt enabled")   921                 self._show_date_controls("dtend", dt, attr.get("TZID"))   922                 page.br()   923                 page.label("End on same day", for_="dtend-enable", class_="disable")   924                 page.div.close()   925    926             page.td.close()   927    928         # Show a label as attendee.   929    930         else:   931             page.td(self.format_datetime(dt, "full"))   932    933     def show_recurrence_controls(self, obj, index, start, end, origin, recurrenceid, recurrenceids, show_start):   934    935         """   936         Show datetime details from the given 'obj' for the recurrence having the   937         given 'index', with the recurrence period described by the datetimes   938         'start' and 'end', indicating the 'origin' of the period from the event   939         details, employing any 'recurrenceid' and 'recurrenceids' for the object   940         to configure the displayed information.   941    942         If 'show_start' is set to a true value, the start details will be shown;   943         otherwise, the end details will be shown.   944         """   945    946         page = self.page   947         sn = self._suffixed_name   948         ssn = self._simple_suffixed_name   949    950         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   951    952         # Change end dates to refer to the actual dates, not the iCalendar   953         # "next day" dates.   954    955         if not isinstance(end, datetime):   956             end -= timedelta(1)   957    958         start_utc = format_datetime(to_timezone(start, "UTC"))   959         replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""   960         css = " ".join([   961             replaced,   962             recurrenceid and start_utc == recurrenceid and "affected" or ""   963             ])   964    965         # Show controls for editing as organiser.   966    967         if is_organiser and not replaced and origin != "RRULE":   968             page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))   969    970             if show_start:   971                 page.div(class_="dt enabled")   972                 self._show_date_controls(ssn("dtstart", "recur", index), start, index=index)   973                 page.br()   974                 page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable")   975                 page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable")   976                 page.div.close()   977    978             else:   979                 page.div(class_="dt disabled")   980                 page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable")   981                 page.div.close()   982                 page.div(class_="dt enabled")   983                 self._show_date_controls(ssn("dtend", "recur", index), end, index=index, show_tzid=False)   984                 page.br()   985                 page.label("End on same day", for_=sn("dtend-enable", index), class_="disable")   986                 page.div.close()   987    988             page.td.close()   989    990         # Show label as attendee.   991    992         else:   993             page.td(self.format_datetime(show_start and start or end, "long"), class_=css)   994    995     # Full page output methods.   996    997     def show(self, path_info):   998    999         "Show an object request using the given 'path_info' for the current user."  1000   1001         uid, recurrenceid = self._get_identifiers(path_info)  1002         obj = self._get_object(uid, recurrenceid)  1003   1004         if not obj:  1005             return False  1006   1007         error = self.handle_request(uid, recurrenceid, obj)  1008   1009         if not error:  1010             return True  1011   1012         self.new_page(title="Event")  1013         self.show_object_on_page(uid, obj, error)  1014   1015         return True  1016   1017     # Utility methods.  1018   1019     def _control(self, name, type, value, selected, **kw):  1020   1021         """  1022         Show a control with the given 'name', 'type' and 'value', with  1023         'selected' indicating whether it should be selected (checked or  1024         equivalent), and with keyword arguments setting other properties.  1025         """  1026   1027         page = self.page  1028         if selected:  1029             page.input(name=name, type=type, value=value, checked=selected, **kw)  1030         else:  1031             page.input(name=name, type=type, value=value, **kw)  1032   1033     def _show_menu(self, name, default, items, class_="", index=None):  1034   1035         """  1036         Show a select menu having the given 'name', set to the given 'default',  1037         providing the given (value, label) 'items', and employing the given CSS  1038         'class_' if specified.  1039         """  1040   1041         page = self.page  1042         values = self.env.get_args().get(name, [default])  1043         if index is not None:  1044             values = values[index:]  1045             values = values and values[0:1] or [default]  1046   1047         page.select(name=name, class_=class_)  1048         for v, label in items:  1049             if v is None:  1050                 continue  1051             if v in values:  1052                 page.option(label, value=v, selected="selected")  1053             else:  1054                 page.option(label, value=v)  1055         page.select.close()  1056   1057     def _show_date_controls(self, name, default, tzid=None, index=None, show_tzid=True):  1058   1059         """  1060         Show date controls for a field with the given 'name' and 'default' value  1061         and 'tzid'. If 'index' is specified, default field values will be  1062         overridden by the element from a collection of existing form values with  1063         the specified index; otherwise, field values will be overridden by a  1064         single form value.  1065   1066         If 'show_tzid' is set to a false value, the time zone menu will not be  1067         provided.  1068         """  1069   1070         page = self.page  1071         args = self.env.get_args()  1072   1073         # Show dates for up to one week around the current date.  1074   1075         base = to_date(default)  1076         items = []  1077         for i in range(-7, 8):  1078             d = base + timedelta(i)  1079             items.append((format_datetime(d), self.format_date(d, "full")))  1080   1081         self._show_menu("%s-date" % name, format_datetime(base), items, index=index)  1082   1083         # Show time details.  1084   1085         default_time = isinstance(default, datetime) and default or None  1086   1087         hour = args.get("%s-hour" % name, [])[index or 0:]  1088         hour = hour and hour[0] or "%02d" % (default_time and default_time.hour or 0)  1089         minute = args.get("%s-minute" % name, [])[index or 0:]  1090         minute = minute and minute[0] or "%02d" % (default_time and default_time.minute or 0)  1091         second = args.get("%s-second" % name, [])[index or 0:]  1092         second = second and second[0] or "%02d" % (default_time and default_time.second or 0)  1093   1094         page.span(class_="time enabled")  1095         page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2)  1096         page.add(":")  1097         page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2)  1098         page.add(":")  1099         page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2)  1100         if show_tzid:  1101             tzid = tzid or self.get_tzid()  1102             page.add(" ")  1103             self._show_timezone_menu("%s-tzid" % name, tzid, index)  1104         page.span.close()  1105   1106     def _show_timezone_menu(self, name, default, index=None):  1107   1108         """  1109         Show timezone controls using a menu with the given 'name', set to the  1110         given 'default' unless a field of the given 'name' provides a value.  1111         """  1112   1113         entries = [(tzid, tzid) for tzid in pytz.all_timezones]  1114         self._show_menu(name, default, entries, index=index)  1115   1116 # vim: tabstop=4 expandtab shiftwidth=4