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, to_recurrence_start 26 from imiptools.period import FreeBusyPeriod, \ 27 remove_period, remove_affected_period, update_freebusy 28 from imipweb.env import CGIEnvironment 29 import babel.dates 30 import imip_store 31 import markup 32 33 class Resource(Client): 34 35 "A Web application resource and calendar client." 36 37 def __init__(self, resource=None): 38 self.encoding = "utf-8" 39 self.env = CGIEnvironment(self.encoding) 40 41 user = self.env.get_user() 42 Client.__init__(self, user and get_uri(user) or None) 43 44 self.locale = None 45 self.requests = None 46 47 self.out = resource and resource.out or self.env.get_output() 48 self.page = resource and resource.page or markup.page() 49 self.html_ids = None 50 51 self.store = imip_store.FileStore() 52 self.objects = {} 53 54 try: 55 self.publisher = imip_store.FilePublisher() 56 except OSError: 57 self.publisher = None 58 59 # Presentation methods. 60 61 def new_page(self, title): 62 self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css")) 63 self.html_ids = set() 64 65 def status(self, code, message): 66 self.header("Status", "%s %s" % (code, message)) 67 68 def header(self, header, value): 69 print >>self.out, "%s: %s" % (header, value) 70 71 def no_user(self): 72 self.status(403, "Forbidden") 73 self.new_page(title="Forbidden") 74 self.page.p("You are not logged in and thus cannot access scheduling requests.") 75 76 def no_page(self): 77 self.status(404, "Not Found") 78 self.new_page(title="Not Found") 79 self.page.p("No page is provided at the given address.") 80 81 def redirect(self, url): 82 self.status(302, "Redirect") 83 self.header("Location", url) 84 self.new_page(title="Redirect") 85 self.page.p("Redirecting to: %s" % url) 86 87 def link_to(self, uid, recurrenceid=None): 88 if recurrenceid: 89 return self.env.new_url("/".join([uid, recurrenceid])) 90 else: 91 return self.env.new_url(uid) 92 93 # Access to objects. 94 95 def _suffixed_name(self, name, index=None): 96 return index is not None and "%s-%d" % (name, index) or name 97 98 def _simple_suffixed_name(self, name, suffix, index=None): 99 return index is not None and "%s-%s" % (name, suffix) or name 100 101 def _get_identifiers(self, path_info): 102 parts = path_info.lstrip("/").split("/") 103 if len(parts) == 1: 104 return parts[0], None 105 else: 106 return parts[:2] 107 108 def _get_object(self, uid, recurrenceid=None): 109 if self.objects.has_key((uid, recurrenceid)): 110 return self.objects[(uid, recurrenceid)] 111 112 fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None 113 obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment) 114 return obj 115 116 def _get_recurrences(self, uid): 117 return self.store.get_recurrences(self.user, uid) 118 119 def _get_requests(self): 120 if self.requests is None: 121 cancellations = self.store.get_cancellations(self.user) 122 requests = set(self.store.get_requests(self.user)) 123 self.requests = requests.difference(cancellations) 124 return self.requests 125 126 def _get_request_summary(self): 127 summary = [] 128 for uid, recurrenceid in self._get_requests(): 129 obj = self._get_object(uid, recurrenceid) 130 if obj: 131 periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()) 132 recurrenceids = self._get_recurrences(uid) 133 134 # Convert the periods to more substantial free/busy items. 135 136 for p in periods: 137 138 # Subtract any recurrences from the free/busy details of a 139 # parent object. 140 141 if recurrenceid or p.start not in recurrenceids: 142 summary.append( 143 FreeBusyPeriod( 144 p.start, p.end, uid, 145 obj.get_value("TRANSP"), 146 recurrenceid, 147 obj.get_value("SUMMARY"), 148 obj.get_value("ORGANIZER") 149 )) 150 return summary 151 152 # Preference methods. 153 154 def get_user_locale(self): 155 if not self.locale: 156 self.locale = self.get_preferences().get("LANG", "en") 157 return self.locale 158 159 # Prettyprinting of dates and times. 160 161 def format_date(self, dt, format): 162 return self._format_datetime(babel.dates.format_date, dt, format) 163 164 def format_time(self, dt, format): 165 return self._format_datetime(babel.dates.format_time, dt, format) 166 167 def format_datetime(self, dt, format): 168 return self._format_datetime( 169 isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date, 170 dt, format) 171 172 def _format_datetime(self, fn, dt, format): 173 return fn(dt, format=format, locale=self.get_user_locale()) 174 175 # Data management methods. 176 177 def remove_request(self, uid, recurrenceid=None): 178 return self.store.dequeue_request(self.user, uid, recurrenceid) 179 180 def remove_event(self, uid, recurrenceid=None): 181 return self.store.remove_event(self.user, uid, recurrenceid) 182 183 def update_freebusy(self, uid, recurrenceid, obj): 184 185 """ 186 Update stored free/busy details for the event with the given 'uid' and 187 'recurrenceid' having a representation of 'obj'. 188 """ 189 190 is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE")) 191 192 freebusy = self.store.get_freebusy(self.user) 193 194 update_freebusy(freebusy, 195 obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()), 196 is_only_organiser and "ORG" or obj.get_value("TRANSP"), 197 uid, recurrenceid, 198 obj.get_value("SUMMARY"), 199 obj.get_value("ORGANIZER")) 200 201 # Subtract any recurrences from the free/busy details of a parent 202 # object. 203 204 for recurrenceid in self._get_recurrences(uid): 205 remove_affected_period(freebusy, uid, to_recurrence_start(recurrenceid)) 206 207 self.store.set_freebusy(self.user, freebusy) 208 self.publish_freebusy(freebusy) 209 210 def remove_from_freebusy(self, uid, recurrenceid=None): 211 freebusy = self.store.get_freebusy(self.user) 212 remove_period(freebusy, uid, recurrenceid) 213 self.store.set_freebusy(self.user, freebusy) 214 self.publish_freebusy(freebusy) 215 216 def publish_freebusy(self, freebusy): 217 218 "Publish the details if configured to share them." 219 220 if self.publisher and self.is_sharing(): 221 self.publisher.set_freebusy(self.user, freebusy) 222 223 # vim: tabstop=4 expandtab shiftwidth=4