imip-agent

imipweb/resource.py

794:481df9da00f0
2015-09-29 Paul Boddie Added support for address usage when specifying attendees, along with usage of the CN attribute for attendees and organisers.
     1 #!/usr/bin/env python     2      3 """     4 Common resource functionality for Web calendar clients.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from datetime import datetime, timedelta    23 from imiptools.client import Client, ClientForObject    24 from imiptools.data import get_uri, uri_values    25 from imiptools.dates import format_datetime, get_recurrence_start_point, to_date    26 from imiptools.period import remove_period, remove_affected_period    27 from imipweb.data import event_period_from_period, form_period_from_period, FormDate    28 from imipweb.env import CGIEnvironment    29 from urllib import urlencode    30 import babel.dates    31 import imip_store    32 import markup    33 import pytz    34     35 class Resource:    36     37     "A Web application resource."    38     39     def __init__(self, resource=None):    40     41         """    42         Initialise a resource, allowing it to share the environment of any given    43         existing 'resource'.    44         """    45     46         self.encoding = "utf-8"    47         self.env = CGIEnvironment(self.encoding)    48     49         self.objects = {}    50         self.locale = None    51         self.requests = None    52     53         self.out = resource and resource.out or self.env.get_output()    54         self.page = resource and resource.page or markup.page()    55         self.html_ids = None    56     57     # Presentation methods.    58     59     def new_page(self, title):    60         self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))    61         self.html_ids = set()    62     63     def status(self, code, message):    64         self.header("Status", "%s %s" % (code, message))    65     66     def header(self, header, value):    67         print >>self.out, "%s: %s" % (header, value)    68     69     def no_user(self):    70         self.status(403, "Forbidden")    71         self.new_page(title="Forbidden")    72         self.page.p("You are not logged in and thus cannot access scheduling requests.")    73     74     def no_page(self):    75         self.status(404, "Not Found")    76         self.new_page(title="Not Found")    77         self.page.p("No page is provided at the given address.")    78     79     def redirect(self, url):    80         self.status(302, "Redirect")    81         self.header("Location", url)    82         self.new_page(title="Redirect")    83         self.page.p("Redirecting to: %s" % url)    84     85     def link_to(self, uid, recurrenceid=None, args=None):    86     87         """    88         Return a link to an object with the given 'uid' and 'recurrenceid'.    89         See get_identifiers for the decoding of such links.    90     91         If 'args' is specified, the given dictionary is encoded and included.    92         """    93     94         path = [uid]    95         if recurrenceid:    96             path.append(recurrenceid)    97         return "%s%s" % (self.env.new_url("/".join(path)), args and ("?%s" % urlencode(args)) or "")    98     99     # Access to objects.   100    101     def get_identifiers(self, path_info):   102    103         """   104         Return identifiers provided by 'path_info', potentially encoded by   105         'link_to'.   106         """   107    108         parts = path_info.lstrip("/").split("/")   109    110         # UID only.   111    112         if len(parts) == 1:   113             return parts[0], None   114    115         # UID and RECURRENCE-ID.   116    117         else:   118             return parts[:2]   119    120     def _get_object(self, uid, recurrenceid=None, section=None, username=None):   121         if self.objects.has_key((uid, recurrenceid, section, username)):   122             return self.objects[(uid, recurrenceid, section, username)]   123    124         obj = self.objects[(uid, recurrenceid, section, username)] = self.get_stored_object(uid, recurrenceid, section, username)   125         return obj   126    127     def _get_recurrences(self, uid):   128         return self.store.get_recurrences(self.user, uid)   129    130     def _get_active_recurrences(self, uid):   131         return self.store.get_active_recurrences(self.user, uid)   132    133     def _get_requests(self):   134         if self.requests is None:   135             self.requests = self.store.get_requests(self.user)   136         return self.requests   137    138     def _have_request(self, uid, recurrenceid=None, type=None, strict=False):   139         return self.store.have_request(self._get_requests(), uid, recurrenceid, type, strict)   140    141     def _get_counters(self, uid, recurrenceid=None):   142         return self.store.get_counters(self.user, uid, recurrenceid)   143    144     def _get_request_summary(self):   145    146         "Return a list of periods comprising the request summary."   147    148         summary = []   149    150         for uid, recurrenceid, request_type in self._get_requests():   151    152             # Obtain either normal objects or counter-proposals.   153    154             if not request_type:   155                 objs = [self._get_object(uid, recurrenceid)]   156             elif request_type == "COUNTER":   157                 objs = []   158                 for attendee in self.store.get_counters(self.user, uid, recurrenceid):   159                     objs.append(self._get_object(uid, recurrenceid, "counters", attendee))   160    161             # For each object, obtain the periods involved.   162    163             for obj in objs:   164                 if obj:   165                     recurrenceids = self._get_active_recurrences(uid)   166    167                     # Obtain only active periods, not those replaced by redefined   168                     # recurrences, converting to free/busy periods.   169    170                     for p in obj.get_active_periods(recurrenceids, self.get_tzid(), self.get_window_end()):   171                         summary.append(obj.get_freebusy_period(p))   172    173         return summary   174    175     # Preference methods.   176    177     def get_user_locale(self):   178         if not self.locale:   179             self.locale = self.get_preferences().get("LANG", "en")   180         return self.locale   181    182     # Prettyprinting of dates and times.   183    184     def format_date(self, dt, format):   185         return self._format_datetime(babel.dates.format_date, dt, format)   186    187     def format_time(self, dt, format):   188         return self._format_datetime(babel.dates.format_time, dt, format)   189    190     def format_datetime(self, dt, format):   191         return self._format_datetime(   192             isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,   193             dt, format)   194    195     def _format_datetime(self, fn, dt, format):   196         return fn(dt, format=format, locale=self.get_user_locale())   197    198     # Data management methods.   199    200     def remove_request(self, uid, recurrenceid=None):   201         return self.store.dequeue_request(self.user, uid, recurrenceid)   202    203     def remove_event(self, uid, recurrenceid=None):   204         return self.store.remove_event(self.user, uid, recurrenceid)   205    206 class ResourceClient(Resource, Client):   207    208     "A Web application resource and calendar client."   209    210     def __init__(self, resource=None):   211         Resource.__init__(self, resource)   212         user = self.env.get_user()   213         Client.__init__(self, user and get_uri(user) or None)   214    215 class ResourceClientForObject(Resource, ClientForObject):   216    217     "A Web application resource and calendar client for a specific object."   218    219     def __init__(self, resource=None):   220         Resource.__init__(self, resource)   221         user = self.env.get_user()   222         ClientForObject.__init__(self, None, user and get_uri(user) or None)   223    224 class FormUtilities:   225    226     "Utility methods resource mix-in."   227    228     def control(self, name, type, value, selected=False, **kw):   229    230         """   231         Show a control with the given 'name', 'type' and 'value', with   232         'selected' indicating whether it should be selected (checked or   233         equivalent), and with keyword arguments setting other properties.   234         """   235    236         page = self.page   237         if type in ("checkbox", "radio") and selected:   238             page.input(name=name, type=type, value=value, checked=selected, **kw)   239         else:   240             page.input(name=name, type=type, value=value, **kw)   241    242     def menu(self, name, default, items, class_="", index=None):   243    244         """   245         Show a select menu having the given 'name', set to the given 'default',   246         providing the given (value, label) 'items', and employing the given CSS   247         'class_' if specified.   248         """   249    250         page = self.page   251         values = self.env.get_args().get(name, [default])   252         if index is not None:   253             values = values[index:]   254             values = values and values[0:1] or [default]   255    256         page.select(name=name, class_=class_)   257         for v, label in items:   258             if v is None:   259                 continue   260             if v in values:   261                 page.option(label, value=v, selected="selected")   262             else:   263                 page.option(label, value=v)   264         page.select.close()   265    266     def date_controls(self, name, default, index=None, show_tzid=True, read_only=False):   267    268         """   269         Show date controls for a field with the given 'name' and 'default' form   270         date value.   271    272         If 'index' is specified, default field values will be overridden by the   273         element from a collection of existing form values with the specified   274         index; otherwise, field values will be overridden by a single form   275         value.   276    277         If 'show_tzid' is set to a false value, the time zone menu will not be   278         provided.   279    280         If 'read_only' is set to a true value, the controls will be hidden and   281         labels will be employed instead.   282         """   283    284         page = self.page   285    286         # Show dates for up to one week around the current date.   287    288         dt = default.as_datetime()   289         if not dt:   290             dt = date.today()   291    292         base = to_date(dt)   293    294         # Show a date label with a hidden field if read-only.   295    296         if read_only:   297             self.control("%s-date" % name, "hidden", format_datetime(base))   298             page.span(self.format_date(base, "long"))   299    300         # Show dates for up to one week around the current date.   301         # NOTE: Support paging to other dates.   302    303         else:   304             items = []   305             for i in range(-7, 8):   306                 d = base + timedelta(i)   307                 items.append((format_datetime(d), self.format_date(d, "full")))   308             self.menu("%s-date" % name, format_datetime(base), items, index=index)   309    310         # Show time details.   311    312         page.span(class_="time enabled")   313    314         if read_only:   315             page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second()))   316             self.control("%s-hour" % name, "hidden", default.get_hour())   317             self.control("%s-minute" % name, "hidden", default.get_minute())   318             self.control("%s-second" % name, "hidden", default.get_second())   319         else:   320             self.control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2)   321             page.add(":")   322             self.control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2)   323             page.add(":")   324             self.control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2)   325    326         # Show time zone details.   327    328         if show_tzid:   329             page.add(" ")   330             tzid = default.get_tzid() or self.get_tzid()   331    332             # Show a label if read-only or a menu otherwise.   333    334             if read_only:   335                 self.control("%s-tzid" % name, "hidden", tzid)   336                 page.span(tzid)   337             else:   338                 self.timezone_menu("%s-tzid" % name, tzid, index)   339    340         page.span.close()   341    342     def timezone_menu(self, name, default, index=None):   343    344         """   345         Show timezone controls using a menu with the given 'name', set to the   346         given 'default' unless a field of the given 'name' provides a value.   347         """   348    349         entries = [(tzid, tzid) for tzid in pytz.all_timezones]   350         self.menu(name, default, entries, index=index)   351    352 class DateTimeFormUtilities:   353    354     "Date/time control methods resource mix-in."   355    356     # Control naming helpers.   357    358     def element_identifier(self, name, index=None):   359         return index is not None and "%s-%d" % (name, index) or name   360    361     def element_name(self, name, suffix, index=None):   362         return index is not None and "%s-%s" % (name, suffix) or name   363    364     def element_enable(self, index=None):   365         return index is not None and str(index) or "enable"   366    367     def show_object_datetime_controls(self, period, index=None):   368    369         """   370         Show datetime-related controls if already active or if an object needs   371         them for the given 'period'. The given 'index' is used to parameterise   372         individual controls for dynamic manipulation.   373         """   374    375         p = form_period_from_period(period)   376    377         page = self.page   378         args = self.env.get_args()   379         _id = self.element_identifier   380         _name = self.element_name   381         _enable = self.element_enable   382    383         # Add a dynamic stylesheet to permit the controls to modify the display.   384         # NOTE: The style details need to be coordinated with the static   385         # NOTE: stylesheet.   386    387         if index is not None:   388             page.style(type="text/css")   389    390             # Unlike the rules for object properties, these affect recurrence   391             # properties.   392    393             page.add("""\   394 input#dttimes-enable-%(index)d,   395 input#dtend-enable-%(index)d,   396 input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,   397 input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,   398 input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,   399 input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {   400     display: none;   401 }""" % {"index" : index})   402    403             page.style.close()   404    405         self.control(   406             _name("dtend-control", "recur", index), "checkbox",   407             _enable(index), p.end_enabled,   408             id=_id("dtend-enable", index)   409             )   410    411         self.control(   412             _name("dttimes-control", "recur", index), "checkbox",   413             _enable(index), p.times_enabled,   414             id=_id("dttimes-enable", index)   415             )   416    417     def show_datetime_controls(self, formdate, show_start):   418    419         """   420         Show datetime details from the current object for the 'formdate',   421         showing start details if 'show_start' is set to a true value. Details   422         will appear as controls for organisers and labels for attendees.   423         """   424    425         page = self.page   426    427         # Show controls for editing as organiser.   428    429         if self.is_organiser():   430             page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))   431    432             if show_start:   433                 page.div(class_="dt enabled")   434                 self.date_controls("dtstart", formdate)   435                 page.br()   436                 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")   437                 page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")   438                 page.div.close()   439    440             else:   441                 page.div(class_="dt disabled")   442                 page.label("Specify end date", for_="dtend-enable", class_="enable")   443                 page.div.close()   444                 page.div(class_="dt enabled")   445                 self.date_controls("dtend", formdate)   446                 page.br()   447                 page.label("End on same day", for_="dtend-enable", class_="disable")   448                 page.div.close()   449    450             page.td.close()   451    452         # Show a label as attendee.   453    454         else:   455             dt = formdate.as_datetime()   456             if dt:   457                 page.td(self.format_datetime(dt, "full"))   458             else:   459                 page.td("(Unrecognised date)")   460    461     def show_recurrence_controls(self, index, period, recurrenceid, recurrenceids, show_start):   462    463         """   464         Show datetime details from the current object for the recurrence having   465         the given 'index', with the recurrence period described by 'period',   466         indicating a start, end and origin of the period from the event details,   467         employing any 'recurrenceid' and 'recurrenceids' for the object to   468         configure the displayed information.   469    470         If 'show_start' is set to a true value, the start details will be shown;   471         otherwise, the end details will be shown.   472         """   473    474         page = self.page   475         _id = self.element_identifier   476         _name = self.element_name   477    478         p = event_period_from_period(period)   479         replaced = not recurrenceid and p.is_replaced(recurrenceids)   480    481         # Show controls for editing as organiser.   482    483         if self.is_organiser() and not replaced:   484             page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))   485    486             read_only = period.origin == "RRULE"   487    488             if show_start:   489                 page.div(class_="dt enabled")   490                 self.date_controls(_name("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only)   491                 if not read_only:   492                     page.br()   493                     page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable")   494                     page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable")   495                 page.div.close()   496    497                 # Put the origin somewhere.   498    499                 self.control("recur-origin", "hidden", p.origin or "")   500    501             else:   502                 page.div(class_="dt disabled")   503                 if not read_only:   504                     page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable")   505                 page.div.close()   506                 page.div(class_="dt enabled")   507                 self.date_controls(_name("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only)   508                 if not read_only:   509                     page.br()   510                     page.label("End on same day", for_=_id("dtend-enable", index), class_="disable")   511                 page.div.close()   512    513             page.td.close()   514    515         # Show label as attendee.   516    517         else:   518             self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start)   519    520     def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start):   521    522         """   523         Show datetime details for the given 'period', employing any   524         'recurrenceid' and 'recurrenceids' for the object to configure the   525         displayed information.   526    527         If 'show_start' is set to a true value, the start details will be shown;   528         otherwise, the end details will be shown.   529         """   530    531         page = self.page   532    533         p = event_period_from_period(period)   534         replaced = not recurrenceid and p.is_replaced(recurrenceids)   535    536         css = " ".join([   537             replaced and "replaced" or "",   538             p.is_affected(recurrenceid) and "affected" or ""   539             ])   540    541         formdate = show_start and p.get_form_start() or p.get_form_end()   542         dt = formdate.as_datetime()   543         if dt:   544             page.td(self.format_datetime(dt, "long"), class_=css)   545         else:   546             page.td("(Unrecognised date)")   547    548     def get_date_control_values(self, name, multiple=False, tzid_name=None):   549    550         """   551         Return a form date object representing fields starting with 'name'. If   552         'multiple' is set to a true value, many date objects will be returned   553         corresponding to a collection of datetimes.   554    555         If 'tzid_name' is specified, the time zone information will be acquired   556         from fields starting with 'tzid_name' instead of 'name'.   557         """   558    559         args = self.env.get_args()   560    561         dates = args.get("%s-date" % name, [])   562         hours = args.get("%s-hour" % name, [])   563         minutes = args.get("%s-minute" % name, [])   564         seconds = args.get("%s-second" % name, [])   565         tzids = args.get("%s-tzid" % (tzid_name or name), [])   566    567         # Handle absent values by employing None values.   568    569         field_values = map(None, dates, hours, minutes, seconds, tzids)   570    571         if not field_values and not multiple:   572             all_values = FormDate()   573         else:   574             all_values = []   575             for date, hour, minute, second, tzid in field_values:   576                 value = FormDate(date, hour, minute, second, tzid or self.get_tzid())   577    578                 # Return a single value or append to a collection of all values.   579    580                 if not multiple:   581                     return value   582                 else:   583                     all_values.append(value)   584    585         return all_values   586    587     def set_date_control_values(self, name, formdates, tzid_name=None):   588    589         """   590         Replace form fields starting with 'name' using the values of the given   591         'formdates'.   592    593         If 'tzid_name' is specified, the time zone information will be stored in   594         fields starting with 'tzid_name' instead of 'name'.   595         """   596    597         args = self.env.get_args()   598    599         args["%s-date" % name] = [d.date for d in formdates]   600         args["%s-hour" % name] = [d.hour for d in formdates]   601         args["%s-minute" % name] = [d.minute for d in formdates]   602         args["%s-second" % name] = [d.second for d in formdates]   603         args["%s-tzid" % (tzid_name or name)] = [d.tzid for d in formdates]   604    605 # vim: tabstop=4 expandtab shiftwidth=4