imip-agent

imipweb/resource.py

764:a92517218628
2015-09-24 Paul Boddie Introduced a mix-in class for form control generation. 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.env import CGIEnvironment    28 import babel.dates    29 import imip_store    30 import markup    31 import pytz    32     33 class Resource:    34     35     "A Web application resource."    36     37     def __init__(self, resource=None):    38     39         """    40         Initialise a resource, allowing it to share the environment of any given    41         existing 'resource'.    42         """    43     44         self.encoding = "utf-8"    45         self.env = CGIEnvironment(self.encoding)    46     47         self.objects = {}    48         self.locale = None    49         self.requests = None    50     51         self.out = resource and resource.out or self.env.get_output()    52         self.page = resource and resource.page or markup.page()    53         self.html_ids = None    54     55     # Presentation methods.    56     57     def new_page(self, title):    58         self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))    59         self.html_ids = set()    60     61     def status(self, code, message):    62         self.header("Status", "%s %s" % (code, message))    63     64     def header(self, header, value):    65         print >>self.out, "%s: %s" % (header, value)    66     67     def no_user(self):    68         self.status(403, "Forbidden")    69         self.new_page(title="Forbidden")    70         self.page.p("You are not logged in and thus cannot access scheduling requests.")    71     72     def no_page(self):    73         self.status(404, "Not Found")    74         self.new_page(title="Not Found")    75         self.page.p("No page is provided at the given address.")    76     77     def redirect(self, url):    78         self.status(302, "Redirect")    79         self.header("Location", url)    80         self.new_page(title="Redirect")    81         self.page.p("Redirecting to: %s" % url)    82     83     def link_to(self, uid, recurrenceid=None):    84     85         """    86         Return a link to an object with the given 'uid' and 'recurrenceid'.    87         See get_identifiers for the decoding of such links.    88         """    89     90         path = [uid]    91         if recurrenceid:    92             path.append(recurrenceid)    93         return self.env.new_url("/".join(path))    94     95     # Control naming helpers.    96     97     def element_identifier(self, name, index=None):    98         return index is not None and "%s-%d" % (name, index) or name    99    100     def element_name(self, name, suffix, index=None):   101         return index is not None and "%s-%s" % (name, suffix) or name   102    103     def element_enable(self, index=None):   104         return index is not None and str(index) or "enable"   105    106     # Access to objects.   107    108     def get_identifiers(self, path_info):   109    110         """   111         Return identifiers provided by 'path_info', potentially encoded by   112         'link_to'.   113         """   114    115         parts = path_info.lstrip("/").split("/")   116    117         # UID only.   118    119         if len(parts) == 1:   120             return parts[0], None   121    122         # UID and RECURRENCE-ID.   123    124         else:   125             return parts[:2]   126    127     def _get_object(self, uid, recurrenceid=None, section=None):   128         if self.objects.has_key((uid, recurrenceid, section)):   129             return self.objects[(uid, recurrenceid, section)]   130    131         obj = self.objects[(uid, recurrenceid, section)] = self.get_stored_object(uid, recurrenceid, section)   132         return obj   133    134     def _get_recurrences(self, uid):   135         return self.store.get_recurrences(self.user, uid)   136    137     def _get_active_recurrences(self, uid):   138         return self.store.get_active_recurrences(self.user, uid)   139    140     def _get_requests(self):   141         if self.requests is None:   142             self.requests = self.store.get_requests(self.user)   143         return self.requests   144    145     def _have_request(self, uid, recurrenceid=None, type=None, strict=False):   146         return self.store.have_request(self._get_requests(), uid, recurrenceid, type, strict)   147    148     def _get_request_summary(self):   149    150         "Return a list of periods comprising the request summary."   151    152         summary = []   153    154         for uid, recurrenceid, request_type in self._get_requests():   155             obj = self.get_stored_object(uid, recurrenceid)   156             if obj:   157                 recurrenceids = self._get_active_recurrences(uid)   158    159                 # Obtain only active periods, not those replaced by redefined   160                 # recurrences, converting to free/busy periods.   161    162                 for p in obj.get_active_periods(recurrenceids, self.get_tzid(), self.get_window_end()):   163                     summary.append(obj.get_freebusy_period(p))   164    165         return summary   166    167     # Preference methods.   168    169     def get_user_locale(self):   170         if not self.locale:   171             self.locale = self.get_preferences().get("LANG", "en")   172         return self.locale   173    174     # Prettyprinting of dates and times.   175    176     def format_date(self, dt, format):   177         return self._format_datetime(babel.dates.format_date, dt, format)   178    179     def format_time(self, dt, format):   180         return self._format_datetime(babel.dates.format_time, dt, format)   181    182     def format_datetime(self, dt, format):   183         return self._format_datetime(   184             isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,   185             dt, format)   186    187     def _format_datetime(self, fn, dt, format):   188         return fn(dt, format=format, locale=self.get_user_locale())   189    190     # Data management methods.   191    192     def remove_request(self, uid, recurrenceid=None):   193         return self.store.dequeue_request(self.user, uid, recurrenceid)   194    195     def remove_event(self, uid, recurrenceid=None):   196         return self.store.remove_event(self.user, uid, recurrenceid)   197    198 class ResourceClient(Resource, Client):   199    200     "A Web application resource and calendar client."   201    202     def __init__(self, resource=None):   203         Resource.__init__(self, resource)   204         user = self.env.get_user()   205         Client.__init__(self, user and get_uri(user) or None)   206    207 class ResourceClientForObject(Resource, ClientForObject):   208    209     "A Web application resource and calendar client for a specific object."   210    211     def __init__(self, resource=None):   212         Resource.__init__(self, resource)   213         user = self.env.get_user()   214         ClientForObject.__init__(self, None, user and get_uri(user) or None)   215    216 class FormUtilities:   217    218     "Utility methods."   219    220     def control(self, name, type, value, selected=False, **kw):   221    222         """   223         Show a control with the given 'name', 'type' and 'value', with   224         'selected' indicating whether it should be selected (checked or   225         equivalent), and with keyword arguments setting other properties.   226         """   227    228         page = self.page   229         if selected:   230             page.input(name=name, type=type, value=value, checked=selected, **kw)   231         else:   232             page.input(name=name, type=type, value=value, **kw)   233    234     def menu(self, name, default, items, class_="", index=None):   235    236         """   237         Show a select menu having the given 'name', set to the given 'default',   238         providing the given (value, label) 'items', and employing the given CSS   239         'class_' if specified.   240         """   241    242         page = self.page   243         values = self.env.get_args().get(name, [default])   244         if index is not None:   245             values = values[index:]   246             values = values and values[0:1] or [default]   247    248         page.select(name=name, class_=class_)   249         for v, label in items:   250             if v is None:   251                 continue   252             if v in values:   253                 page.option(label, value=v, selected="selected")   254             else:   255                 page.option(label, value=v)   256         page.select.close()   257    258     def date_controls(self, name, default, index=None, show_tzid=True, read_only=False):   259    260         """   261         Show date controls for a field with the given 'name' and 'default' form   262         date value.   263    264         If 'index' is specified, default field values will be overridden by the   265         element from a collection of existing form values with the specified   266         index; otherwise, field values will be overridden by a single form   267         value.   268    269         If 'show_tzid' is set to a false value, the time zone menu will not be   270         provided.   271    272         If 'read_only' is set to a true value, the controls will be hidden and   273         labels will be employed instead.   274         """   275    276         page = self.page   277    278         # Show dates for up to one week around the current date.   279    280         dt = default.as_datetime()   281         if not dt:   282             dt = date.today()   283    284         base = to_date(dt)   285    286         # Show a date label with a hidden field if read-only.   287    288         if read_only:   289             self.control("%s-date" % name, "hidden", format_datetime(base))   290             page.span(self.format_date(base, "long"))   291    292         # Show dates for up to one week around the current date.   293         # NOTE: Support paging to other dates.   294    295         else:   296             items = []   297             for i in range(-7, 8):   298                 d = base + timedelta(i)   299                 items.append((format_datetime(d), self.format_date(d, "full")))   300             self.menu("%s-date" % name, format_datetime(base), items, index=index)   301    302         # Show time details.   303    304         page.span(class_="time enabled")   305    306         if read_only:   307             page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second()))   308             self.control("%s-hour" % name, "hidden", default.get_hour())   309             self.control("%s-minute" % name, "hidden", default.get_minute())   310             self.control("%s-second" % name, "hidden", default.get_second())   311         else:   312             self.control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2)   313             page.add(":")   314             self.control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2)   315             page.add(":")   316             self.control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2)   317    318         # Show time zone details.   319    320         if show_tzid:   321             page.add(" ")   322             tzid = default.get_tzid() or self.get_tzid()   323    324             # Show a label if read-only or a menu otherwise.   325    326             if read_only:   327                 self.control("%s-tzid" % name, "hidden", tzid)   328                 page.span(tzid)   329             else:   330                 self.timezone_menu("%s-tzid" % name, tzid, index)   331    332         page.span.close()   333    334     def timezone_menu(self, name, default, index=None):   335    336         """   337         Show timezone controls using a menu with the given 'name', set to the   338         given 'default' unless a field of the given 'name' provides a value.   339         """   340    341         entries = [(tzid, tzid) for tzid in pytz.all_timezones]   342         self.menu(name, default, entries, index=index)   343    344 # vim: tabstop=4 expandtab shiftwidth=4