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