imip-agent

imip_manager.py

346:86f9ba2b3bdc
2015-02-12 Paul Boddie Use the UTC-converted value of RECURRENCE-ID so that it may be used to find and remove/exclude recurring events from the general event. recurring-events
     1 #!/usr/bin/env python     2      3 """     4 A Web interface to a user's calendar.     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 # Edit this path to refer to the location of the imiptools libraries, if    23 # necessary.    24     25 LIBRARY_PATH = "/var/lib/imip-agent"    26     27 from datetime import date, datetime, timedelta    28 import babel.dates    29 import cgi, os, sys    30     31 sys.path.append(LIBRARY_PATH)    32     33 from imiptools.content import Handler    34 from imiptools.data import get_address, get_uri, make_freebusy, Object, to_part, \    35                            uri_dict, uri_item, uri_items, uri_values    36 from imiptools.dates import format_datetime, format_time, get_date, get_datetime, \    37                             get_datetime_item, get_default_timezone, \    38                             get_end_of_day, get_start_of_day, get_start_of_next_day, \    39                             get_timestamp, ends_on_same_day, to_timezone    40 from imiptools.mail import Messenger    41 from imiptools.period import add_day_start_points, add_empty_days, add_slots, \    42                              convert_periods, get_freebusy_details, \    43                              get_scale, have_conflict, get_slots, get_spans, \    44                              partition_by_day, remove_from_freebusy, update_freebusy, \    45                              _update_freebusy    46 from imiptools.profile import Preferences    47 import imip_store    48 import markup    49     50 getenv = os.environ.get    51 setenv = os.environ.__setitem__    52     53 class CGIEnvironment:    54     55     "A CGI-compatible environment."    56     57     def __init__(self, charset=None):    58         self.charset = charset    59         self.args = None    60         self.method = None    61         self.path = None    62         self.path_info = None    63         self.user = None    64     65     def get_args(self):    66         if self.args is None:    67             if self.get_method() != "POST":    68                 setenv("QUERY_STRING", "")    69             args = cgi.parse(keep_blank_values=True)    70     71             if not self.charset:    72                 self.args = args    73             else:    74                 self.args = {}    75                 for key, values in args.items():    76                     self.args[key] = [unicode(value, self.charset) for value in values]    77     78         return self.args    79     80     def get_method(self):    81         if self.method is None:    82             self.method = getenv("REQUEST_METHOD") or "GET"    83         return self.method    84     85     def get_path(self):    86         if self.path is None:    87             self.path = getenv("SCRIPT_NAME") or ""    88         return self.path    89     90     def get_path_info(self):    91         if self.path_info is None:    92             self.path_info = getenv("PATH_INFO") or ""    93         return self.path_info    94     95     def get_user(self):    96         if self.user is None:    97             self.user = getenv("REMOTE_USER") or ""    98         return self.user    99    100     def get_output(self):   101         return sys.stdout   102    103     def get_url(self):   104         path = self.get_path()   105         path_info = self.get_path_info()   106         return "%s%s" % (path.rstrip("/"), path_info)   107    108     def new_url(self, path_info):   109         path = self.get_path()   110         return "%s/%s" % (path.rstrip("/"), path_info.lstrip("/"))   111    112 class Common:   113    114     "Common handler and manager methods."   115    116     def __init__(self, user):   117         self.user = user   118         self.preferences = None   119    120     def get_preferences(self):   121         if not self.preferences:   122             self.preferences = Preferences(self.user)   123         return self.preferences   124    125     def get_tzid(self):   126         prefs = self.get_preferences()   127         return prefs.get("TZID") or get_default_timezone()   128    129 class ManagerHandler(Handler, Common):   130    131     """   132     A content handler for use by the manager, as opposed to operating within the   133     mail processing pipeline.   134     """   135    136     def __init__(self, obj, user, messenger):   137         Handler.__init__(self, messenger=messenger)   138         Common.__init__(self, user)   139    140         self.set_object(obj)   141    142     # Communication methods.   143    144     def send_message(self, method, sender, for_organiser):   145    146         """   147         Create a full calendar object employing the given 'method', and send it   148         to the appropriate recipients, also sending a copy to the 'sender'. The   149         'for_organiser' value indicates whether the organiser is sending this   150         message.   151         """   152    153         parts = [self.obj.to_part(method)]   154    155         # As organiser, send an invitation to attendees, excluding oneself if   156         # also attending. The updated event will be saved by the outgoing   157         # handler.   158    159         organiser = get_uri(self.obj.get_value("ORGANIZER"))   160         attendees = uri_values(self.obj.get_values("ATTENDEE"))   161    162         if for_organiser:   163             recipients = [get_address(attendee) for attendee in attendees if attendee != self.user]   164         else:   165             recipients = [get_address(organiser)]   166    167         # Bundle free/busy information if appropriate.   168    169         preferences = Preferences(self.user)   170    171         if preferences.get("freebusy_sharing") == "share" and \   172            preferences.get("freebusy_bundling") == "always":   173    174             # Invent a unique identifier.   175    176             utcnow = get_timestamp()   177             uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))   178    179             freebusy = self.store.get_freebusy(self.user)   180    181             # Replace the non-updated free/busy details for this event with   182             # newer details (since the outgoing handler updates this user's   183             # free/busy details).   184    185             tzid = self.get_tzid()   186    187             _update_freebusy(freebusy, self.obj.get_periods_for_freebusy(tzid),   188                 self.obj.get_value("TRANSP") or "OPAQUE",   189                 self.uid, self.recurrenceid)   190    191             user_attr = self.messenger and self.messenger.sender != get_address(self.user) and \   192                 {"SENT-BY" : get_uri(self.messenger.sender)} or {}   193    194             parts.append(to_part("PUBLISH", [   195                 make_freebusy(freebusy, uid, self.user, user_attr)   196                 ]))   197    198         message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)   199         self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)   200    201     # Action methods.   202    203     def process_received_request(self, update=False):   204    205         """   206         Process the current request for the given 'user'. Return whether any   207         action was taken.   208    209         If 'update' is given, the sequence number will be incremented in order   210         to override any previous response.   211         """   212    213         # Reply only on behalf of this user.   214    215         for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")):   216    217             if attendee == self.user:   218                 if attendee_attr.has_key("RSVP"):   219                     del attendee_attr["RSVP"]   220                 if self.messenger and self.messenger.sender != get_address(attendee):   221                     attendee_attr["SENT-BY"] = get_uri(self.messenger.sender)   222                 self.obj["ATTENDEE"] = [(attendee, attendee_attr)]   223    224                 self.update_dtstamp()   225                 self.set_sequence(update)   226    227                 self.send_message("REPLY", get_address(attendee), for_organiser=False)   228    229                 return True   230    231         return False   232    233     def process_created_request(self, method, update=False, removed=None, added=None):   234    235         """   236         Process the current request for the given 'user', sending a created   237         request of the given 'method' to attendees. Return whether any action   238         was taken.   239    240         If 'update' is given, the sequence number will be incremented in order   241         to override any previous message.   242    243         If 'removed' is specified, a list of participants to be removed is   244         provided.   245    246         If 'added' is specified, a list of participants to be added is provided.   247         """   248    249         organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER"))   250    251         if self.messenger and self.messenger.sender != get_address(organiser):   252             organiser_attr["SENT-BY"] = get_uri(self.messenger.sender)   253    254         to_cancel = []   255    256         if added or removed:   257             attendees = uri_items(self.obj.get_items("ATTENDEE") or [])   258    259             if removed:   260                 remaining = []   261    262                 for attendee, attendee_attr in attendees:   263                     if attendee in removed:   264                         to_cancel.append((attendee, attendee_attr))   265                     else:   266                         remaining.append((attendee, attendee_attr))   267    268                 attendees = remaining   269    270             if added:   271                 for attendee in added:   272                     attendees.append((attendee, {"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"}))   273    274             self.obj["ATTENDEE"] = attendees   275    276         self.update_dtstamp()   277         self.set_sequence(update)   278    279         self.send_message(method, get_address(organiser), for_organiser=True)   280    281         # When cancelling, replace the attendees with those for whom the event   282         # is now cancelled.   283    284         if to_cancel:   285             self.obj["ATTENDEE"] = to_cancel   286             self.send_message("CANCEL", get_address(organiser), for_organiser=True)   287    288             # Just in case more work is done with this event, the attendees are   289             # now restored.   290    291             self.obj["ATTENDEE"] = remaining   292    293         return True   294    295 class Manager(Common):   296    297     "A simple manager application."   298    299     def __init__(self, messenger=None):   300         self.messenger = messenger or Messenger()   301         self.encoding = "utf-8"   302         self.env = CGIEnvironment(self.encoding)   303    304         user = self.env.get_user()   305         Common.__init__(self, user and get_uri(user) or None)   306    307         self.locale = None   308         self.requests = None   309    310         self.out = self.env.get_output()   311         self.page = markup.page()   312    313         self.store = imip_store.FileStore()   314         self.objects = {}   315    316         try:   317             self.publisher = imip_store.FilePublisher()   318         except OSError:   319             self.publisher = None   320    321     def _get_identifiers(self, path_info):   322         parts = path_info.lstrip("/").split("/")   323         if len(parts) == 1:   324             return parts[0], None   325         else:   326             return parts[:2]   327    328     def _get_object(self, uid, recurrenceid=None):   329         if self.objects.has_key((uid, recurrenceid)):   330             return self.objects[(uid, recurrenceid)]   331    332         fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None   333         obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment)   334         return obj   335    336     def _get_requests(self):   337         if self.requests is None:   338             cancellations = self.store.get_cancellations(self.user)   339             requests = set(self.store.get_requests(self.user))   340             self.requests = requests.difference(cancellations)   341         return self.requests   342    343     def _get_request_summary(self):   344         summary = []   345         for uid, recurrenceid in self._get_requests():   346             obj = self._get_object(uid, recurrenceid)   347             if obj:   348                 for start, end in obj.get_periods_for_freebusy(self.get_tzid()):   349                     summary.append((start, end, uid, obj.get_value("TRANSP"), recurrenceid))   350         return summary   351    352     # Preference methods.   353    354     def get_user_locale(self):   355         if not self.locale:   356             self.locale = self.get_preferences().get("LANG", "C")   357         return self.locale   358    359     # Prettyprinting of dates and times.   360    361     def format_date(self, dt, format):   362         return self._format_datetime(babel.dates.format_date, dt, format)   363    364     def format_time(self, dt, format):   365         return self._format_datetime(babel.dates.format_time, dt, format)   366    367     def format_datetime(self, dt, format):   368         return self._format_datetime(   369             isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,   370             dt, format)   371    372     def _format_datetime(self, fn, dt, format):   373         return fn(dt, format=format, locale=self.get_user_locale())   374    375     # Data management methods.   376    377     def remove_request(self, uid, recurrenceid=None):   378         return self.store.dequeue_request(self.user, uid, recurrenceid)   379    380     def remove_event(self, uid, recurrenceid=None):   381         return self.store.remove_event(self.user, uid, recurrenceid)   382    383     def update_freebusy(self, uid, recurrenceid, obj):   384         tzid = self.get_tzid()   385         freebusy = self.store.get_freebusy(self.user)   386         update_freebusy(freebusy, self.user, obj.get_periods_for_freebusy(tzid),   387             obj.get_value("TRANSP"), uid, recurrenceid, self.store)   388    389     def remove_from_freebusy(self, uid, recurrenceid=None):   390         freebusy = self.store.get_freebusy(self.user)   391         remove_from_freebusy(freebusy, self.user, uid, recurrenceid, self.store)   392    393     # Presentation methods.   394    395     def new_page(self, title):   396         self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))   397    398     def status(self, code, message):   399         self.header("Status", "%s %s" % (code, message))   400    401     def header(self, header, value):   402         print >>self.out, "%s: %s" % (header, value)   403    404     def no_user(self):   405         self.status(403, "Forbidden")   406         self.new_page(title="Forbidden")   407         self.page.p("You are not logged in and thus cannot access scheduling requests.")   408    409     def no_page(self):   410         self.status(404, "Not Found")   411         self.new_page(title="Not Found")   412         self.page.p("No page is provided at the given address.")   413    414     def redirect(self, url):   415         self.status(302, "Redirect")   416         self.header("Location", url)   417         self.new_page(title="Redirect")   418         self.page.p("Redirecting to: %s" % url)   419    420     def link_to(self, uid, recurrenceid=None):   421         if recurrenceid:   422             return self.env.new_url("/".join([uid, recurrenceid]))   423         else:   424             return self.env.new_url(uid)   425    426     # Request logic methods.   427    428     def handle_newevent(self):   429    430         """   431         Handle any new event operation, creating a new event and redirecting to   432         the event page for further activity.   433         """   434    435         # Handle a submitted form.   436    437         args = self.env.get_args()   438    439         if not args.has_key("newevent"):   440             return   441    442         # Create a new event using the available information.   443    444         slots = args.get("slot", [])   445         participants = args.get("participants", [])   446    447         if not slots:   448             return   449    450         # Obtain the user's timezone.   451    452         tzid = self.get_tzid()   453    454         # Coalesce the selected slots.   455    456         slots.sort()   457         coalesced = []   458         last = None   459    460         for slot in slots:   461             start, end = slot.split("-")   462             start = get_datetime(start, {"TZID" : tzid})   463             end = end and get_datetime(end, {"TZID" : tzid}) or get_start_of_next_day(start, tzid)   464    465             if last:   466                 last_start, last_end = last   467    468                 # Merge adjacent dates and datetimes.   469    470                 if start == last_end or get_start_of_day(last_end, tzid) == get_start_of_day(start, tzid):   471                     last = last_start, end   472                     continue   473    474                 # Handle datetimes within dates.   475                 # Datetime periods are within single days and are therefore   476                 # discarded.   477    478                 elif get_start_of_day(start, tzid) == get_start_of_day(last_start, tzid):   479                     continue   480    481                 # Add separate dates and datetimes.   482    483                 else:   484                     coalesced.append(last)   485    486             last = start, end   487    488         if last:   489             coalesced.append(last)   490    491         # Invent a unique identifier.   492    493         utcnow = get_timestamp()   494         uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))   495    496         # Define a single occurrence if only one coalesced slot exists.   497         # Otherwise, many occurrences are defined.   498    499         for i, (start, end) in enumerate(coalesced):   500             this_uid = "%s-%s" % (uid, i)   501    502             start_value, start_attr = get_datetime_item(start, tzid)   503             end_value, end_attr = get_datetime_item(end, tzid)   504    505             # Create a calendar object and store it as a request.   506    507             record = []   508             rwrite = record.append   509    510             rwrite(("UID", {}, this_uid))   511             rwrite(("SUMMARY", {}, "New event at %s" % utcnow))   512             rwrite(("DTSTAMP", {}, utcnow))   513             rwrite(("DTSTART", start_attr, start_value))   514             rwrite(("DTEND", end_attr, end_value))   515             rwrite(("ORGANIZER", {}, self.user))   516    517             for participant in participants:   518                 if not participant:   519                     continue   520                 participant = get_uri(participant)   521                 rwrite(("ATTENDEE", {"RSVP" : "TRUE", "PARTSTAT" : "NEEDS-ACTION"}, participant))   522    523             node = ("VEVENT", {}, record)   524    525             self.store.set_event(self.user, this_uid, node=node)   526             self.store.queue_request(self.user, this_uid)   527    528         # Redirect to the object (or the first of the objects), where instead of   529         # attendee controls, there will be organiser controls.   530    531         self.redirect(self.link_to("%s-0" % uid))   532    533     def handle_request(self, uid, obj):   534    535         """   536         Handle actions involving the given 'uid' and 'obj' object, returning an   537         error if one occurred, or None if the request was successfully handled.   538         """   539    540         # Handle a submitted form.   541    542         args = self.env.get_args()   543    544         # Get the possible actions.   545    546         reply = args.has_key("reply")   547         discard = args.has_key("discard")   548         invite = args.has_key("invite")   549         cancel = args.has_key("cancel")   550         save = args.has_key("save")   551    552         have_action = reply or discard or invite or cancel or save   553    554         if not have_action:   555             return ["action"]   556    557         # Update the object.   558    559         if args.has_key("summary"):   560             obj["SUMMARY"] = [(args["summary"][0], {})]   561    562         organisers = uri_dict(obj.get_value_map("ORGANIZER"))   563         attendees = uri_dict(obj.get_value_map("ATTENDEE"))   564    565         if args.has_key("partstat"):   566             for d in attendees, organisers:   567                 if d.has_key(self.user):   568                     d[self.user]["PARTSTAT"] = args["partstat"][0]   569                     if d[self.user].has_key("RSVP"):   570                         del d[self.user]["RSVP"]   571    572         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   573    574         # Obtain the user's timezone and process datetime values.   575    576         update = False   577    578         if is_organiser:   579             dtend_enabled = args.get("dtend-control", [None])[0] == "enable"   580             dttimes_enabled = args.get("dttimes-control", [None])[0] == "enable"   581    582             t = self.handle_date_controls("dtstart", dttimes_enabled)   583             if t:   584                 dtstart, attr = t   585                 update = self.set_datetime_in_object(dtstart, attr.get("TZID"), "DTSTART", obj) or update   586             else:   587                 return ["dtstart"]   588    589             # Handle specified end datetimes.   590    591             if dtend_enabled:   592                 t = self.handle_date_controls("dtend", dttimes_enabled)   593                 if t:   594                     dtend, attr = t   595    596                     # Convert end dates to iCalendar "next day" dates.   597    598                     if not isinstance(dtend, datetime):   599                         dtend += timedelta(1)   600                     update = self.set_datetime_in_object(dtend, attr.get("TZID"), "DTEND", obj) or update   601                 else:   602                     return ["dtend"]   603    604             # Otherwise, treat the end date as the start date. Datetimes are   605             # handled by making the event occupy the rest of the day.   606    607             else:   608                 dtend = dtstart + timedelta(1)   609                 if isinstance(dtstart, datetime):   610                     dtend = get_start_of_day(dtend, attr["TZID"])   611                 update = self.set_datetime_in_object(dtend, attr.get("TZID"), "DTEND", obj) or update   612    613             if dtstart >= dtend:   614                 return ["dtstart", "dtend"]   615    616         # Obtain any participants to be added or removed.   617    618         removed = args.get("remove")   619         added = args.get("added")   620    621         # Process any action.   622    623         handled = True   624    625         if reply or invite or cancel:   626    627             handler = ManagerHandler(obj, self.user, self.messenger)   628    629             # Process the object and remove it from the list of requests.   630    631             if reply and handler.process_received_request(update) or \   632                is_organiser and (invite or cancel) and \   633                handler.process_created_request(invite and "REQUEST" or "CANCEL", update, removed, added):   634    635                 self.remove_request(uid)   636    637         # Save single user events.   638    639         elif save:   640             self.store.set_event(self.user, uid, node=obj.to_node())   641             self.update_freebusy(uid, None, obj=obj)   642             self.remove_request(uid)   643    644         # Remove the request and the object.   645    646         elif discard:   647             self.remove_from_freebusy(uid)   648             self.remove_event(uid)   649             self.remove_request(uid)   650    651         else:   652             handled = False   653    654         # Upon handling an action, redirect to the main page.   655    656         if handled:   657             self.redirect(self.env.get_path())   658    659         return None   660    661     def handle_date_controls(self, name, with_time=True):   662    663         """   664         Handle date control information for fields starting with 'name',   665         returning a (datetime, attr) tuple or None if the fields cannot be used   666         to construct a datetime object.   667         """   668    669         args = self.env.get_args()   670    671         if args.has_key("%s-date" % name):   672             date = args["%s-date" % name][0]   673    674             if with_time:   675                 hour = args.get("%s-hour" % name, [None])[0]   676                 minute = args.get("%s-minute" % name, [None])[0]   677                 second = args.get("%s-second" % name, [None])[0]   678                 tzid = args.get("%s-tzid" % name, [self.get_tzid()])[0]   679    680                 time = (hour or minute or second) and "T%s%s%s" % (hour, minute, second) or ""   681                 value = "%s%s" % (date, time)   682                 attr = {"TZID" : tzid, "VALUE" : "DATE-TIME"}   683                 dt = get_datetime(value, attr)   684             else:   685                 attr = {"VALUE" : "DATE"}   686                 dt = get_datetime(date)   687    688             if dt:   689                 return dt, attr   690    691         return None   692    693     def set_datetime_in_object(self, dt, tzid, property, obj):   694    695         """   696         Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether   697         an update has occurred.   698         """   699    700         if dt:   701             old_value = obj.get_value(property)   702             obj[property] = [get_datetime_item(dt, tzid)]   703             return format_datetime(dt) != old_value   704    705         return False   706    707     # Page fragment methods.   708    709     def show_request_controls(self, obj):   710    711         "Show form controls for a request concerning 'obj'."   712    713         page = self.page   714         args = self.env.get_args()   715    716         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   717    718         attendees = uri_values((obj.get_values("ATTENDEE") or []) + args.get("attendee", []))   719         is_attendee = self.user in attendees   720    721         is_request = (obj.get_value("UID"), obj.get_value("RECURRENCE-ID")) in self._get_requests()   722    723         have_other_attendees = len(attendees) > (is_attendee and 1 or 0)   724    725         # Show appropriate options depending on the role of the user.   726    727         if is_attendee and not is_organiser:   728             page.p("An action is required for this request:")   729    730             page.p()   731             page.input(name="reply", type="submit", value="Reply")   732             page.add(" ")   733             page.input(name="discard", type="submit", value="Discard")   734             page.p.close()   735    736         if is_organiser:   737             if have_other_attendees:   738                 page.p("As organiser, you can perform the following:")   739    740                 page.p()   741                 page.input(name="invite", type="submit", value="Invite")   742                 page.add(" ")   743                 if is_request:   744                     page.input(name="discard", type="submit", value="Discard")   745                 else:   746                     page.input(name="cancel", type="submit", value="Cancel")   747                 page.p.close()   748             else:   749                 page.p("As attendee, you can perform the following:")   750    751                 page.p()   752                 page.input(name="save", type="submit", value="Save")   753                 page.add(" ")   754                 page.input(name="discard", type="submit", value="Discard")   755                 page.p.close()   756    757     property_items = [   758         ("SUMMARY", "Summary"),   759         ("DTSTART", "Start"),   760         ("DTEND", "End"),   761         ("ORGANIZER", "Organiser"),   762         ("ATTENDEE", "Attendee"),   763         ]   764    765     partstat_items = [   766         ("NEEDS-ACTION", "Not confirmed"),   767         ("ACCEPTED", "Attending"),   768         ("TENTATIVE", "Tentatively attending"),   769         ("DECLINED", "Not attending"),   770         ("DELEGATED", "Delegated"),   771         ]   772    773     def show_object_on_page(self, uid, obj, error=None):   774    775         """   776         Show the calendar object with the given 'uid' and representation 'obj'   777         on the current page. If 'error' is given, show a suitable message.   778         """   779    780         page = self.page   781         page.form(method="POST")   782    783         # Obtain the user's timezone.   784    785         tzid = self.get_tzid()   786    787         # Provide controls to change the displayed object.   788    789         args = self.env.get_args()   790    791         # Add or remove new attendees.   792         # This does not affect the stored object.   793    794         existing_attendees = uri_values(obj.get_values("ATTENDEE") or [])   795         new_attendees = args.get("added", [])   796         new_attendee = args.get("attendee", [""])[0]   797    798         if args.has_key("add"):   799             if new_attendee.strip():   800                 new_attendee = get_uri(new_attendee.strip())   801                 if new_attendee not in new_attendees and new_attendee not in existing_attendees:   802                     new_attendees.append(new_attendee)   803                 new_attendee = ""   804    805         if args.has_key("removenew"):   806             removed_attendee = args["removenew"][0]   807             if removed_attendee in new_attendees:   808                 new_attendees.remove(removed_attendee)   809    810         # Configure the start and end datetimes.   811    812         dtend_control = args.get("dtend-control", [None])[0]   813         dttimes_control = args.get("dttimes-control", [None])[0]   814         with_time = dttimes_control == "enable"   815    816         t = self.handle_date_controls("dtstart", with_time)   817         if t:   818             dtstart, dtstart_attr = t   819         else:   820             dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")   821    822         if dtend_control == "enable":   823             t = self.handle_date_controls("dtend", with_time)   824             if t:   825                 dtend, dtend_attr = t   826             else:   827                 dtend, dtend_attr = None, {}   828         elif dtend_control == "disable":   829             dtend, dtend_attr = None, {}   830         else:   831             dtend, dtend_attr = obj.get_datetime_item("DTEND")   832    833         # Change end dates to refer to the actual dates, not the iCalendar   834         # "next day" dates.   835    836         if dtend and not isinstance(dtend, datetime):   837             dtend -= timedelta(1)   838    839         # Show the end datetime controls if already active or if an object needs   840         # them.   841    842         dtend_enabled = dtend_control == "enable" or isinstance(dtend, datetime) or dtstart != dtend   843         dttimes_enabled = dttimes_control == "enable" or isinstance(dtstart, datetime) or isinstance(dtend, datetime)   844    845         if dtend_enabled:   846             page.input(name="dtend-control", type="radio", value="enable", id="dtend-enable", checked="checked")   847             page.input(name="dtend-control", type="radio", value="disable", id="dtend-disable")   848         else:   849             page.input(name="dtend-control", type="radio", value="enable", id="dtend-enable")   850             page.input(name="dtend-control", type="radio", value="disable", id="dtend-disable", checked="checked")   851    852         if dttimes_enabled:   853             page.input(name="dttimes-control", type="radio", value="enable", id="dttimes-enable", checked="checked")   854             page.input(name="dttimes-control", type="radio", value="disable", id="dttimes-disable")   855         else:   856             page.input(name="dttimes-control", type="radio", value="enable", id="dttimes-enable")   857             page.input(name="dttimes-control", type="radio", value="disable", id="dttimes-disable", checked="checked")   858    859         # Provide a summary of the object.   860    861         page.table(class_="object", cellspacing=5, cellpadding=5)   862         page.thead()   863         page.tr()   864         page.th("Event", class_="mainheading", colspan=2)   865         page.tr.close()   866         page.thead.close()   867         page.tbody()   868    869         is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user   870    871         for name, label in self.property_items:   872             page.tr()   873    874             # Handle datetimes specially.   875    876             if name in ["DTSTART", "DTEND"]:   877                 field = name.lower()   878    879                 page.th(label, class_="objectheading %s%s" % (field, error and field in error and " error" or ""))   880    881                 # Obtain the datetime.   882    883                 if name == "DTSTART":   884                     dt, attr, event_tzid = dtstart, dtstart_attr, dtstart_attr.get("TZID", tzid)   885    886                 # Where no end datetime exists, use the start datetime as the   887                 # basis of any potential datetime specified if dt-control is   888                 # set.   889    890                 else:   891                     dt, attr, event_tzid = dtend or dtstart, dtend_attr or dtstart_attr, (dtend_attr or dtstart_attr).get("TZID", tzid)   892    893                 # Show controls for editing as organiser.   894    895                 if is_organiser:   896                     value = format_datetime(dt)   897    898                     page.td(class_="objectvalue %s" % field)   899                     if name == "DTEND":   900                         page.div(class_="dt disabled")   901                         page.label("Specify end date", for_="dtend-enable", class_="enable")   902                         page.div.close()   903    904                     page.div(class_="dt enabled")   905                     self._show_date_controls(field, value, attr, tzid)   906                     if name == "DTSTART":   907                         page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")   908                         page.label("Specify dates only", for_="dttimes-disable", class_="time enabled disable")   909                     elif name == "DTEND":   910                         page.label("End on same day", for_="dtend-disable", class_="disable")   911                     page.div.close()   912    913                     page.td.close()   914    915                 # Show a label as attendee.   916    917                 else:   918                     page.td(self.format_datetime(dt, "full"))   919    920                 page.tr.close()   921    922             # Handle the summary specially.   923    924             elif name == "SUMMARY":   925                 value = args.get("summary", [obj.get_value(name)])[0]   926    927                 page.th(label, class_="objectheading")   928                 page.td()   929                 if is_organiser:   930                     page.input(name="summary", type="text", value=value, size=80)   931                 else:   932                     page.add(value)   933                 page.td.close()   934                 page.tr.close()   935    936             # Handle potentially many values.   937    938             else:   939                 items = obj.get_items(name) or []   940                 rowspan = len(items)   941    942                 if name == "ATTENDEE":   943                     rowspan += len(new_attendees) + 1   944                 elif not items:   945                     continue   946    947                 page.th(label, class_="objectheading", rowspan=rowspan)   948    949                 first = True   950    951                 for i, (value, attr) in enumerate(items):   952                     if not first:   953                         page.tr()   954                     else:   955                         first = False   956    957                     if name in ("ATTENDEE", "ORGANIZER"):   958                         value = get_uri(value)   959    960                         page.td(class_="objectvalue")   961                         page.add(value)   962                         page.add(" ")   963    964                         partstat = attr.get("PARTSTAT")   965                         if value == self.user and (not is_organiser or name == "ORGANIZER"):   966                             self._show_menu("partstat", partstat, self.partstat_items, "partstat")   967                         else:   968                             page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")   969    970                         if is_organiser and name == "ATTENDEE":   971                             if value in args.get("remove", []):   972                                 page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove", checked="checked")   973                             else:   974                                 page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove")   975                             page.label("Remove", for_="remove-%d" % i, class_="remove")   976                             page.label("Uninvited", for_="remove-%d" % i, class_="removed")   977    978                     else:   979                         page.td(class_="objectvalue")   980                         page.add(value)   981    982                     page.td.close()   983                     page.tr.close()   984    985                 # Allow more attendees to be specified.   986    987                 if is_organiser and name == "ATTENDEE":   988                     for i, attendee in enumerate(new_attendees):   989                         if not first:   990                             page.tr()   991                         else:   992                             first = False   993    994                         page.td()   995                         page.input(name="added", type="value", value=attendee)   996                         page.input(name="removenew", type="submit", value=attendee, id="removenew-%d" % i, class_="remove")   997                         page.label("Remove", for_="removenew-%d" % i, class_="remove")   998                         page.td.close()   999                         page.tr.close()  1000   1001                     if not first:  1002                         page.tr()  1003   1004                     page.td()  1005                     page.input(name="attendee", type="value", value=new_attendee)  1006                     page.input(name="add", type="submit", value="add", id="add-%d" % i, class_="add")  1007                     page.label("Add", for_="add-%d" % i, class_="add")  1008                     page.td.close()  1009                     page.tr.close()  1010   1011         page.tbody.close()  1012         page.table.close()  1013   1014         self.show_recurrences(obj)  1015         self.show_conflicting_events(uid, obj)  1016         self.show_request_controls(obj)  1017   1018         page.form.close()  1019   1020     def show_recurrences(self, obj):  1021   1022         "Show recurrences for the object having the given representation 'obj'."  1023   1024         page = self.page  1025   1026         # Obtain the user's timezone.  1027   1028         tzid = self.get_tzid()  1029   1030         window_size = 100  1031   1032         periods = obj.get_periods(self.get_tzid(), window_size)  1033   1034         if len(periods) == 1:  1035             return  1036   1037         page.p("This event occurs on the following occasions within the next %d days:" % window_size)  1038   1039         page.table(cellspacing=5, cellpadding=5, class_="conflicts")  1040         page.thead()  1041         page.tr()  1042         page.th("Start")  1043         page.th("End")  1044         page.tr.close()  1045         page.thead.close()  1046         page.tbody()  1047   1048         for start, end in periods:  1049             page.tr()  1050             page.td(self.format_datetime(start, "long"))  1051             page.td(self.format_datetime(end, "long"))  1052             page.tr.close()  1053   1054         page.tbody.close()  1055         page.table.close()  1056   1057     def show_conflicting_events(self, uid, obj):  1058   1059         """  1060         Show conflicting events for the object having the given 'uid' and  1061         representation 'obj'.  1062         """  1063   1064         page = self.page  1065   1066         # Obtain the user's timezone.  1067   1068         tzid = self.get_tzid()  1069   1070         dtstart = format_datetime(obj.get_utc_datetime("DTSTART"))  1071         dtend = format_datetime(obj.get_utc_datetime("DTEND"))  1072   1073         # Indicate whether there are conflicting events.  1074   1075         freebusy = self.store.get_freebusy(self.user)  1076   1077         if freebusy:  1078   1079             # Obtain any time zone details from the suggested event.  1080   1081             _dtstart, attr = obj.get_item("DTSTART")  1082             tzid = attr.get("TZID", tzid)  1083   1084             # Show any conflicts.  1085   1086             conflicts = [t for t in have_conflict(freebusy, [(dtstart, dtend)], True) if t[2] != uid]  1087   1088             if conflicts:  1089                 page.p("This event conflicts with others:")  1090   1091                 page.table(cellspacing=5, cellpadding=5, class_="conflicts")  1092                 page.thead()  1093                 page.tr()  1094                 page.th("Event")  1095                 page.th("Start")  1096                 page.th("End")  1097                 page.tr.close()  1098                 page.thead.close()  1099                 page.tbody()  1100   1101                 for t in conflicts:  1102                     start, end, found_uid, transp, found_recurrenceid = t[:5]  1103   1104                     # Provide details of any conflicting event.  1105   1106                     start = self.format_datetime(to_timezone(get_datetime(start), tzid), "long")  1107                     end = self.format_datetime(to_timezone(get_datetime(end), tzid), "long")  1108   1109                     page.tr()  1110   1111                     # Show the event summary for the conflicting event.  1112   1113                     page.td()  1114   1115                     found_obj = self._get_object(found_uid, found_recurrenceid)  1116                     if found_obj:  1117                         page.a(found_obj.get_value("SUMMARY"), href=self.link_to(found_uid))  1118                     else:  1119                         page.add("No details available")  1120   1121                     page.td.close()  1122   1123                     page.td(start)  1124                     page.td(end)  1125   1126                     page.tr.close()  1127   1128                 page.tbody.close()  1129                 page.table.close()  1130   1131     def show_requests_on_page(self):  1132   1133         "Show requests for the current user."  1134   1135         # NOTE: This list could be more informative, but it is envisaged that  1136         # NOTE: the requests would be visited directly anyway.  1137   1138         requests = self._get_requests()  1139   1140         self.page.div(id="pending-requests")  1141   1142         if requests:  1143             self.page.p("Pending requests:")  1144   1145             self.page.ul()  1146   1147             for uid, recurrenceid in requests:  1148                 obj = self._get_object(uid, recurrenceid)  1149                 if obj:  1150                     self.page.li()  1151                     self.page.a(obj.get_value("SUMMARY"), href="#request-%s-%s" % (uid, recurrenceid or ""))  1152                     self.page.li.close()  1153   1154             self.page.ul.close()  1155   1156         else:  1157             self.page.p("There are no pending requests.")  1158   1159         self.page.div.close()  1160   1161     def show_participants_on_page(self):  1162   1163         "Show participants for scheduling purposes."  1164   1165         args = self.env.get_args()  1166         participants = args.get("participants", [])  1167   1168         try:  1169             for name, value in args.items():  1170                 if name.startswith("remove-participant-"):  1171                     i = int(name[len("remove-participant-"):])  1172                     del participants[i]  1173                     break  1174         except ValueError:  1175             pass  1176   1177         # Trim empty participants.  1178   1179         while participants and not participants[-1].strip():  1180             participants.pop()  1181   1182         # Show any specified participants together with controls to remove and  1183         # add participants.  1184   1185         self.page.div(id="participants")  1186   1187         self.page.p("Participants for scheduling:")  1188   1189         for i, participant in enumerate(participants):  1190             self.page.p()  1191             self.page.input(name="participants", type="text", value=participant)  1192             self.page.input(name="remove-participant-%d" % i, type="submit", value="Remove")  1193             self.page.p.close()  1194   1195         self.page.p()  1196         self.page.input(name="participants", type="text")  1197         self.page.input(name="add-participant", type="submit", value="Add")  1198         self.page.p.close()  1199   1200         self.page.div.close()  1201   1202         return participants  1203   1204     # Full page output methods.  1205   1206     def show_object(self, path_info):  1207   1208         "Show an object request using the given 'path_info' for the current user."  1209   1210         uid, recurrenceid = self._get_identifiers(path_info)  1211         obj = self._get_object(uid, recurrenceid)  1212   1213         if not obj:  1214             return False  1215   1216         error = self.handle_request(uid, obj)  1217   1218         if not error:  1219             return True  1220   1221         self.new_page(title="Event")  1222         self.show_object_on_page(uid, obj, error)  1223   1224         return True  1225   1226     def show_calendar(self):  1227   1228         "Show the calendar for the current user."  1229   1230         handled = self.handle_newevent()  1231   1232         self.new_page(title="Calendar")  1233         page = self.page  1234   1235         # Form controls are used in various places on the calendar page.  1236   1237         page.form(method="POST")  1238   1239         self.show_requests_on_page()  1240         participants = self.show_participants_on_page()  1241   1242         # Show a button for scheduling a new event.  1243   1244         page.p(class_="controls")  1245         page.input(name="newevent", type="submit", value="New event", id="newevent", accesskey="N")  1246         page.input(name="reset", type="submit", value="Clear selections", id="reset")  1247         page.p.close()  1248   1249         # Show controls for hiding empty days and busy slots.  1250         # The positioning of the control, paragraph and table are important here.  1251   1252         page.input(name="showdays", type="checkbox", value="show", id="showdays", accesskey="D")  1253         page.input(name="hidebusy", type="checkbox", value="hide", id="hidebusy", accesskey="B")  1254   1255         page.p(class_="controls")  1256         page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable")  1257         page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable")  1258         page.label("Show empty days", for_="showdays", class_="showdays disable")  1259         page.label("Hide empty days", for_="showdays", class_="showdays enable")  1260         page.p.close()  1261   1262         freebusy = self.store.get_freebusy(self.user)  1263   1264         if not freebusy:  1265             page.p("No events scheduled.")  1266             return  1267   1268         # Obtain the user's timezone.  1269   1270         tzid = self.get_tzid()  1271   1272         # Day view: start at the earliest known day and produce days until the  1273         # latest known day, perhaps with expandable sections of empty days.  1274   1275         # Month view: start at the earliest known month and produce months until  1276         # the latest known month, perhaps with expandable sections of empty  1277         # months.  1278   1279         # Details of users to invite to new events could be superimposed on the  1280         # calendar.  1281   1282         # Requests are listed and linked to their tentative positions in the  1283         # calendar. Other participants are also shown.  1284   1285         request_summary = self._get_request_summary()  1286   1287         period_groups = [request_summary, freebusy]  1288         period_group_types = ["request", "freebusy"]  1289         period_group_sources = ["Pending requests", "Your schedule"]  1290   1291         for i, participant in enumerate(participants):  1292             period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant)))  1293             period_group_types.append("freebusy-part%d" % i)  1294             period_group_sources.append(participant)  1295   1296         groups = []  1297         group_columns = []  1298         group_types = period_group_types  1299         group_sources = period_group_sources  1300         all_points = set()  1301   1302         # Obtain time point information for each group of periods.  1303   1304         for periods in period_groups:  1305             periods = convert_periods(periods, tzid)  1306   1307             # Get the time scale with start and end points.  1308   1309             scale = get_scale(periods)  1310   1311             # Get the time slots for the periods.  1312   1313             slots = get_slots(scale)  1314   1315             # Add start of day time points for multi-day periods.  1316   1317             add_day_start_points(slots, tzid)  1318   1319             # Record the slots and all time points employed.  1320   1321             groups.append(slots)  1322             all_points.update([point for point, active in slots])  1323   1324         # Partition the groups into days.  1325   1326         days = {}  1327         partitioned_groups = []  1328         partitioned_group_types = []  1329         partitioned_group_sources = []  1330   1331         for slots, group_type, group_source in zip(groups, group_types, group_sources):  1332   1333             # Propagate time points to all groups of time slots.  1334   1335             add_slots(slots, all_points)  1336   1337             # Count the number of columns employed by the group.  1338   1339             columns = 0  1340   1341             # Partition the time slots by day.  1342   1343             partitioned = {}  1344   1345             for day, day_slots in partition_by_day(slots).items():  1346                 intervals = []  1347                 last = None  1348   1349                 for point, active in day_slots:  1350                     columns = max(columns, len(active))  1351                     if last:  1352                         intervals.append((last, point))  1353                     last = point  1354   1355                 if last:  1356                     intervals.append((last, None))  1357   1358                 if not days.has_key(day):  1359                     days[day] = set()  1360   1361                 # Convert each partition to a mapping from points to active  1362                 # periods.  1363   1364                 partitioned[day] = dict(day_slots)  1365   1366                 # Record the divisions or intervals within each day.  1367   1368                 days[day].update(intervals)  1369   1370             if group_type != "request" or columns:  1371                 group_columns.append(columns)  1372                 partitioned_groups.append(partitioned)  1373                 partitioned_group_types.append(group_type)  1374                 partitioned_group_sources.append(group_source)  1375   1376         # Add empty days.  1377   1378         add_empty_days(days, tzid)  1379   1380         # Show the controls permitting day selection.  1381   1382         self.show_calendar_day_controls(days)  1383   1384         # Show the calendar itself.  1385   1386         page.table(cellspacing=5, cellpadding=5, class_="calendar")  1387         self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)  1388         self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns)  1389         page.table.close()  1390   1391         # End the form region.  1392   1393         page.form.close()  1394   1395     # More page fragment methods.  1396   1397     def show_calendar_day_controls(self, days):  1398   1399         "Show controls for the given 'days' in the calendar."  1400   1401         page = self.page  1402         slots = self.env.get_args().get("slot", [])  1403   1404         for day in days:  1405             value, identifier = self._day_value_and_identifier(day)  1406             self._slot_selector(value, identifier, slots)  1407   1408         # Generate a dynamic stylesheet to allow day selections to colour  1409         # specific days.  1410         # NOTE: The style details need to be coordinated with the static  1411         # NOTE: stylesheet.  1412   1413         page.style(type="text/css")  1414   1415         for day in days:  1416             daystr = format_datetime(day)  1417             page.add("""\  1418 input.newevent.selector#day-%s-:checked ~ table label.day.day-%s,  1419 input.newevent.selector#day-%s-:checked ~ table label.timepoint.day-%s {  1420     background-color: #5f4;  1421     text-decoration: underline;  1422 }  1423 """ % (daystr, daystr, daystr, daystr))  1424   1425         page.style.close()  1426   1427     def show_calendar_participant_headings(self, group_types, group_sources, group_columns):  1428   1429         """  1430         Show headings for the participants and other scheduling contributors,  1431         defined by 'group_types', 'group_sources' and 'group_columns'.  1432         """  1433   1434         page = self.page  1435   1436         page.colgroup(span=1, id="columns-timeslot")  1437   1438         for group_type, columns in zip(group_types, group_columns):  1439             page.colgroup(span=max(columns, 1), id="columns-%s" % group_type)  1440   1441         page.thead()  1442         page.tr()  1443         page.th("", class_="emptyheading")  1444   1445         for group_type, source, columns in zip(group_types, group_sources, group_columns):  1446             page.th(source,  1447                 class_=(group_type == "request" and "requestheading" or "participantheading"),  1448                 colspan=max(columns, 1))  1449   1450         page.tr.close()  1451         page.thead.close()  1452   1453     def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns):  1454   1455         """  1456         Show calendar days, defined by a collection of 'days', the contributing  1457         period information as 'partitioned_groups' (partitioned by day), the  1458         'partitioned_group_types' indicating the kind of contribution involved,  1459         and the 'group_columns' defining the number of columns in each group.  1460         """  1461   1462         page = self.page  1463   1464         # Determine the number of columns required. Where participants provide  1465         # no columns for events, one still needs to be provided for the  1466         # participant itself.  1467   1468         all_columns = sum([max(columns, 1) for columns in group_columns])  1469   1470         # Determine the days providing time slots.  1471   1472         all_days = days.items()  1473         all_days.sort()  1474   1475         # Produce a heading and time points for each day.  1476   1477         for day, intervals in all_days:  1478             groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]  1479             is_empty = True  1480   1481             for slots in groups_for_day:  1482                 if not slots:  1483                     continue  1484   1485                 for active in slots.values():  1486                     if active:  1487                         is_empty = False  1488                         break  1489   1490             page.thead(class_="separator%s" % (is_empty and " empty" or ""))  1491             page.tr()  1492             page.th(class_="dayheading container", colspan=all_columns+1)  1493             self._day_heading(day)  1494             page.th.close()  1495             page.tr.close()  1496             page.thead.close()  1497   1498             page.tbody(class_="points%s" % (is_empty and " empty" or ""))  1499             self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns)  1500             page.tbody.close()  1501   1502     def show_calendar_points(self, intervals, groups, group_types, group_columns):  1503   1504         """  1505         Show the time 'intervals' along with period information from the given  1506         'groups', having the indicated 'group_types', each with the number of  1507         columns given by 'group_columns'.  1508         """  1509   1510         page = self.page  1511   1512         # Obtain the user's timezone.  1513   1514         tzid = self.get_tzid()  1515   1516         # Produce a row for each interval.  1517   1518         intervals = list(intervals)  1519         intervals.sort()  1520   1521         for point, endpoint in intervals:  1522             continuation = point == get_start_of_day(point, tzid)  1523   1524             # Some rows contain no period details and are marked as such.  1525   1526             have_active = reduce(lambda x, y: x or y, [slots and slots.get(point) for slots in groups], None)  1527   1528             css = " ".join(  1529                 ["slot"] +  1530                 (have_active and ["busy"] or ["empty"]) +  1531                 (continuation and ["daystart"] or [])  1532                 )  1533   1534             page.tr(class_=css)  1535             page.th(class_="timeslot")  1536             self._time_point(point, endpoint)  1537             page.th.close()  1538   1539             # Obtain slots for the time point from each group.  1540   1541             for columns, slots, group_type in zip(group_columns, groups, group_types):  1542                 active = slots and slots.get(point)  1543   1544                 # Where no periods exist for the given time interval, generate  1545                 # an empty cell. Where a participant provides no periods at all,  1546                 # the colspan is adjusted to be 1, not 0.  1547   1548                 if not active:  1549                     page.td(class_="empty container", colspan=max(columns, 1))  1550                     self._empty_slot(point, endpoint)  1551                     page.td.close()  1552                     continue  1553   1554                 slots = slots.items()  1555                 slots.sort()  1556                 spans = get_spans(slots)  1557   1558                 empty = 0  1559   1560                 # Show a column for each active period.  1561   1562                 for t in active:  1563                     if t and len(t) >= 2:  1564   1565                         # Flush empty slots preceding this one.  1566   1567                         if empty:  1568                             page.td(class_="empty container", colspan=empty)  1569                             self._empty_slot(point, endpoint)  1570                             page.td.close()  1571                             empty = 0  1572   1573                         start, end, uid, recurrenceid, key = get_freebusy_details(t)  1574                         span = spans[key]  1575   1576                         # Produce a table cell only at the start of the period  1577                         # or when continued at the start of a day.  1578   1579                         if point == start or continuation:  1580   1581                             obj = self._get_object(uid, recurrenceid)  1582   1583                             has_continued = continuation and point != start  1584                             will_continue = not ends_on_same_day(point, end, tzid)  1585                             is_organiser = obj and get_uri(obj.get_value("ORGANIZER")) == self.user  1586   1587                             css = " ".join(  1588                                 ["event"] +  1589                                 (has_continued and ["continued"] or []) +  1590                                 (will_continue and ["continues"] or []) +  1591                                 (is_organiser and ["organising"] or ["attending"])  1592                                 )  1593   1594                             # Only anchor the first cell of events.  1595                             # NOTE: Need to only anchor the first period for a  1596                             # NOTE: recurring event.  1597   1598                             if point == start:  1599                                 page.td(class_=css, rowspan=span, id="%s-%s-%s" % (group_type, uid, recurrenceid or ""))  1600                             else:  1601                                 page.td(class_=css, rowspan=span)  1602   1603                             if not obj:  1604                                 page.span("(Participant is busy)")  1605                             else:  1606                                 summary = obj.get_value("SUMMARY")  1607   1608                                 # Only link to events if they are not being  1609                                 # updated by requests.  1610   1611                                 if (uid, recurrenceid) in self._get_requests() and group_type != "request":  1612                                     page.span(summary)  1613                                 else:  1614                                     page.a(summary, href=self.link_to(uid, recurrenceid))  1615   1616                             page.td.close()  1617                     else:  1618                         empty += 1  1619   1620                 # Pad with empty columns.  1621   1622                 empty = columns - len(active)  1623   1624                 if empty:  1625                     page.td(class_="empty container", colspan=empty)  1626                     self._empty_slot(point, endpoint)  1627                     page.td.close()  1628   1629             page.tr.close()  1630   1631     def _day_heading(self, day):  1632   1633         """  1634         Generate a heading for 'day' of the following form:  1635   1636         <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label>  1637         """  1638   1639         page = self.page  1640         daystr = format_datetime(day)  1641         value, identifier = self._day_value_and_identifier(day)  1642         page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier)  1643   1644     def _time_point(self, point, endpoint):  1645   1646         """  1647         Generate headings for the 'point' to 'endpoint' period of the following  1648         form:  1649   1650         <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>  1651         <span class="endpoint">10:00:00 CET</span>  1652         """  1653   1654         page = self.page  1655         tzid = self.get_tzid()  1656         daystr = format_datetime(point.date())  1657         value, identifier = self._slot_value_and_identifier(point, endpoint)  1658         slots = self.env.get_args().get("slot", [])  1659         self._slot_selector(value, identifier, slots)  1660         page.label(self.format_time(point, "long"), class_="timepoint day-%s" % daystr, for_=identifier)  1661         page.span(self.format_time(endpoint or get_end_of_day(point, tzid), "long"), class_="endpoint")  1662   1663     def _slot_selector(self, value, identifier, slots):  1664         reset = self.env.get_args().has_key("reset")  1665         page = self.page  1666         if not reset and value in slots:  1667             page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector", checked="checked")  1668         else:  1669             page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector")  1670   1671     def _empty_slot(self, point, endpoint):  1672         page = self.page  1673         value, identifier = self._slot_value_and_identifier(point, endpoint)  1674         page.label("Select/deselect period", class_="newevent popup", for_=identifier)  1675   1676     def _day_value_and_identifier(self, day):  1677         value = "%s-" % format_datetime(day)  1678         identifier = "day-%s" % value  1679         return value, identifier  1680   1681     def _slot_value_and_identifier(self, point, endpoint):  1682         value = "%s-%s" % (format_datetime(point), endpoint and format_datetime(endpoint) or "")  1683         identifier = "slot-%s" % value  1684         return value, identifier  1685   1686     def _show_menu(self, name, default, items, class_=""):  1687         page = self.page  1688         values = self.env.get_args().get(name, [default])  1689         page.select(name=name, class_=class_)  1690         for v, label in items:  1691             if v in values:  1692                 page.option(label, value=v, selected="selected")  1693             else:  1694                 page.option(label, value=v)  1695         page.select.close()  1696   1697     def _show_date_controls(self, name, default, attr, tzid):  1698   1699         """  1700         Show date controls for a field with the given 'name' and 'default' value  1701         and 'attr', with the given 'tzid' being used if no other time regime  1702         information is provided.  1703         """  1704   1705         page = self.page  1706         args = self.env.get_args()  1707   1708         event_tzid = attr.get("TZID", tzid)  1709         dt = get_datetime(default, attr)  1710   1711         # Show dates for up to one week around the current date.  1712   1713         base = get_date(dt)  1714         items = []  1715         for i in range(-7, 8):  1716             d = base + timedelta(i)  1717             items.append((format_datetime(d), self.format_date(d, "full")))  1718   1719         self._show_menu("%s-date" % name, format_datetime(base), items)  1720   1721         # Show time details.  1722   1723         dt_time = isinstance(dt, datetime) and dt or None  1724         hour = args.get("%s-hour" % name, "%02d" % (dt_time and dt_time.hour or 0))  1725         minute = args.get("%s-minute" % name, "%02d" % (dt_time and dt_time.minute or 0))  1726         second = args.get("%s-second" % name, "%02d" % (dt_time and dt_time.second or 0))  1727   1728         page.span(class_="time enabled")  1729         page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2)  1730         page.add(":")  1731         page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2)  1732         page.add(":")  1733         page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2)  1734         page.add(" ")  1735         self._show_menu("%s-tzid" % name, event_tzid,  1736             [(event_tzid, event_tzid)] + (  1737             event_tzid != tzid and [(tzid, tzid)] or []  1738             ))  1739         page.span.close()  1740   1741     # Incoming HTTP request direction.  1742   1743     def select_action(self):  1744   1745         "Select the desired action and show the result."  1746   1747         path_info = self.env.get_path_info().strip("/")  1748   1749         if not path_info:  1750             self.show_calendar()  1751         elif self.show_object(path_info):  1752             pass  1753         else:  1754             self.no_page()  1755   1756     def __call__(self):  1757   1758         "Interpret a request and show an appropriate response."  1759   1760         if not self.user:  1761             self.no_user()  1762         else:  1763             self.select_action()  1764   1765         # Write the headers and actual content.  1766   1767         print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding  1768         print >>self.out  1769         self.out.write(unicode(self.page).encode(self.encoding))  1770   1771 if __name__ == "__main__":  1772     Manager()()  1773   1774 # vim: tabstop=4 expandtab shiftwidth=4