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, get_window_end, Object, uri_values 25 from imiptools.dates import format_datetime, format_time, get_recurrence_start, \ 26 get_recurrence_start_point, to_recurrence_start_point 27 from imiptools.period import FreeBusyPeriod, \ 28 remove_period, remove_affected_period, update_freebusy 29 from imipweb.env import CGIEnvironment 30 import babel.dates 31 import imip_store 32 import markup 33 34 class Resource(Client): 35 36 "A Web application resource and calendar client." 37 38 def __init__(self, resource=None): 39 self.encoding = "utf-8" 40 self.env = CGIEnvironment(self.encoding) 41 42 user = self.env.get_user() 43 Client.__init__(self, user and get_uri(user) or None) 44 45 self.locale = None 46 self.requests = None 47 48 self.out = resource and resource.out or self.env.get_output() 49 self.page = resource and resource.page or markup.page() 50 self.html_ids = None 51 52 self.store = imip_store.FileStore() 53 self.objects = {} 54 55 try: 56 self.publisher = imip_store.FilePublisher() 57 except OSError: 58 self.publisher = None 59 60 # Presentation methods. 61 62 def new_page(self, title): 63 self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css")) 64 self.html_ids = set() 65 66 def status(self, code, message): 67 self.header("Status", "%s %s" % (code, message)) 68 69 def header(self, header, value): 70 print >>self.out, "%s: %s" % (header, value) 71 72 def no_user(self): 73 self.status(403, "Forbidden") 74 self.new_page(title="Forbidden") 75 self.page.p("You are not logged in and thus cannot access scheduling requests.") 76 77 def no_page(self): 78 self.status(404, "Not Found") 79 self.new_page(title="Not Found") 80 self.page.p("No page is provided at the given address.") 81 82 def redirect(self, url): 83 self.status(302, "Redirect") 84 self.header("Location", url) 85 self.new_page(title="Redirect") 86 self.page.p("Redirecting to: %s" % url) 87 88 def link_to(self, uid, recurrenceid=None): 89 if recurrenceid: 90 return self.env.new_url("/".join([uid, recurrenceid])) 91 else: 92 return self.env.new_url(uid) 93 94 # Access to objects. 95 96 def _suffixed_name(self, name, index=None): 97 return index is not None and "%s-%d" % (name, index) or name 98 99 def _simple_suffixed_name(self, name, suffix, index=None): 100 return index is not None and "%s-%s" % (name, suffix) or name 101 102 def _get_identifiers(self, path_info): 103 parts = path_info.lstrip("/").split("/") 104 if len(parts) == 1: 105 return parts[0], None 106 else: 107 return parts[:2] 108 109 def _get_object(self, uid, recurrenceid=None): 110 if self.objects.has_key((uid, recurrenceid)): 111 return self.objects[(uid, recurrenceid)] 112 113 fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None 114 obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment) 115 return obj 116 117 def _get_recurrences(self, uid): 118 return self.store.get_recurrences(self.user, uid) 119 120 def _get_requests(self): 121 if self.requests is None: 122 cancellations = self.store.get_cancellations(self.user) 123 requests = set(self.store.get_requests(self.user)) 124 self.requests = requests.difference(cancellations) 125 return self.requests 126 127 def _get_request_summary(self): 128 summary = [] 129 for uid, recurrenceid in self._get_requests(): 130 obj = self._get_object(uid, recurrenceid) 131 if obj: 132 periods = obj.get_periods(self.get_tzid(), self.get_window_end()) 133 recurrenceids = self._get_recurrences(uid) 134 135 # Convert the periods to more substantial free/busy items. 136 137 for p in periods: 138 139 # Subtract any recurrences from the free/busy details of a 140 # parent object. 141 142 if recurrenceid or not self.is_replaced(p, recurrenceids): 143 summary.append( 144 FreeBusyPeriod( 145 p.get_start(), 146 p.get_end(), 147 uid, 148 obj.get_value("TRANSP"), 149 recurrenceid, 150 obj.get_value("SUMMARY"), 151 obj.get_value("ORGANIZER"), 152 p.get_tzid() 153 )) 154 return summary 155 156 # Period and recurrence testing. 157 158 def is_replaced(self, period, recurrenceids): 159 for s in recurrenceids: 160 d = get_recurrence_start(s) 161 dt = get_recurrence_start_point(s, self.get_tzid()) 162 if period.get_start() == d or period.get_start_point() == dt: 163 return s 164 return None 165 166 def is_affected(self, period, recurrenceid): 167 if not recurrenceid: 168 return None 169 d = get_recurrence_start(recurrenceid) 170 dt = get_recurrence_start_point(recurrenceid, self.get_tzid()) 171 if period.get_start() == d or period.get_start_point() == dt: 172 return recurrenceid 173 return None 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 def update_freebusy(self, uid, recurrenceid, obj): 207 208 """ 209 Update stored free/busy details for the event with the given 'uid' and 210 'recurrenceid' having a representation of 'obj'. 211 """ 212 213 is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE")) 214 215 freebusy = self.store.get_freebusy(self.user) 216 217 update_freebusy(freebusy, 218 obj.get_periods(self.get_tzid(), self.get_window_end()), 219 is_only_organiser and "ORG" or obj.get_value("TRANSP"), 220 uid, recurrenceid, 221 obj.get_value("SUMMARY"), 222 obj.get_value("ORGANIZER")) 223 224 # Subtract any recurrences from the free/busy details of a parent 225 # object. 226 # NOTE: The time zone may need obtaining using the object details. 227 228 for recurrenceid in self._get_recurrences(uid): 229 remove_affected_period(freebusy, uid, to_recurrence_start_point(recurrenceid, self.get_tzid())) 230 231 self.store.set_freebusy(self.user, freebusy) 232 self.publish_freebusy(freebusy) 233 234 def remove_from_freebusy(self, uid, recurrenceid=None): 235 freebusy = self.store.get_freebusy(self.user) 236 remove_period(freebusy, uid, recurrenceid) 237 self.store.set_freebusy(self.user, freebusy) 238 self.publish_freebusy(freebusy) 239 240 def publish_freebusy(self, freebusy): 241 242 "Publish the details if configured to share them." 243 244 if self.publisher and self.is_sharing(): 245 self.publisher.set_freebusy(self.user, freebusy) 246 247 # vim: tabstop=4 expandtab shiftwidth=4