imip-agent

imipweb/resource.py

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