1 #!/usr/bin/env python 2 3 """ 4 A simple filesystem-based store of calendar data. 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.config import STORE_DIR, PUBLISH_DIR 24 from imiptools.filesys import fix_permissions, FileBase 25 from os.path import exists, isfile, join 26 from os import listdir 27 from vCalendar import iterwrite 28 29 def make_calendar(fragment, method=None): 30 31 """ 32 Return a complete calendar item wrapping the given 'fragment' and employing 33 the given 'method', if indicated. 34 """ 35 36 return ("VCALENDAR", {}, 37 (method and [("METHOD", {}, method)] or []) + 38 [("VERSION", {}, "2.0")] + 39 fragment 40 ) 41 42 def to_stream(out, fragment, encoding="utf-8"): 43 iterwrite(out, encoding=encoding).append(fragment) 44 45 class FileStore(FileBase): 46 47 "A file store of tabular free/busy data and objects." 48 49 def __init__(self, store_dir=STORE_DIR): 50 FileBase.__init__(self, store_dir) 51 52 def get_events(self, user): 53 54 "Return a list of event identifiers." 55 56 filename = self.get_object_in_store(user, "objects") 57 if not filename or not exists(filename): 58 return None 59 60 return [name for name in listdir(filename) if isfile(join(filename, name))] 61 62 def get_event(self, user, uid): 63 64 "Get the event for the given 'user' with the given 'uid'." 65 66 filename = self.get_object_in_store(user, "objects", uid) 67 if not filename or not exists(filename): 68 return None 69 70 return open(filename) or None 71 72 def set_event(self, user, uid, node): 73 74 "Set an event for 'user' having the given 'uid' and 'node'." 75 76 filename = self.get_object_in_store(user, "objects", uid) 77 if not filename: 78 return False 79 80 f = open(filename, "w") 81 try: 82 to_stream(f, node) 83 finally: 84 f.close() 85 fix_permissions(filename) 86 87 return True 88 89 def get_freebusy(self, user): 90 91 "Get free/busy details for the given 'user'." 92 93 filename = self.get_object_in_store(user, "freebusy") 94 if not filename or not exists(filename): 95 return None 96 else: 97 return self._get_freebusy(filename) 98 99 def get_freebusy_for_other(self, user, other): 100 101 "For the given 'user', get free/busy details for the 'other' user." 102 103 filename = self.get_object_in_store(user, "freebusy-other", other) 104 if not filename: 105 return None 106 else: 107 return self._get_freebusy(filename) 108 109 def _get_freebusy(self, filename): 110 f = open(filename) 111 try: 112 l = [] 113 for line in f.readlines(): 114 l.append(tuple(line.strip().split("\t"))) 115 return l 116 finally: 117 f.close() 118 119 def set_freebusy(self, user, freebusy): 120 121 "For the given 'user', set 'freebusy' details." 122 123 filename = self.get_object_in_store(user, "freebusy") 124 if not filename: 125 return False 126 127 self._set_freebusy(filename, freebusy) 128 return True 129 130 def set_freebusy_for_other(self, user, freebusy, other): 131 132 "For the given 'user', set 'freebusy' details for the 'other' user." 133 134 filename = self.get_object_in_store(user, "freebusy-other", other) 135 if not filename: 136 return False 137 138 self._set_freebusy(filename, freebusy) 139 return True 140 141 def _set_freebusy(self, filename, freebusy): 142 f = open(filename, "w") 143 try: 144 for item in freebusy: 145 f.write("\t".join([(value or "OPAQUE") for value in item]) + "\n") 146 finally: 147 f.close() 148 fix_permissions(filename) 149 150 def _get_requests(self, user, queue): 151 152 "Get requests for the given 'user' from the given 'queue'." 153 154 filename = self.get_object_in_store(user, queue) 155 if not filename or not exists(filename): 156 return None 157 158 f = open(filename) 159 try: 160 return [line.strip() for line in f.readlines()] 161 finally: 162 f.close() 163 164 def get_requests(self, user): 165 166 "Get requests for the given 'user'." 167 168 return self._get_requests(user, "requests") 169 170 def get_cancellations(self, user): 171 172 "Get cancellations for the given 'user'." 173 174 return self._get_requests(user, "cancellations") 175 176 def _set_requests(self, user, requests, queue): 177 178 """ 179 For the given 'user', set the list of queued 'requests' in the given 180 'queue'. 181 """ 182 183 filename = self.get_object_in_store(user, queue) 184 if not filename: 185 return False 186 187 f = open(filename, "w") 188 try: 189 for request in requests: 190 print >>f, request 191 finally: 192 f.close() 193 fix_permissions(filename) 194 195 return True 196 197 def set_requests(self, user, requests): 198 199 "For the given 'user', set the list of queued 'requests'." 200 201 return self._set_requests(user, requests, "requests") 202 203 def set_cancellations(self, user, cancellations): 204 205 "For the given 'user', set the list of queued 'cancellations'." 206 207 return self._set_requests(user, cancellations, "cancellations") 208 209 def _set_request(self, user, request, queue): 210 211 "For the given 'user', set the queued 'request' in the given 'queue'." 212 213 filename = self.get_object_in_store(user, queue) 214 if not filename: 215 return False 216 217 f = open(filename, "a") 218 try: 219 print >>f, request 220 finally: 221 f.close() 222 fix_permissions(filename) 223 224 return True 225 226 def set_request(self, user, request): 227 228 "For the given 'user', set the queued 'request'." 229 230 return self._set_request(user, request, "requests") 231 232 def set_cancellation(self, user, cancellation): 233 234 "For the given 'user', set the queued 'cancellation'." 235 236 return self._set_request(user, cancellation, "cancellations") 237 238 def queue_request(self, user, uid): 239 240 "Queue a request for 'user' having the given 'uid'." 241 242 requests = self.get_requests(user) or [] 243 244 if uid not in requests: 245 return self.set_request(user, uid) 246 247 return False 248 249 def dequeue_request(self, user, uid): 250 251 "Dequeue a request for 'user' having the given 'uid'." 252 253 requests = self.get_requests(user) or [] 254 255 try: 256 requests.remove(uid) 257 self.set_requests(user, requests) 258 except ValueError: 259 return False 260 else: 261 return True 262 263 def cancel_event(self, user, uid): 264 265 "Queue an event for cancellation for 'user' having the given 'uid'." 266 267 cancellations = self.get_cancellations(user) or [] 268 269 if uid not in cancellations: 270 return self.set_cancellation(user, uid) 271 272 return False 273 274 class FilePublisher(FileBase): 275 276 "A publisher of objects." 277 278 def __init__(self, store_dir=PUBLISH_DIR): 279 FileBase.__init__(self, store_dir) 280 281 def set_freebusy(self, user, freebusy): 282 283 "For the given 'user', set 'freebusy' details." 284 285 filename = self.get_object_in_store(user, "freebusy") 286 if not filename: 287 return False 288 289 record = [] 290 rwrite = record.append 291 292 rwrite(("ORGANIZER", {}, user)) 293 rwrite(("UID", {}, user)) 294 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 295 296 for start, end, uid, transp in freebusy: 297 if not transp or transp == "OPAQUE": 298 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) 299 300 f = open(filename, "w") 301 try: 302 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 303 finally: 304 f.close() 305 fix_permissions(filename) 306 307 return True 308 309 # vim: tabstop=4 expandtab shiftwidth=4