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 23 from imiptools.client import Client 24 from imiptools.data import get_uri, uri_values 25 from imiptools.dates import get_recurrence_start_point 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 32 class Resource(Client): 33 34 "A Web application resource and calendar client." 35 36 def __init__(self, resource=None): 37 self.encoding = "utf-8" 38 self.env = CGIEnvironment(self.encoding) 39 40 user = self.env.get_user() 41 Client.__init__(self, user and get_uri(user) or None) 42 43 self.locale = None 44 self.requests = None 45 46 self.out = resource and resource.out or self.env.get_output() 47 self.page = resource and resource.page or markup.page() 48 self.html_ids = None 49 50 self.store = imip_store.FileStore() 51 self.objects = {} 52 53 try: 54 self.publisher = imip_store.FilePublisher() 55 except OSError: 56 self.publisher = None 57 58 # Presentation methods. 59 60 def new_page(self, title): 61 self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css")) 62 self.html_ids = set() 63 64 def status(self, code, message): 65 self.header("Status", "%s %s" % (code, message)) 66 67 def header(self, header, value): 68 print >>self.out, "%s: %s" % (header, value) 69 70 def no_user(self): 71 self.status(403, "Forbidden") 72 self.new_page(title="Forbidden") 73 self.page.p("You are not logged in and thus cannot access scheduling requests.") 74 75 def no_page(self): 76 self.status(404, "Not Found") 77 self.new_page(title="Not Found") 78 self.page.p("No page is provided at the given address.") 79 80 def redirect(self, url): 81 self.status(302, "Redirect") 82 self.header("Location", url) 83 self.new_page(title="Redirect") 84 self.page.p("Redirecting to: %s" % url) 85 86 def link_to(self, uid, recurrenceid=None, section=None): 87 88 """ 89 Return a link to an object with the given 'uid', 'recurrenceid' and 90 'section'. See get_identifiers for the decoding of such links. 91 """ 92 93 path = [uid] 94 if recurrenceid: 95 path.append(recurrenceid) 96 if section: 97 path.append(section) 98 return self.env.new_url("/".join(path)) 99 100 # Control naming helpers. 101 102 def element_identifier(self, name, index=None): 103 return index is not None and "%s-%d" % (name, index) or name 104 105 def element_name(self, name, suffix, index=None): 106 return index is not None and "%s-%s" % (name, suffix) or name 107 108 def element_enable(self, index=None): 109 return index is not None and str(index) or "enable" 110 111 # Access to objects. 112 113 def get_identifiers(self, path_info): 114 115 """ 116 Return identifiers provided by 'path_info', potentially encoded by 117 'link_to'. 118 """ 119 120 parts = path_info.lstrip("/").split("/") 121 122 # UID only. 123 124 if len(parts) == 1: 125 return parts[0], None, None 126 127 # UID and RECURRENCE-ID or UID and section. 128 129 elif len(parts) == 2: 130 if parts[1] == "counter": 131 return parts[0], None, "counters" 132 else: 133 return parts[0], parts[1], parts[2] == "counter" and "counters" or None 134 135 # UID, RECURRENCE-ID and section. 136 137 else: 138 return parts[:3] 139 140 def _get_object(self, uid, recurrenceid=None, section=None): 141 if self.objects.has_key((uid, recurrenceid, section)): 142 return self.objects[(uid, recurrenceid, section)] 143 144 obj = self.objects[(uid, recurrenceid, section)] = self.get_stored_object(uid, recurrenceid, section) 145 return obj 146 147 def _get_recurrences(self, uid): 148 return self.store.get_recurrences(self.user, uid) 149 150 def _get_active_recurrences(self, uid): 151 return self.store.get_active_recurrences(self.user, uid) 152 153 def _get_requests(self): 154 if self.requests is None: 155 self.requests = self.store.get_requests(self.user) 156 return self.requests 157 158 def _have_request(self, uid, recurrenceid=None, type=None, strict=False): 159 return self.store.have_request(self._get_requests(), uid, recurrenceid, type, strict) 160 161 def _get_request_summary(self): 162 163 "Return a list of periods comprising the request summary." 164 165 summary = [] 166 167 for uid, recurrenceid, request_type in self._get_requests(): 168 obj = self.get_stored_object(uid, recurrenceid) 169 if obj: 170 recurrenceids = self._get_active_recurrences(uid) 171 172 # Obtain only active periods, not those replaced by redefined 173 # recurrences, converting to free/busy periods. 174 175 for p in obj.get_active_periods(recurrenceids, self.get_tzid(), self.get_window_end()): 176 summary.append(obj.get_freebusy_period(p)) 177 178 return summary 179 180 # Preference methods. 181 182 def get_user_locale(self): 183 if not self.locale: 184 self.locale = self.get_preferences().get("LANG", "en") 185 return self.locale 186 187 # Prettyprinting of dates and times. 188 189 def format_date(self, dt, format): 190 return self._format_datetime(babel.dates.format_date, dt, format) 191 192 def format_time(self, dt, format): 193 return self._format_datetime(babel.dates.format_time, dt, format) 194 195 def format_datetime(self, dt, format): 196 return self._format_datetime( 197 isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date, 198 dt, format) 199 200 def _format_datetime(self, fn, dt, format): 201 return fn(dt, format=format, locale=self.get_user_locale()) 202 203 # Data management methods. 204 205 def remove_request(self, uid, recurrenceid=None): 206 return self.store.dequeue_request(self.user, uid, recurrenceid) 207 208 def remove_event(self, uid, recurrenceid=None): 209 return self.store.remove_event(self.user, uid, recurrenceid) 210 211 def update_freebusy(self, uid, recurrenceid, obj): 212 213 """ 214 Update stored free/busy details for the event with the given 'uid' and 215 'recurrenceid' having a representation of 'obj'. 216 """ 217 218 is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE")) 219 220 freebusy = self.store.get_freebusy(self.user) 221 222 Client.update_freebusy(self, freebusy, self.get_periods(obj), 223 is_only_organiser and "ORG" or obj.get_value("TRANSP"), 224 uid, recurrenceid, 225 obj.get_value("SUMMARY"), 226 obj.get_value("ORGANIZER")) 227 228 # Subtract any recurrences from the free/busy details of a parent 229 # object. 230 231 for recurrenceid in self._get_recurrences(uid): 232 remove_affected_period(freebusy, uid, obj.get_recurrence_start_point(recurrenceid, self.get_tzid())) 233 234 self.store.set_freebusy(self.user, freebusy) 235 self.publish_freebusy(freebusy) 236 237 # Update free/busy provider information if the event may recur 238 # indefinitely. 239 240 if obj.possibly_recurring_indefinitely(): 241 self.store.append_freebusy_provider(self.user, obj) 242 243 def remove_from_freebusy(self, uid, recurrenceid=None): 244 freebusy = self.store.get_freebusy(self.user) 245 remove_period(freebusy, uid, recurrenceid) 246 self.store.set_freebusy(self.user, freebusy) 247 self.publish_freebusy(freebusy) 248 249 # Update free/busy provider information if the event may recur 250 # indefinitely. 251 252 if obj.possibly_recurring_indefinitely(): 253 self.store.remove_freebusy_provider(self.user, obj) 254 255 def publish_freebusy(self, freebusy): 256 257 "Publish the details if configured to share them." 258 259 if self.publisher and self.is_sharing() and self.is_publishing(): 260 self.publisher.set_freebusy(self.user, freebusy) 261 262 # vim: tabstop=4 expandtab shiftwidth=4