imip-agent

imip_manager.py

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