1.1 --- a/imip_store.py Sat Mar 05 23:45:47 2016 +0100
1.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
1.3 @@ -1,1075 +0,0 @@
1.4 -#!/usr/bin/env python
1.5 -
1.6 -"""
1.7 -A simple filesystem-based store of calendar data.
1.8 -
1.9 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
1.10 -
1.11 -This program is free software; you can redistribute it and/or modify it under
1.12 -the terms of the GNU General Public License as published by the Free Software
1.13 -Foundation; either version 3 of the License, or (at your option) any later
1.14 -version.
1.15 -
1.16 -This program is distributed in the hope that it will be useful, but WITHOUT
1.17 -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1.18 -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1.19 -details.
1.20 -
1.21 -You should have received a copy of the GNU General Public License along with
1.22 -this program. If not, see <http://www.gnu.org/licenses/>.
1.23 -"""
1.24 -
1.25 -from datetime import datetime
1.26 -from imiptools.config import STORE_DIR, PUBLISH_DIR, JOURNAL_DIR
1.27 -from imiptools.data import make_calendar, parse_object, to_stream
1.28 -from imiptools.dates import format_datetime, get_datetime, to_timezone
1.29 -from imiptools.filesys import fix_permissions, FileBase
1.30 -from imiptools.period import FreeBusyPeriod, FreeBusyCollection
1.31 -from imiptools.text import parse_line
1.32 -from os.path import isdir, isfile, join
1.33 -from os import listdir, remove, rmdir
1.34 -from time import sleep
1.35 -import codecs
1.36 -
1.37 -class FileStoreBase(FileBase):
1.38 -
1.39 - "A file store supporting user-specific locking and tabular data."
1.40 -
1.41 - def acquire_lock(self, user, timeout=None):
1.42 - FileBase.acquire_lock(self, timeout, user)
1.43 -
1.44 - def release_lock(self, user):
1.45 - FileBase.release_lock(self, user)
1.46 -
1.47 - # Utility methods.
1.48 -
1.49 - def _set_defaults(self, t, empty_defaults):
1.50 - for i, default in empty_defaults:
1.51 - if i >= len(t):
1.52 - t += [None] * (i - len(t) + 1)
1.53 - if not t[i]:
1.54 - t[i] = default
1.55 - return t
1.56 -
1.57 - def _get_table(self, user, filename, empty_defaults=None, tab_separated=True):
1.58 -
1.59 - """
1.60 - From the file for the given 'user' having the given 'filename', return
1.61 - a list of tuples representing the file's contents.
1.62 -
1.63 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.64 - default value where a column either does not exist or provides an empty
1.65 - value.
1.66 -
1.67 - If 'tab_separated' is specified and is a false value, line parsing using
1.68 - the imiptools.text.parse_line function will be performed instead of
1.69 - splitting each line of the file using tab characters as separators.
1.70 - """
1.71 -
1.72 - f = codecs.open(filename, "rb", encoding="utf-8")
1.73 - try:
1.74 - l = []
1.75 - for line in f.readlines():
1.76 - line = line.strip(" \r\n")
1.77 - if tab_separated:
1.78 - t = line.split("\t")
1.79 - else:
1.80 - t = parse_line(line)
1.81 - if empty_defaults:
1.82 - t = self._set_defaults(t, empty_defaults)
1.83 - l.append(tuple(t))
1.84 - return l
1.85 - finally:
1.86 - f.close()
1.87 -
1.88 - def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True):
1.89 -
1.90 - """
1.91 - From the file for the given 'user' having the given 'filename', return
1.92 - a list of tuples representing the file's contents.
1.93 -
1.94 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.95 - default value where a column either does not exist or provides an empty
1.96 - value.
1.97 -
1.98 - If 'tab_separated' is specified and is a false value, line parsing using
1.99 - the imiptools.text.parse_line function will be performed instead of
1.100 - splitting each line of the file using tab characters as separators.
1.101 - """
1.102 -
1.103 - self.acquire_lock(user)
1.104 - try:
1.105 - return self._get_table(user, filename, empty_defaults, tab_separated)
1.106 - finally:
1.107 - self.release_lock(user)
1.108 -
1.109 - def _set_table(self, user, filename, items, empty_defaults=None):
1.110 -
1.111 - """
1.112 - For the given 'user', write to the file having the given 'filename' the
1.113 - 'items'.
1.114 -
1.115 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.116 - default value where a column either does not exist or provides an empty
1.117 - value.
1.118 - """
1.119 -
1.120 - f = codecs.open(filename, "wb", encoding="utf-8")
1.121 - try:
1.122 - for item in items:
1.123 - self._set_table_item(f, item, empty_defaults)
1.124 - finally:
1.125 - f.close()
1.126 - fix_permissions(filename)
1.127 -
1.128 - def _set_table_item(self, f, item, empty_defaults=None):
1.129 -
1.130 - "Set in table 'f' the given 'item', using any 'empty_defaults'."
1.131 -
1.132 - if empty_defaults:
1.133 - item = self._set_defaults(list(item), empty_defaults)
1.134 - f.write("\t".join(item) + "\n")
1.135 -
1.136 - def _set_table_atomic(self, user, filename, items, empty_defaults=None):
1.137 -
1.138 - """
1.139 - For the given 'user', write to the file having the given 'filename' the
1.140 - 'items'.
1.141 -
1.142 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.143 - default value where a column either does not exist or provides an empty
1.144 - value.
1.145 - """
1.146 -
1.147 - self.acquire_lock(user)
1.148 - try:
1.149 - self._set_table(user, filename, items, empty_defaults)
1.150 - finally:
1.151 - self.release_lock(user)
1.152 -
1.153 -class FileStore(FileStoreBase):
1.154 -
1.155 - "A file store of tabular free/busy data and objects."
1.156 -
1.157 - def __init__(self, store_dir=None):
1.158 - FileBase.__init__(self, store_dir or STORE_DIR)
1.159 -
1.160 - # Store object access.
1.161 -
1.162 - def _get_object(self, user, filename):
1.163 -
1.164 - """
1.165 - Return the parsed object for the given 'user' having the given
1.166 - 'filename'.
1.167 - """
1.168 -
1.169 - self.acquire_lock(user)
1.170 - try:
1.171 - f = open(filename, "rb")
1.172 - try:
1.173 - return parse_object(f, "utf-8")
1.174 - finally:
1.175 - f.close()
1.176 - finally:
1.177 - self.release_lock(user)
1.178 -
1.179 - def _set_object(self, user, filename, node):
1.180 -
1.181 - """
1.182 - Set an object for the given 'user' having the given 'filename', using
1.183 - 'node' to define the object.
1.184 - """
1.185 -
1.186 - self.acquire_lock(user)
1.187 - try:
1.188 - f = open(filename, "wb")
1.189 - try:
1.190 - to_stream(f, node)
1.191 - finally:
1.192 - f.close()
1.193 - fix_permissions(filename)
1.194 - finally:
1.195 - self.release_lock(user)
1.196 -
1.197 - return True
1.198 -
1.199 - def _remove_object(self, filename):
1.200 -
1.201 - "Remove the object with the given 'filename'."
1.202 -
1.203 - try:
1.204 - remove(filename)
1.205 - except OSError:
1.206 - return False
1.207 -
1.208 - return True
1.209 -
1.210 - def _remove_collection(self, filename):
1.211 -
1.212 - "Remove the collection with the given 'filename'."
1.213 -
1.214 - try:
1.215 - rmdir(filename)
1.216 - except OSError:
1.217 - return False
1.218 -
1.219 - return True
1.220 -
1.221 - # User discovery.
1.222 -
1.223 - def get_users(self):
1.224 -
1.225 - "Return a list of users."
1.226 -
1.227 - return listdir(self.store_dir)
1.228 -
1.229 - # Event and event metadata access.
1.230 -
1.231 - def get_events(self, user):
1.232 -
1.233 - "Return a list of event identifiers."
1.234 -
1.235 - filename = self.get_object_in_store(user, "objects")
1.236 - if not filename or not isdir(filename):
1.237 - return None
1.238 -
1.239 - return [name for name in listdir(filename) if isfile(join(filename, name))]
1.240 -
1.241 - def get_all_events(self, user):
1.242 -
1.243 - "Return a set of (uid, recurrenceid) tuples for all events."
1.244 -
1.245 - uids = self.get_events(user)
1.246 - if not uids:
1.247 - return set()
1.248 -
1.249 - all_events = set()
1.250 - for uid in uids:
1.251 - all_events.add((uid, None))
1.252 - all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
1.253 -
1.254 - return all_events
1.255 -
1.256 - def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
1.257 -
1.258 - """
1.259 - Get the filename providing the event for the given 'user' with the given
1.260 - 'uid'. If the optional 'recurrenceid' is specified, a specific instance
1.261 - or occurrence of an event is returned.
1.262 -
1.263 - Where 'dirname' is specified, the given directory name is used as the
1.264 - base of the location within which any filename will reside.
1.265 - """
1.266 -
1.267 - if recurrenceid:
1.268 - return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
1.269 - else:
1.270 - return self.get_complete_event_filename(user, uid, dirname, username)
1.271 -
1.272 - def get_event(self, user, uid, recurrenceid=None, dirname=None):
1.273 -
1.274 - """
1.275 - Get the event for the given 'user' with the given 'uid'. If
1.276 - the optional 'recurrenceid' is specified, a specific instance or
1.277 - occurrence of an event is returned.
1.278 - """
1.279 -
1.280 - filename = self.get_event_filename(user, uid, recurrenceid, dirname)
1.281 - if not filename or not isfile(filename):
1.282 - return None
1.283 -
1.284 - return filename and self._get_object(user, filename)
1.285 -
1.286 - def get_complete_event_filename(self, user, uid, dirname=None, username=None):
1.287 -
1.288 - """
1.289 - Get the filename providing the event for the given 'user' with the given
1.290 - 'uid'.
1.291 -
1.292 - Where 'dirname' is specified, the given directory name is used as the
1.293 - base of the location within which any filename will reside.
1.294 -
1.295 - Where 'username' is specified, the event details will reside in a file
1.296 - bearing that name within a directory having 'uid' as its name.
1.297 - """
1.298 -
1.299 - return self.get_object_in_store(user, dirname, "objects", uid, username)
1.300 -
1.301 - def get_complete_event(self, user, uid):
1.302 -
1.303 - "Get the event for the given 'user' with the given 'uid'."
1.304 -
1.305 - filename = self.get_complete_event_filename(user, uid)
1.306 - if not filename or not isfile(filename):
1.307 - return None
1.308 -
1.309 - return filename and self._get_object(user, filename)
1.310 -
1.311 - def set_event(self, user, uid, recurrenceid, node):
1.312 -
1.313 - """
1.314 - Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
1.315 - if the latter is specified, a specific instance or occurrence of an
1.316 - event is referenced), using the given 'node' description.
1.317 - """
1.318 -
1.319 - if recurrenceid:
1.320 - return self.set_recurrence(user, uid, recurrenceid, node)
1.321 - else:
1.322 - return self.set_complete_event(user, uid, node)
1.323 -
1.324 - def set_complete_event(self, user, uid, node):
1.325 -
1.326 - "Set an event for 'user' having the given 'uid' and 'node'."
1.327 -
1.328 - filename = self.get_object_in_store(user, "objects", uid)
1.329 - if not filename:
1.330 - return False
1.331 -
1.332 - return self._set_object(user, filename, node)
1.333 -
1.334 - def remove_event(self, user, uid, recurrenceid=None):
1.335 -
1.336 - """
1.337 - Remove an event for 'user' having the given 'uid'. If the optional
1.338 - 'recurrenceid' is specified, a specific instance or occurrence of an
1.339 - event is removed.
1.340 - """
1.341 -
1.342 - if recurrenceid:
1.343 - return self.remove_recurrence(user, uid, recurrenceid)
1.344 - else:
1.345 - for recurrenceid in self.get_recurrences(user, uid) or []:
1.346 - self.remove_recurrence(user, uid, recurrenceid)
1.347 - return self.remove_complete_event(user, uid)
1.348 -
1.349 - def remove_complete_event(self, user, uid):
1.350 -
1.351 - "Remove an event for 'user' having the given 'uid'."
1.352 -
1.353 - self.remove_recurrences(user, uid)
1.354 -
1.355 - filename = self.get_object_in_store(user, "objects", uid)
1.356 - if not filename:
1.357 - return False
1.358 -
1.359 - return self._remove_object(filename)
1.360 -
1.361 - def get_recurrences(self, user, uid):
1.362 -
1.363 - """
1.364 - Get additional event instances for an event of the given 'user' with the
1.365 - indicated 'uid'. Both active and cancelled recurrences are returned.
1.366 - """
1.367 -
1.368 - return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
1.369 -
1.370 - def get_active_recurrences(self, user, uid):
1.371 -
1.372 - """
1.373 - Get additional event instances for an event of the given 'user' with the
1.374 - indicated 'uid'. Cancelled recurrences are not returned.
1.375 - """
1.376 -
1.377 - filename = self.get_object_in_store(user, "recurrences", uid)
1.378 - if not filename or not isdir(filename):
1.379 - return []
1.380 -
1.381 - return [name for name in listdir(filename) if isfile(join(filename, name))]
1.382 -
1.383 - def get_cancelled_recurrences(self, user, uid):
1.384 -
1.385 - """
1.386 - Get additional event instances for an event of the given 'user' with the
1.387 - indicated 'uid'. Only cancelled recurrences are returned.
1.388 - """
1.389 -
1.390 - filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
1.391 - if not filename or not isdir(filename):
1.392 - return []
1.393 -
1.394 - return [name for name in listdir(filename) if isfile(join(filename, name))]
1.395 -
1.396 - def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
1.397 -
1.398 - """
1.399 - For the event of the given 'user' with the given 'uid', return the
1.400 - filename providing the recurrence with the given 'recurrenceid'.
1.401 -
1.402 - Where 'dirname' is specified, the given directory name is used as the
1.403 - base of the location within which any filename will reside.
1.404 -
1.405 - Where 'username' is specified, the event details will reside in a file
1.406 - bearing that name within a directory having 'uid' as its name.
1.407 - """
1.408 -
1.409 - return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
1.410 -
1.411 - def get_recurrence(self, user, uid, recurrenceid):
1.412 -
1.413 - """
1.414 - For the event of the given 'user' with the given 'uid', return the
1.415 - specific recurrence indicated by the 'recurrenceid'.
1.416 - """
1.417 -
1.418 - filename = self.get_recurrence_filename(user, uid, recurrenceid)
1.419 - if not filename or not isfile(filename):
1.420 - return None
1.421 -
1.422 - return filename and self._get_object(user, filename)
1.423 -
1.424 - def set_recurrence(self, user, uid, recurrenceid, node):
1.425 -
1.426 - "Set an event for 'user' having the given 'uid' and 'node'."
1.427 -
1.428 - filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
1.429 - if not filename:
1.430 - return False
1.431 -
1.432 - return self._set_object(user, filename, node)
1.433 -
1.434 - def remove_recurrence(self, user, uid, recurrenceid):
1.435 -
1.436 - """
1.437 - Remove a special recurrence from an event stored by 'user' having the
1.438 - given 'uid' and 'recurrenceid'.
1.439 - """
1.440 -
1.441 - filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
1.442 - if not filename:
1.443 - return False
1.444 -
1.445 - return self._remove_object(filename)
1.446 -
1.447 - def remove_recurrences(self, user, uid):
1.448 -
1.449 - """
1.450 - Remove all recurrences for an event stored by 'user' having the given
1.451 - 'uid'.
1.452 - """
1.453 -
1.454 - for recurrenceid in self.get_recurrences(user, uid):
1.455 - self.remove_recurrence(user, uid, recurrenceid)
1.456 -
1.457 - recurrences = self.get_object_in_store(user, "recurrences", uid)
1.458 - if recurrences:
1.459 - return self._remove_collection(recurrences)
1.460 -
1.461 - return True
1.462 -
1.463 - # Free/busy period providers, upon extension of the free/busy records.
1.464 -
1.465 - def _get_freebusy_providers(self, user):
1.466 -
1.467 - """
1.468 - Return the free/busy providers for the given 'user'.
1.469 -
1.470 - This function returns any stored datetime and a list of providers as a
1.471 - 2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
1.472 - """
1.473 -
1.474 - filename = self.get_object_in_store(user, "freebusy-providers")
1.475 - if not filename or not isfile(filename):
1.476 - return None
1.477 -
1.478 - # Attempt to read providers, with a declaration of the datetime
1.479 - # from which such providers are considered as still being active.
1.480 -
1.481 - t = self._get_table_atomic(user, filename, [(1, None)])
1.482 - try:
1.483 - dt_string = t[0][0]
1.484 - except IndexError:
1.485 - return None
1.486 -
1.487 - return dt_string, t[1:]
1.488 -
1.489 - def get_freebusy_providers(self, user, dt=None):
1.490 -
1.491 - """
1.492 - Return a set of uncancelled events of the form (uid, recurrenceid)
1.493 - providing free/busy details beyond the given datetime 'dt'.
1.494 -
1.495 - If 'dt' is not specified, all events previously found to provide
1.496 - details will be returned. Otherwise, if 'dt' is earlier than the
1.497 - datetime recorded for the known providers, None is returned, indicating
1.498 - that the list of providers must be recomputed.
1.499 -
1.500 - This function returns a list of (uid, recurrenceid) tuples upon success.
1.501 - """
1.502 -
1.503 - t = self._get_freebusy_providers(user)
1.504 - if not t:
1.505 - return None
1.506 -
1.507 - dt_string, t = t
1.508 -
1.509 - # If the requested datetime is earlier than the stated datetime, the
1.510 - # providers will need to be recomputed.
1.511 -
1.512 - if dt:
1.513 - providers_dt = get_datetime(dt_string)
1.514 - if not providers_dt or providers_dt > dt:
1.515 - return None
1.516 -
1.517 - # Otherwise, return the providers.
1.518 -
1.519 - return t[1:]
1.520 -
1.521 - def _set_freebusy_providers(self, user, dt_string, t):
1.522 -
1.523 - "Set the given provider timestamp 'dt_string' and table 't'."
1.524 -
1.525 - filename = self.get_object_in_store(user, "freebusy-providers")
1.526 - if not filename:
1.527 - return False
1.528 -
1.529 - t.insert(0, (dt_string,))
1.530 - self._set_table_atomic(user, filename, t, [(1, "")])
1.531 - return True
1.532 -
1.533 - def set_freebusy_providers(self, user, dt, providers):
1.534 -
1.535 - """
1.536 - Define the uncancelled events providing free/busy details beyond the
1.537 - given datetime 'dt'.
1.538 - """
1.539 -
1.540 - t = []
1.541 -
1.542 - for obj in providers:
1.543 - t.append((obj.get_uid(), obj.get_recurrenceid()))
1.544 -
1.545 - return self._set_freebusy_providers(user, format_datetime(dt), t)
1.546 -
1.547 - def append_freebusy_provider(self, user, provider):
1.548 -
1.549 - "For the given 'user', append the free/busy 'provider'."
1.550 -
1.551 - t = self._get_freebusy_providers(user)
1.552 - if not t:
1.553 - return False
1.554 -
1.555 - dt_string, t = t
1.556 - t.append((provider.get_uid(), provider.get_recurrenceid()))
1.557 -
1.558 - return self._set_freebusy_providers(user, dt_string, t)
1.559 -
1.560 - def remove_freebusy_provider(self, user, provider):
1.561 -
1.562 - "For the given 'user', remove the free/busy 'provider'."
1.563 -
1.564 - t = self._get_freebusy_providers(user)
1.565 - if not t:
1.566 - return False
1.567 -
1.568 - dt_string, t = t
1.569 - try:
1.570 - t.remove((provider.get_uid(), provider.get_recurrenceid()))
1.571 - except ValueError:
1.572 - return False
1.573 -
1.574 - return self._set_freebusy_providers(user, dt_string, t)
1.575 -
1.576 - # Free/busy period access.
1.577 -
1.578 - def get_freebusy(self, user, name=None):
1.579 -
1.580 - "Get free/busy details for the given 'user'."
1.581 -
1.582 - filename = self.get_object_in_store(user, name or "freebusy")
1.583 -
1.584 - if not filename or not isfile(filename):
1.585 - periods = []
1.586 - else:
1.587 - periods = map(lambda t: FreeBusyPeriod(*t),
1.588 - self._get_table_atomic(user, filename))
1.589 -
1.590 - return FreeBusyCollection(periods)
1.591 -
1.592 - def get_freebusy_for_other(self, user, other):
1.593 -
1.594 - "For the given 'user', get free/busy details for the 'other' user."
1.595 -
1.596 - filename = self.get_object_in_store(user, "freebusy-other", other)
1.597 -
1.598 - if not filename or not isfile(filename):
1.599 - periods = []
1.600 - else:
1.601 - periods = map(lambda t: FreeBusyPeriod(*t),
1.602 - self._get_table_atomic(user, filename))
1.603 -
1.604 - return FreeBusyCollection(periods)
1.605 -
1.606 - def set_freebusy(self, user, freebusy, name=None):
1.607 -
1.608 - "For the given 'user', set 'freebusy' details."
1.609 -
1.610 - filename = self.get_object_in_store(user, name or "freebusy")
1.611 - if not filename:
1.612 - return False
1.613 -
1.614 - self._set_table_atomic(user, filename,
1.615 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.616 - return True
1.617 -
1.618 - def set_freebusy_for_other(self, user, freebusy, other):
1.619 -
1.620 - "For the given 'user', set 'freebusy' details for the 'other' user."
1.621 -
1.622 - filename = self.get_object_in_store(user, "freebusy-other", other)
1.623 - if not filename:
1.624 - return False
1.625 -
1.626 - self._set_table_atomic(user, filename,
1.627 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.628 - return True
1.629 -
1.630 - # Tentative free/busy periods related to countering.
1.631 -
1.632 - def get_freebusy_offers(self, user):
1.633 -
1.634 - "Get free/busy offers for the given 'user'."
1.635 -
1.636 - offers = []
1.637 - expired = []
1.638 - now = to_timezone(datetime.utcnow(), "UTC")
1.639 -
1.640 - # Expire old offers and save the collection if modified.
1.641 -
1.642 - self.acquire_lock(user)
1.643 - try:
1.644 - l = self.get_freebusy(user, "freebusy-offers")
1.645 - for fb in l:
1.646 - if fb.expires and get_datetime(fb.expires) <= now:
1.647 - expired.append(fb)
1.648 - else:
1.649 - offers.append(fb)
1.650 -
1.651 - if expired:
1.652 - self.set_freebusy_offers(user, offers)
1.653 - finally:
1.654 - self.release_lock(user)
1.655 -
1.656 - return FreeBusyCollection(offers)
1.657 -
1.658 - def set_freebusy_offers(self, user, freebusy):
1.659 -
1.660 - "For the given 'user', set 'freebusy' offers."
1.661 -
1.662 - return self.set_freebusy(user, freebusy, "freebusy-offers")
1.663 -
1.664 - # Requests and counter-proposals.
1.665 -
1.666 - def _get_requests(self, user, queue):
1.667 -
1.668 - "Get requests for the given 'user' from the given 'queue'."
1.669 -
1.670 - filename = self.get_object_in_store(user, queue)
1.671 - if not filename or not isfile(filename):
1.672 - return None
1.673 -
1.674 - return self._get_table_atomic(user, filename, [(1, None), (2, None)])
1.675 -
1.676 - def get_requests(self, user):
1.677 -
1.678 - "Get requests for the given 'user'."
1.679 -
1.680 - return self._get_requests(user, "requests")
1.681 -
1.682 - def _set_requests(self, user, requests, queue):
1.683 -
1.684 - """
1.685 - For the given 'user', set the list of queued 'requests' in the given
1.686 - 'queue'.
1.687 - """
1.688 -
1.689 - filename = self.get_object_in_store(user, queue)
1.690 - if not filename:
1.691 - return False
1.692 -
1.693 - self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
1.694 - return True
1.695 -
1.696 - def set_requests(self, user, requests):
1.697 -
1.698 - "For the given 'user', set the list of queued 'requests'."
1.699 -
1.700 - return self._set_requests(user, requests, "requests")
1.701 -
1.702 - def _set_request(self, user, request, queue):
1.703 -
1.704 - """
1.705 - For the given 'user', set the given 'request' in the given 'queue'.
1.706 - """
1.707 -
1.708 - filename = self.get_object_in_store(user, queue)
1.709 - if not filename:
1.710 - return False
1.711 -
1.712 - self.acquire_lock(user)
1.713 - try:
1.714 - f = codecs.open(filename, "ab", encoding="utf-8")
1.715 - try:
1.716 - self._set_table_item(f, request, [(1, ""), (2, "")])
1.717 - finally:
1.718 - f.close()
1.719 - fix_permissions(filename)
1.720 - finally:
1.721 - self.release_lock(user)
1.722 -
1.723 - return True
1.724 -
1.725 - def set_request(self, user, uid, recurrenceid=None, type=None):
1.726 -
1.727 - """
1.728 - For the given 'user', set the queued 'uid' and 'recurrenceid',
1.729 - indicating a request, along with any given 'type'.
1.730 - """
1.731 -
1.732 - return self._set_request(user, (uid, recurrenceid, type), "requests")
1.733 -
1.734 - def queue_request(self, user, uid, recurrenceid=None, type=None):
1.735 -
1.736 - """
1.737 - Queue a request for 'user' having the given 'uid'. If the optional
1.738 - 'recurrenceid' is specified, the entry refers to a specific instance
1.739 - or occurrence of an event. The 'type' parameter can be used to indicate
1.740 - a specific type of request.
1.741 - """
1.742 -
1.743 - requests = self.get_requests(user) or []
1.744 -
1.745 - if not self.have_request(requests, uid, recurrenceid):
1.746 - return self.set_request(user, uid, recurrenceid, type)
1.747 -
1.748 - return False
1.749 -
1.750 - def dequeue_request(self, user, uid, recurrenceid=None):
1.751 -
1.752 - """
1.753 - Dequeue all requests for 'user' having the given 'uid'. If the optional
1.754 - 'recurrenceid' is specified, all requests for that specific instance or
1.755 - occurrence of an event are dequeued.
1.756 - """
1.757 -
1.758 - requests = self.get_requests(user) or []
1.759 - result = []
1.760 -
1.761 - for request in requests:
1.762 - if request[:2] != (uid, recurrenceid):
1.763 - result.append(request)
1.764 -
1.765 - self.set_requests(user, result)
1.766 - return True
1.767 -
1.768 - def has_request(self, user, uid, recurrenceid=None, type=None, strict=False):
1.769 - return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict)
1.770 -
1.771 - def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):
1.772 -
1.773 - """
1.774 - Return whether 'requests' contains a request with the given 'uid' and
1.775 - any specified 'recurrenceid' and 'type'. If 'strict' is set to a true
1.776 - value, the precise type of the request must match; otherwise, any type
1.777 - of request for the identified object may be matched.
1.778 - """
1.779 -
1.780 - for request in requests:
1.781 - if request[:2] == (uid, recurrenceid) and (
1.782 - not strict or
1.783 - not request[2:] and not type or
1.784 - request[2:] and request[2] == type):
1.785 -
1.786 - return True
1.787 -
1.788 - return False
1.789 -
1.790 - def get_counters(self, user, uid, recurrenceid=None):
1.791 -
1.792 - """
1.793 - For the given 'user', return a list of users from whom counter-proposals
1.794 - have been received for the given 'uid' and optional 'recurrenceid'.
1.795 - """
1.796 -
1.797 - filename = self.get_event_filename(user, uid, recurrenceid, "counters")
1.798 - if not filename or not isdir(filename):
1.799 - return False
1.800 -
1.801 - return [name for name in listdir(filename) if isfile(join(filename, name))]
1.802 -
1.803 - def get_counter(self, user, other, uid, recurrenceid=None):
1.804 -
1.805 - """
1.806 - For the given 'user', return the counter-proposal from 'other' for the
1.807 - given 'uid' and optional 'recurrenceid'.
1.808 - """
1.809 -
1.810 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.811 - if not filename:
1.812 - return False
1.813 -
1.814 - return self._get_object(user, filename)
1.815 -
1.816 - def set_counter(self, user, other, node, uid, recurrenceid=None):
1.817 -
1.818 - """
1.819 - For the given 'user', store a counter-proposal received from 'other' the
1.820 - given 'node' representing that proposal for the given 'uid' and
1.821 - 'recurrenceid'.
1.822 - """
1.823 -
1.824 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.825 - if not filename:
1.826 - return False
1.827 -
1.828 - return self._set_object(user, filename, node)
1.829 -
1.830 - def remove_counters(self, user, uid, recurrenceid=None):
1.831 -
1.832 - """
1.833 - For the given 'user', remove all counter-proposals associated with the
1.834 - given 'uid' and 'recurrenceid'.
1.835 - """
1.836 -
1.837 - filename = self.get_event_filename(user, uid, recurrenceid, "counters")
1.838 - if not filename or not isdir(filename):
1.839 - return False
1.840 -
1.841 - removed = False
1.842 -
1.843 - for other in listdir(filename):
1.844 - counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.845 - removed = removed or self._remove_object(counter_filename)
1.846 -
1.847 - return removed
1.848 -
1.849 - def remove_counter(self, user, other, uid, recurrenceid=None):
1.850 -
1.851 - """
1.852 - For the given 'user', remove any counter-proposal from 'other'
1.853 - associated with the given 'uid' and 'recurrenceid'.
1.854 - """
1.855 -
1.856 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
1.857 - if not filename or not isfile(filename):
1.858 - return False
1.859 -
1.860 - return self._remove_object(filename)
1.861 -
1.862 - # Event cancellation.
1.863 -
1.864 - def cancel_event(self, user, uid, recurrenceid=None):
1.865 -
1.866 - """
1.867 - Cancel an event for 'user' having the given 'uid'. If the optional
1.868 - 'recurrenceid' is specified, a specific instance or occurrence of an
1.869 - event is cancelled.
1.870 - """
1.871 -
1.872 - filename = self.get_event_filename(user, uid, recurrenceid)
1.873 - cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
1.874 -
1.875 - if filename and cancelled_filename and isfile(filename):
1.876 - return self.move_object(filename, cancelled_filename)
1.877 -
1.878 - return False
1.879 -
1.880 - def uncancel_event(self, user, uid, recurrenceid=None):
1.881 -
1.882 - """
1.883 - Uncancel an event for 'user' having the given 'uid'. If the optional
1.884 - 'recurrenceid' is specified, a specific instance or occurrence of an
1.885 - event is uncancelled.
1.886 - """
1.887 -
1.888 - filename = self.get_event_filename(user, uid, recurrenceid)
1.889 - cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
1.890 -
1.891 - if filename and cancelled_filename and isfile(cancelled_filename):
1.892 - return self.move_object(cancelled_filename, filename)
1.893 -
1.894 - return False
1.895 -
1.896 - def remove_cancellations(self, user, uid, recurrenceid=None):
1.897 -
1.898 - """
1.899 - Remove cancellations for 'user' for any event having the given 'uid'. If
1.900 - the optional 'recurrenceid' is specified, a specific instance or
1.901 - occurrence of an event is affected.
1.902 - """
1.903 -
1.904 - # Remove all recurrence cancellations if a general event is indicated.
1.905 -
1.906 - if not recurrenceid:
1.907 - for _recurrenceid in self.get_cancelled_recurrences(user, uid):
1.908 - self.remove_cancellation(user, uid, _recurrenceid)
1.909 -
1.910 - return self.remove_cancellation(user, uid, recurrenceid)
1.911 -
1.912 - def remove_cancellation(self, user, uid, recurrenceid=None):
1.913 -
1.914 - """
1.915 - Remove a cancellation for 'user' for the event having the given 'uid'.
1.916 - If the optional 'recurrenceid' is specified, a specific instance or
1.917 - occurrence of an event is affected.
1.918 - """
1.919 -
1.920 - # Remove any parent event cancellation or a specific recurrence
1.921 - # cancellation if indicated.
1.922 -
1.923 - filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
1.924 -
1.925 - if filename and isfile(filename):
1.926 - return self._remove_object(filename)
1.927 -
1.928 - return False
1.929 -
1.930 -class FilePublisher(FileBase):
1.931 -
1.932 - "A publisher of objects."
1.933 -
1.934 - def __init__(self, store_dir=None):
1.935 - FileBase.__init__(self, store_dir or PUBLISH_DIR)
1.936 -
1.937 - def set_freebusy(self, user, freebusy):
1.938 -
1.939 - "For the given 'user', set 'freebusy' details."
1.940 -
1.941 - filename = self.get_object_in_store(user, "freebusy")
1.942 - if not filename:
1.943 - return False
1.944 -
1.945 - record = []
1.946 - rwrite = record.append
1.947 -
1.948 - rwrite(("ORGANIZER", {}, user))
1.949 - rwrite(("UID", {}, user))
1.950 - rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
1.951 -
1.952 - for fb in freebusy:
1.953 - if not fb.transp or fb.transp == "OPAQUE":
1.954 - rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
1.955 - map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
1.956 -
1.957 - f = open(filename, "wb")
1.958 - try:
1.959 - to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
1.960 - finally:
1.961 - f.close()
1.962 - fix_permissions(filename)
1.963 -
1.964 - return True
1.965 -
1.966 -class FileJournal(FileStoreBase):
1.967 -
1.968 - "A journal system to support quotas."
1.969 -
1.970 - def __init__(self, store_dir=None):
1.971 - FileBase.__init__(self, store_dir or JOURNAL_DIR)
1.972 -
1.973 - # Quota and user identity/group discovery.
1.974 -
1.975 - def get_quotas(self):
1.976 -
1.977 - "Return a list of quotas."
1.978 -
1.979 - return listdir(self.store_dir)
1.980 -
1.981 - def get_quota_users(self, quota):
1.982 -
1.983 - "Return a list of quota users."
1.984 -
1.985 - filename = self.get_object_in_store(quota, "journal")
1.986 - if not filename or not isdir(filename):
1.987 - return []
1.988 -
1.989 - return listdir(filename)
1.990 -
1.991 - # Groups of users sharing quotas.
1.992 -
1.993 - def get_groups(self, quota):
1.994 -
1.995 - "Return the identity mappings for the given 'quota' as a dictionary."
1.996 -
1.997 - filename = self.get_object_in_store(quota, "groups")
1.998 - if not filename or not isfile(filename):
1.999 - return {}
1.1000 -
1.1001 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
1.1002 -
1.1003 - def get_limits(self, quota):
1.1004 -
1.1005 - """
1.1006 - Return the limits for the 'quota' as a dictionary mapping identities or
1.1007 - groups to durations.
1.1008 - """
1.1009 -
1.1010 - filename = self.get_object_in_store(quota, "limits")
1.1011 - if not filename or not isfile(filename):
1.1012 - return None
1.1013 -
1.1014 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
1.1015 -
1.1016 - # Free/busy period access for users within quota groups.
1.1017 -
1.1018 - def get_freebusy(self, quota, user):
1.1019 -
1.1020 - "Get free/busy details for the given 'quota' and 'user'."
1.1021 -
1.1022 - filename = self.get_object_in_store(quota, "freebusy", user)
1.1023 -
1.1024 - if not filename or not isfile(filename):
1.1025 - periods = []
1.1026 - else:
1.1027 - periods = map(lambda t: FreeBusyPeriod(*t),
1.1028 - self._get_table_atomic(quota, filename))
1.1029 -
1.1030 - return FreeBusyCollection(periods)
1.1031 -
1.1032 - def set_freebusy(self, quota, user, freebusy):
1.1033 -
1.1034 - "For the given 'quota' and 'user', set 'freebusy' details."
1.1035 -
1.1036 - filename = self.get_object_in_store(quota, "freebusy", user)
1.1037 - if not filename:
1.1038 - return False
1.1039 -
1.1040 - self._set_table_atomic(quota, filename,
1.1041 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
1.1042 - return True
1.1043 -
1.1044 - # Journal entry methods.
1.1045 -
1.1046 - def get_entries(self, quota, group):
1.1047 -
1.1048 - """
1.1049 - Return a list of journal entries for the given 'quota' for the indicated
1.1050 - 'group'.
1.1051 - """
1.1052 -
1.1053 - filename = self.get_object_in_store(quota, "journal", group)
1.1054 -
1.1055 - if not filename or not isfile(filename):
1.1056 - periods = []
1.1057 - else:
1.1058 - periods = map(lambda t: FreeBusyPeriod(*t),
1.1059 - self._get_table_atomic(quota, filename))
1.1060 -
1.1061 - return FreeBusyCollection(periods)
1.1062 -
1.1063 - def set_entries(self, quota, group, entries):
1.1064 -
1.1065 - """
1.1066 - For the given 'quota' and indicated 'group', set the list of journal
1.1067 - 'entries'.
1.1068 - """
1.1069 -
1.1070 - filename = self.get_object_in_store(quota, "journal", group)
1.1071 - if not filename:
1.1072 - return False
1.1073 -
1.1074 - self._set_table_atomic(quota, filename,
1.1075 - map(lambda fb: fb.as_tuple(strings_only=True), entries.periods))
1.1076 - return True
1.1077 -
1.1078 -# vim: tabstop=4 expandtab shiftwidth=4