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