1.1 --- a/docs/wiki/Developing Sat Mar 05 23:45:47 2016 +0100
1.2 +++ b/docs/wiki/Developing Sun Mar 06 00:46:26 2016 +0100
1.3 @@ -61,10 +61,6 @@
1.4 `imip_manager.py`
1.5 || The [[../CalendarManager|management interface]] main program file
1.6 ==
1.7 -`imip_store.py`
1.8 -|| The Python module providing an abstraction over the
1.9 -.. [[../FilesystemUsage|data storage structures]]
1.10 -==
1.11 `markup.py`
1.12 || A Python library providing HTML generation support
1.13 ==
1.14 @@ -112,6 +108,22 @@
1.15 system client, typically handling a single calendar object at any given
1.16 time.
1.17
1.18 +The `imiptools.stores` package provides the basis for calendar data
1.19 +persistence. From the very start, the nature of data organisation for
1.20 +calendar users was centred on the storage of each user's free/busy records,
1.21 +since the priority was to generate such records from messages exchanged
1.22 +over e-mail, and the use of plain text files was chosen as the simplest
1.23 +and most transparent approach. Beyond this, the need to retain calendar
1.24 +objects arose, and thus a [[../FilesystemUsage|filesystem-based approach]]
1.25 +was cultivated to manage such data.
1.26 +
1.27 +In the future, other persistence mechanisms could be supported. However,
1.28 +aside from performance concerns around access to free/busy schedules,
1.29 +there may be no urgent need to adopt relational database technologies,
1.30 +particularly as each user's data should remain isolated from that of
1.31 +other users, and thus the volumes of data should remain relatively small
1.32 +if managed well enough.
1.33 +
1.34 === imipweb ===
1.35
1.36 Most of the `imipweb` package is concerned with the display of calendar
1.37 @@ -138,24 +150,6 @@
1.38 Web page, interpreted by the program, written back to the form, all
1.39 without losing information.
1.40
1.41 -=== imip_store ===
1.42 -
1.43 -The `imip_store` module provides the basis for calendar data persistence.
1.44 -From the very start, the nature of data organisation for calendar users
1.45 -was centred on the storage of each user's free/busy records, since the
1.46 -priority was to generate such records from messages exchanged over e-mail,
1.47 -and the use of plain text files was chosen as the simplest and most
1.48 -transparent approach. Beyond this, the need to retain calendar objects
1.49 -arose, and thus a [[../FilesystemUsage|filesystem-based approach]] was
1.50 -cultivated to manage such data.
1.51 -
1.52 -In the future, other persistence mechanisms could be supported. However,
1.53 -aside from performance concerns around access to free/busy schedules,
1.54 -there may be no urgent need to adopt relational database technologies,
1.55 -particularly as each user's data should remain isolated from that of
1.56 -other users, and thus the volumes of data should remain relatively small
1.57 -if managed well enough.
1.58 -
1.59 == Localisation ==
1.60
1.61 The traditional [[https://www.gnu.org/s/gettext|gettext]] mechanisms for
2.1 --- a/imip_store.py Sat Mar 05 23:45:47 2016 +0100
2.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2.3 @@ -1,1075 +0,0 @@
2.4 -#!/usr/bin/env python
2.5 -
2.6 -"""
2.7 -A simple filesystem-based store of calendar data.
2.8 -
2.9 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
2.10 -
2.11 -This program is free software; you can redistribute it and/or modify it under
2.12 -the terms of the GNU General Public License as published by the Free Software
2.13 -Foundation; either version 3 of the License, or (at your option) any later
2.14 -version.
2.15 -
2.16 -This program is distributed in the hope that it will be useful, but WITHOUT
2.17 -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
2.18 -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
2.19 -details.
2.20 -
2.21 -You should have received a copy of the GNU General Public License along with
2.22 -this program. If not, see <http://www.gnu.org/licenses/>.
2.23 -"""
2.24 -
2.25 -from datetime import datetime
2.26 -from imiptools.config import STORE_DIR, PUBLISH_DIR, JOURNAL_DIR
2.27 -from imiptools.data import make_calendar, parse_object, to_stream
2.28 -from imiptools.dates import format_datetime, get_datetime, to_timezone
2.29 -from imiptools.filesys import fix_permissions, FileBase
2.30 -from imiptools.period import FreeBusyPeriod, FreeBusyCollection
2.31 -from imiptools.text import parse_line
2.32 -from os.path import isdir, isfile, join
2.33 -from os import listdir, remove, rmdir
2.34 -from time import sleep
2.35 -import codecs
2.36 -
2.37 -class FileStoreBase(FileBase):
2.38 -
2.39 - "A file store supporting user-specific locking and tabular data."
2.40 -
2.41 - def acquire_lock(self, user, timeout=None):
2.42 - FileBase.acquire_lock(self, timeout, user)
2.43 -
2.44 - def release_lock(self, user):
2.45 - FileBase.release_lock(self, user)
2.46 -
2.47 - # Utility methods.
2.48 -
2.49 - def _set_defaults(self, t, empty_defaults):
2.50 - for i, default in empty_defaults:
2.51 - if i >= len(t):
2.52 - t += [None] * (i - len(t) + 1)
2.53 - if not t[i]:
2.54 - t[i] = default
2.55 - return t
2.56 -
2.57 - def _get_table(self, user, filename, empty_defaults=None, tab_separated=True):
2.58 -
2.59 - """
2.60 - From the file for the given 'user' having the given 'filename', return
2.61 - a list of tuples representing the file's contents.
2.62 -
2.63 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.64 - default value where a column either does not exist or provides an empty
2.65 - value.
2.66 -
2.67 - If 'tab_separated' is specified and is a false value, line parsing using
2.68 - the imiptools.text.parse_line function will be performed instead of
2.69 - splitting each line of the file using tab characters as separators.
2.70 - """
2.71 -
2.72 - f = codecs.open(filename, "rb", encoding="utf-8")
2.73 - try:
2.74 - l = []
2.75 - for line in f.readlines():
2.76 - line = line.strip(" \r\n")
2.77 - if tab_separated:
2.78 - t = line.split("\t")
2.79 - else:
2.80 - t = parse_line(line)
2.81 - if empty_defaults:
2.82 - t = self._set_defaults(t, empty_defaults)
2.83 - l.append(tuple(t))
2.84 - return l
2.85 - finally:
2.86 - f.close()
2.87 -
2.88 - def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True):
2.89 -
2.90 - """
2.91 - From the file for the given 'user' having the given 'filename', return
2.92 - a list of tuples representing the file's contents.
2.93 -
2.94 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.95 - default value where a column either does not exist or provides an empty
2.96 - value.
2.97 -
2.98 - If 'tab_separated' is specified and is a false value, line parsing using
2.99 - the imiptools.text.parse_line function will be performed instead of
2.100 - splitting each line of the file using tab characters as separators.
2.101 - """
2.102 -
2.103 - self.acquire_lock(user)
2.104 - try:
2.105 - return self._get_table(user, filename, empty_defaults, tab_separated)
2.106 - finally:
2.107 - self.release_lock(user)
2.108 -
2.109 - def _set_table(self, user, filename, items, empty_defaults=None):
2.110 -
2.111 - """
2.112 - For the given 'user', write to the file having the given 'filename' the
2.113 - 'items'.
2.114 -
2.115 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.116 - default value where a column either does not exist or provides an empty
2.117 - value.
2.118 - """
2.119 -
2.120 - f = codecs.open(filename, "wb", encoding="utf-8")
2.121 - try:
2.122 - for item in items:
2.123 - self._set_table_item(f, item, empty_defaults)
2.124 - finally:
2.125 - f.close()
2.126 - fix_permissions(filename)
2.127 -
2.128 - def _set_table_item(self, f, item, empty_defaults=None):
2.129 -
2.130 - "Set in table 'f' the given 'item', using any 'empty_defaults'."
2.131 -
2.132 - if empty_defaults:
2.133 - item = self._set_defaults(list(item), empty_defaults)
2.134 - f.write("\t".join(item) + "\n")
2.135 -
2.136 - def _set_table_atomic(self, user, filename, items, empty_defaults=None):
2.137 -
2.138 - """
2.139 - For the given 'user', write to the file having the given 'filename' the
2.140 - 'items'.
2.141 -
2.142 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.143 - default value where a column either does not exist or provides an empty
2.144 - value.
2.145 - """
2.146 -
2.147 - self.acquire_lock(user)
2.148 - try:
2.149 - self._set_table(user, filename, items, empty_defaults)
2.150 - finally:
2.151 - self.release_lock(user)
2.152 -
2.153 -class FileStore(FileStoreBase):
2.154 -
2.155 - "A file store of tabular free/busy data and objects."
2.156 -
2.157 - def __init__(self, store_dir=None):
2.158 - FileBase.__init__(self, store_dir or STORE_DIR)
2.159 -
2.160 - # Store object access.
2.161 -
2.162 - def _get_object(self, user, filename):
2.163 -
2.164 - """
2.165 - Return the parsed object for the given 'user' having the given
2.166 - 'filename'.
2.167 - """
2.168 -
2.169 - self.acquire_lock(user)
2.170 - try:
2.171 - f = open(filename, "rb")
2.172 - try:
2.173 - return parse_object(f, "utf-8")
2.174 - finally:
2.175 - f.close()
2.176 - finally:
2.177 - self.release_lock(user)
2.178 -
2.179 - def _set_object(self, user, filename, node):
2.180 -
2.181 - """
2.182 - Set an object for the given 'user' having the given 'filename', using
2.183 - 'node' to define the object.
2.184 - """
2.185 -
2.186 - self.acquire_lock(user)
2.187 - try:
2.188 - f = open(filename, "wb")
2.189 - try:
2.190 - to_stream(f, node)
2.191 - finally:
2.192 - f.close()
2.193 - fix_permissions(filename)
2.194 - finally:
2.195 - self.release_lock(user)
2.196 -
2.197 - return True
2.198 -
2.199 - def _remove_object(self, filename):
2.200 -
2.201 - "Remove the object with the given 'filename'."
2.202 -
2.203 - try:
2.204 - remove(filename)
2.205 - except OSError:
2.206 - return False
2.207 -
2.208 - return True
2.209 -
2.210 - def _remove_collection(self, filename):
2.211 -
2.212 - "Remove the collection with the given 'filename'."
2.213 -
2.214 - try:
2.215 - rmdir(filename)
2.216 - except OSError:
2.217 - return False
2.218 -
2.219 - return True
2.220 -
2.221 - # User discovery.
2.222 -
2.223 - def get_users(self):
2.224 -
2.225 - "Return a list of users."
2.226 -
2.227 - return listdir(self.store_dir)
2.228 -
2.229 - # Event and event metadata access.
2.230 -
2.231 - def get_events(self, user):
2.232 -
2.233 - "Return a list of event identifiers."
2.234 -
2.235 - filename = self.get_object_in_store(user, "objects")
2.236 - if not filename or not isdir(filename):
2.237 - return None
2.238 -
2.239 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.240 -
2.241 - def get_all_events(self, user):
2.242 -
2.243 - "Return a set of (uid, recurrenceid) tuples for all events."
2.244 -
2.245 - uids = self.get_events(user)
2.246 - if not uids:
2.247 - return set()
2.248 -
2.249 - all_events = set()
2.250 - for uid in uids:
2.251 - all_events.add((uid, None))
2.252 - all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
2.253 -
2.254 - return all_events
2.255 -
2.256 - def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
2.257 -
2.258 - """
2.259 - Get the filename providing the event for the given 'user' with the given
2.260 - 'uid'. If the optional 'recurrenceid' is specified, a specific instance
2.261 - or occurrence of an event is returned.
2.262 -
2.263 - Where 'dirname' is specified, the given directory name is used as the
2.264 - base of the location within which any filename will reside.
2.265 - """
2.266 -
2.267 - if recurrenceid:
2.268 - return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
2.269 - else:
2.270 - return self.get_complete_event_filename(user, uid, dirname, username)
2.271 -
2.272 - def get_event(self, user, uid, recurrenceid=None, dirname=None):
2.273 -
2.274 - """
2.275 - Get the event for the given 'user' with the given 'uid'. If
2.276 - the optional 'recurrenceid' is specified, a specific instance or
2.277 - occurrence of an event is returned.
2.278 - """
2.279 -
2.280 - filename = self.get_event_filename(user, uid, recurrenceid, dirname)
2.281 - if not filename or not isfile(filename):
2.282 - return None
2.283 -
2.284 - return filename and self._get_object(user, filename)
2.285 -
2.286 - def get_complete_event_filename(self, user, uid, dirname=None, username=None):
2.287 -
2.288 - """
2.289 - Get the filename providing the event for the given 'user' with the given
2.290 - 'uid'.
2.291 -
2.292 - Where 'dirname' is specified, the given directory name is used as the
2.293 - base of the location within which any filename will reside.
2.294 -
2.295 - Where 'username' is specified, the event details will reside in a file
2.296 - bearing that name within a directory having 'uid' as its name.
2.297 - """
2.298 -
2.299 - return self.get_object_in_store(user, dirname, "objects", uid, username)
2.300 -
2.301 - def get_complete_event(self, user, uid):
2.302 -
2.303 - "Get the event for the given 'user' with the given 'uid'."
2.304 -
2.305 - filename = self.get_complete_event_filename(user, uid)
2.306 - if not filename or not isfile(filename):
2.307 - return None
2.308 -
2.309 - return filename and self._get_object(user, filename)
2.310 -
2.311 - def set_event(self, user, uid, recurrenceid, node):
2.312 -
2.313 - """
2.314 - Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
2.315 - if the latter is specified, a specific instance or occurrence of an
2.316 - event is referenced), using the given 'node' description.
2.317 - """
2.318 -
2.319 - if recurrenceid:
2.320 - return self.set_recurrence(user, uid, recurrenceid, node)
2.321 - else:
2.322 - return self.set_complete_event(user, uid, node)
2.323 -
2.324 - def set_complete_event(self, user, uid, node):
2.325 -
2.326 - "Set an event for 'user' having the given 'uid' and 'node'."
2.327 -
2.328 - filename = self.get_object_in_store(user, "objects", uid)
2.329 - if not filename:
2.330 - return False
2.331 -
2.332 - return self._set_object(user, filename, node)
2.333 -
2.334 - def remove_event(self, user, uid, recurrenceid=None):
2.335 -
2.336 - """
2.337 - Remove an event for 'user' having the given 'uid'. If the optional
2.338 - 'recurrenceid' is specified, a specific instance or occurrence of an
2.339 - event is removed.
2.340 - """
2.341 -
2.342 - if recurrenceid:
2.343 - return self.remove_recurrence(user, uid, recurrenceid)
2.344 - else:
2.345 - for recurrenceid in self.get_recurrences(user, uid) or []:
2.346 - self.remove_recurrence(user, uid, recurrenceid)
2.347 - return self.remove_complete_event(user, uid)
2.348 -
2.349 - def remove_complete_event(self, user, uid):
2.350 -
2.351 - "Remove an event for 'user' having the given 'uid'."
2.352 -
2.353 - self.remove_recurrences(user, uid)
2.354 -
2.355 - filename = self.get_object_in_store(user, "objects", uid)
2.356 - if not filename:
2.357 - return False
2.358 -
2.359 - return self._remove_object(filename)
2.360 -
2.361 - def get_recurrences(self, user, uid):
2.362 -
2.363 - """
2.364 - Get additional event instances for an event of the given 'user' with the
2.365 - indicated 'uid'. Both active and cancelled recurrences are returned.
2.366 - """
2.367 -
2.368 - return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
2.369 -
2.370 - def get_active_recurrences(self, user, uid):
2.371 -
2.372 - """
2.373 - Get additional event instances for an event of the given 'user' with the
2.374 - indicated 'uid'. Cancelled recurrences are not returned.
2.375 - """
2.376 -
2.377 - filename = self.get_object_in_store(user, "recurrences", uid)
2.378 - if not filename or not isdir(filename):
2.379 - return []
2.380 -
2.381 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.382 -
2.383 - def get_cancelled_recurrences(self, user, uid):
2.384 -
2.385 - """
2.386 - Get additional event instances for an event of the given 'user' with the
2.387 - indicated 'uid'. Only cancelled recurrences are returned.
2.388 - """
2.389 -
2.390 - filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
2.391 - if not filename or not isdir(filename):
2.392 - return []
2.393 -
2.394 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.395 -
2.396 - def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
2.397 -
2.398 - """
2.399 - For the event of the given 'user' with the given 'uid', return the
2.400 - filename providing the recurrence with the given 'recurrenceid'.
2.401 -
2.402 - Where 'dirname' is specified, the given directory name is used as the
2.403 - base of the location within which any filename will reside.
2.404 -
2.405 - Where 'username' is specified, the event details will reside in a file
2.406 - bearing that name within a directory having 'uid' as its name.
2.407 - """
2.408 -
2.409 - return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
2.410 -
2.411 - def get_recurrence(self, user, uid, recurrenceid):
2.412 -
2.413 - """
2.414 - For the event of the given 'user' with the given 'uid', return the
2.415 - specific recurrence indicated by the 'recurrenceid'.
2.416 - """
2.417 -
2.418 - filename = self.get_recurrence_filename(user, uid, recurrenceid)
2.419 - if not filename or not isfile(filename):
2.420 - return None
2.421 -
2.422 - return filename and self._get_object(user, filename)
2.423 -
2.424 - def set_recurrence(self, user, uid, recurrenceid, node):
2.425 -
2.426 - "Set an event for 'user' having the given 'uid' and 'node'."
2.427 -
2.428 - filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
2.429 - if not filename:
2.430 - return False
2.431 -
2.432 - return self._set_object(user, filename, node)
2.433 -
2.434 - def remove_recurrence(self, user, uid, recurrenceid):
2.435 -
2.436 - """
2.437 - Remove a special recurrence from an event stored by 'user' having the
2.438 - given 'uid' and 'recurrenceid'.
2.439 - """
2.440 -
2.441 - filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
2.442 - if not filename:
2.443 - return False
2.444 -
2.445 - return self._remove_object(filename)
2.446 -
2.447 - def remove_recurrences(self, user, uid):
2.448 -
2.449 - """
2.450 - Remove all recurrences for an event stored by 'user' having the given
2.451 - 'uid'.
2.452 - """
2.453 -
2.454 - for recurrenceid in self.get_recurrences(user, uid):
2.455 - self.remove_recurrence(user, uid, recurrenceid)
2.456 -
2.457 - recurrences = self.get_object_in_store(user, "recurrences", uid)
2.458 - if recurrences:
2.459 - return self._remove_collection(recurrences)
2.460 -
2.461 - return True
2.462 -
2.463 - # Free/busy period providers, upon extension of the free/busy records.
2.464 -
2.465 - def _get_freebusy_providers(self, user):
2.466 -
2.467 - """
2.468 - Return the free/busy providers for the given 'user'.
2.469 -
2.470 - This function returns any stored datetime and a list of providers as a
2.471 - 2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
2.472 - """
2.473 -
2.474 - filename = self.get_object_in_store(user, "freebusy-providers")
2.475 - if not filename or not isfile(filename):
2.476 - return None
2.477 -
2.478 - # Attempt to read providers, with a declaration of the datetime
2.479 - # from which such providers are considered as still being active.
2.480 -
2.481 - t = self._get_table_atomic(user, filename, [(1, None)])
2.482 - try:
2.483 - dt_string = t[0][0]
2.484 - except IndexError:
2.485 - return None
2.486 -
2.487 - return dt_string, t[1:]
2.488 -
2.489 - def get_freebusy_providers(self, user, dt=None):
2.490 -
2.491 - """
2.492 - Return a set of uncancelled events of the form (uid, recurrenceid)
2.493 - providing free/busy details beyond the given datetime 'dt'.
2.494 -
2.495 - If 'dt' is not specified, all events previously found to provide
2.496 - details will be returned. Otherwise, if 'dt' is earlier than the
2.497 - datetime recorded for the known providers, None is returned, indicating
2.498 - that the list of providers must be recomputed.
2.499 -
2.500 - This function returns a list of (uid, recurrenceid) tuples upon success.
2.501 - """
2.502 -
2.503 - t = self._get_freebusy_providers(user)
2.504 - if not t:
2.505 - return None
2.506 -
2.507 - dt_string, t = t
2.508 -
2.509 - # If the requested datetime is earlier than the stated datetime, the
2.510 - # providers will need to be recomputed.
2.511 -
2.512 - if dt:
2.513 - providers_dt = get_datetime(dt_string)
2.514 - if not providers_dt or providers_dt > dt:
2.515 - return None
2.516 -
2.517 - # Otherwise, return the providers.
2.518 -
2.519 - return t[1:]
2.520 -
2.521 - def _set_freebusy_providers(self, user, dt_string, t):
2.522 -
2.523 - "Set the given provider timestamp 'dt_string' and table 't'."
2.524 -
2.525 - filename = self.get_object_in_store(user, "freebusy-providers")
2.526 - if not filename:
2.527 - return False
2.528 -
2.529 - t.insert(0, (dt_string,))
2.530 - self._set_table_atomic(user, filename, t, [(1, "")])
2.531 - return True
2.532 -
2.533 - def set_freebusy_providers(self, user, dt, providers):
2.534 -
2.535 - """
2.536 - Define the uncancelled events providing free/busy details beyond the
2.537 - given datetime 'dt'.
2.538 - """
2.539 -
2.540 - t = []
2.541 -
2.542 - for obj in providers:
2.543 - t.append((obj.get_uid(), obj.get_recurrenceid()))
2.544 -
2.545 - return self._set_freebusy_providers(user, format_datetime(dt), t)
2.546 -
2.547 - def append_freebusy_provider(self, user, provider):
2.548 -
2.549 - "For the given 'user', append the free/busy 'provider'."
2.550 -
2.551 - t = self._get_freebusy_providers(user)
2.552 - if not t:
2.553 - return False
2.554 -
2.555 - dt_string, t = t
2.556 - t.append((provider.get_uid(), provider.get_recurrenceid()))
2.557 -
2.558 - return self._set_freebusy_providers(user, dt_string, t)
2.559 -
2.560 - def remove_freebusy_provider(self, user, provider):
2.561 -
2.562 - "For the given 'user', remove the free/busy 'provider'."
2.563 -
2.564 - t = self._get_freebusy_providers(user)
2.565 - if not t:
2.566 - return False
2.567 -
2.568 - dt_string, t = t
2.569 - try:
2.570 - t.remove((provider.get_uid(), provider.get_recurrenceid()))
2.571 - except ValueError:
2.572 - return False
2.573 -
2.574 - return self._set_freebusy_providers(user, dt_string, t)
2.575 -
2.576 - # Free/busy period access.
2.577 -
2.578 - def get_freebusy(self, user, name=None):
2.579 -
2.580 - "Get free/busy details for the given 'user'."
2.581 -
2.582 - filename = self.get_object_in_store(user, name or "freebusy")
2.583 -
2.584 - if not filename or not isfile(filename):
2.585 - periods = []
2.586 - else:
2.587 - periods = map(lambda t: FreeBusyPeriod(*t),
2.588 - self._get_table_atomic(user, filename))
2.589 -
2.590 - return FreeBusyCollection(periods)
2.591 -
2.592 - def get_freebusy_for_other(self, user, other):
2.593 -
2.594 - "For the given 'user', get free/busy details for the 'other' user."
2.595 -
2.596 - filename = self.get_object_in_store(user, "freebusy-other", other)
2.597 -
2.598 - if not filename or not isfile(filename):
2.599 - periods = []
2.600 - else:
2.601 - periods = map(lambda t: FreeBusyPeriod(*t),
2.602 - self._get_table_atomic(user, filename))
2.603 -
2.604 - return FreeBusyCollection(periods)
2.605 -
2.606 - def set_freebusy(self, user, freebusy, name=None):
2.607 -
2.608 - "For the given 'user', set 'freebusy' details."
2.609 -
2.610 - filename = self.get_object_in_store(user, name or "freebusy")
2.611 - if not filename:
2.612 - return False
2.613 -
2.614 - self._set_table_atomic(user, filename,
2.615 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
2.616 - return True
2.617 -
2.618 - def set_freebusy_for_other(self, user, freebusy, other):
2.619 -
2.620 - "For the given 'user', set 'freebusy' details for the 'other' user."
2.621 -
2.622 - filename = self.get_object_in_store(user, "freebusy-other", other)
2.623 - if not filename:
2.624 - return False
2.625 -
2.626 - self._set_table_atomic(user, filename,
2.627 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
2.628 - return True
2.629 -
2.630 - # Tentative free/busy periods related to countering.
2.631 -
2.632 - def get_freebusy_offers(self, user):
2.633 -
2.634 - "Get free/busy offers for the given 'user'."
2.635 -
2.636 - offers = []
2.637 - expired = []
2.638 - now = to_timezone(datetime.utcnow(), "UTC")
2.639 -
2.640 - # Expire old offers and save the collection if modified.
2.641 -
2.642 - self.acquire_lock(user)
2.643 - try:
2.644 - l = self.get_freebusy(user, "freebusy-offers")
2.645 - for fb in l:
2.646 - if fb.expires and get_datetime(fb.expires) <= now:
2.647 - expired.append(fb)
2.648 - else:
2.649 - offers.append(fb)
2.650 -
2.651 - if expired:
2.652 - self.set_freebusy_offers(user, offers)
2.653 - finally:
2.654 - self.release_lock(user)
2.655 -
2.656 - return FreeBusyCollection(offers)
2.657 -
2.658 - def set_freebusy_offers(self, user, freebusy):
2.659 -
2.660 - "For the given 'user', set 'freebusy' offers."
2.661 -
2.662 - return self.set_freebusy(user, freebusy, "freebusy-offers")
2.663 -
2.664 - # Requests and counter-proposals.
2.665 -
2.666 - def _get_requests(self, user, queue):
2.667 -
2.668 - "Get requests for the given 'user' from the given 'queue'."
2.669 -
2.670 - filename = self.get_object_in_store(user, queue)
2.671 - if not filename or not isfile(filename):
2.672 - return None
2.673 -
2.674 - return self._get_table_atomic(user, filename, [(1, None), (2, None)])
2.675 -
2.676 - def get_requests(self, user):
2.677 -
2.678 - "Get requests for the given 'user'."
2.679 -
2.680 - return self._get_requests(user, "requests")
2.681 -
2.682 - def _set_requests(self, user, requests, queue):
2.683 -
2.684 - """
2.685 - For the given 'user', set the list of queued 'requests' in the given
2.686 - 'queue'.
2.687 - """
2.688 -
2.689 - filename = self.get_object_in_store(user, queue)
2.690 - if not filename:
2.691 - return False
2.692 -
2.693 - self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
2.694 - return True
2.695 -
2.696 - def set_requests(self, user, requests):
2.697 -
2.698 - "For the given 'user', set the list of queued 'requests'."
2.699 -
2.700 - return self._set_requests(user, requests, "requests")
2.701 -
2.702 - def _set_request(self, user, request, queue):
2.703 -
2.704 - """
2.705 - For the given 'user', set the given 'request' in the given 'queue'.
2.706 - """
2.707 -
2.708 - filename = self.get_object_in_store(user, queue)
2.709 - if not filename:
2.710 - return False
2.711 -
2.712 - self.acquire_lock(user)
2.713 - try:
2.714 - f = codecs.open(filename, "ab", encoding="utf-8")
2.715 - try:
2.716 - self._set_table_item(f, request, [(1, ""), (2, "")])
2.717 - finally:
2.718 - f.close()
2.719 - fix_permissions(filename)
2.720 - finally:
2.721 - self.release_lock(user)
2.722 -
2.723 - return True
2.724 -
2.725 - def set_request(self, user, uid, recurrenceid=None, type=None):
2.726 -
2.727 - """
2.728 - For the given 'user', set the queued 'uid' and 'recurrenceid',
2.729 - indicating a request, along with any given 'type'.
2.730 - """
2.731 -
2.732 - return self._set_request(user, (uid, recurrenceid, type), "requests")
2.733 -
2.734 - def queue_request(self, user, uid, recurrenceid=None, type=None):
2.735 -
2.736 - """
2.737 - Queue a request for 'user' having the given 'uid'. If the optional
2.738 - 'recurrenceid' is specified, the entry refers to a specific instance
2.739 - or occurrence of an event. The 'type' parameter can be used to indicate
2.740 - a specific type of request.
2.741 - """
2.742 -
2.743 - requests = self.get_requests(user) or []
2.744 -
2.745 - if not self.have_request(requests, uid, recurrenceid):
2.746 - return self.set_request(user, uid, recurrenceid, type)
2.747 -
2.748 - return False
2.749 -
2.750 - def dequeue_request(self, user, uid, recurrenceid=None):
2.751 -
2.752 - """
2.753 - Dequeue all requests for 'user' having the given 'uid'. If the optional
2.754 - 'recurrenceid' is specified, all requests for that specific instance or
2.755 - occurrence of an event are dequeued.
2.756 - """
2.757 -
2.758 - requests = self.get_requests(user) or []
2.759 - result = []
2.760 -
2.761 - for request in requests:
2.762 - if request[:2] != (uid, recurrenceid):
2.763 - result.append(request)
2.764 -
2.765 - self.set_requests(user, result)
2.766 - return True
2.767 -
2.768 - def has_request(self, user, uid, recurrenceid=None, type=None, strict=False):
2.769 - return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict)
2.770 -
2.771 - def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):
2.772 -
2.773 - """
2.774 - Return whether 'requests' contains a request with the given 'uid' and
2.775 - any specified 'recurrenceid' and 'type'. If 'strict' is set to a true
2.776 - value, the precise type of the request must match; otherwise, any type
2.777 - of request for the identified object may be matched.
2.778 - """
2.779 -
2.780 - for request in requests:
2.781 - if request[:2] == (uid, recurrenceid) and (
2.782 - not strict or
2.783 - not request[2:] and not type or
2.784 - request[2:] and request[2] == type):
2.785 -
2.786 - return True
2.787 -
2.788 - return False
2.789 -
2.790 - def get_counters(self, user, uid, recurrenceid=None):
2.791 -
2.792 - """
2.793 - For the given 'user', return a list of users from whom counter-proposals
2.794 - have been received for the given 'uid' and optional 'recurrenceid'.
2.795 - """
2.796 -
2.797 - filename = self.get_event_filename(user, uid, recurrenceid, "counters")
2.798 - if not filename or not isdir(filename):
2.799 - return False
2.800 -
2.801 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.802 -
2.803 - def get_counter(self, user, other, uid, recurrenceid=None):
2.804 -
2.805 - """
2.806 - For the given 'user', return the counter-proposal from 'other' for the
2.807 - given 'uid' and optional 'recurrenceid'.
2.808 - """
2.809 -
2.810 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.811 - if not filename:
2.812 - return False
2.813 -
2.814 - return self._get_object(user, filename)
2.815 -
2.816 - def set_counter(self, user, other, node, uid, recurrenceid=None):
2.817 -
2.818 - """
2.819 - For the given 'user', store a counter-proposal received from 'other' the
2.820 - given 'node' representing that proposal for the given 'uid' and
2.821 - 'recurrenceid'.
2.822 - """
2.823 -
2.824 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.825 - if not filename:
2.826 - return False
2.827 -
2.828 - return self._set_object(user, filename, node)
2.829 -
2.830 - def remove_counters(self, user, uid, recurrenceid=None):
2.831 -
2.832 - """
2.833 - For the given 'user', remove all counter-proposals associated with the
2.834 - given 'uid' and 'recurrenceid'.
2.835 - """
2.836 -
2.837 - filename = self.get_event_filename(user, uid, recurrenceid, "counters")
2.838 - if not filename or not isdir(filename):
2.839 - return False
2.840 -
2.841 - removed = False
2.842 -
2.843 - for other in listdir(filename):
2.844 - counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.845 - removed = removed or self._remove_object(counter_filename)
2.846 -
2.847 - return removed
2.848 -
2.849 - def remove_counter(self, user, other, uid, recurrenceid=None):
2.850 -
2.851 - """
2.852 - For the given 'user', remove any counter-proposal from 'other'
2.853 - associated with the given 'uid' and 'recurrenceid'.
2.854 - """
2.855 -
2.856 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.857 - if not filename or not isfile(filename):
2.858 - return False
2.859 -
2.860 - return self._remove_object(filename)
2.861 -
2.862 - # Event cancellation.
2.863 -
2.864 - def cancel_event(self, user, uid, recurrenceid=None):
2.865 -
2.866 - """
2.867 - Cancel an event for 'user' having the given 'uid'. If the optional
2.868 - 'recurrenceid' is specified, a specific instance or occurrence of an
2.869 - event is cancelled.
2.870 - """
2.871 -
2.872 - filename = self.get_event_filename(user, uid, recurrenceid)
2.873 - cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
2.874 -
2.875 - if filename and cancelled_filename and isfile(filename):
2.876 - return self.move_object(filename, cancelled_filename)
2.877 -
2.878 - return False
2.879 -
2.880 - def uncancel_event(self, user, uid, recurrenceid=None):
2.881 -
2.882 - """
2.883 - Uncancel an event for 'user' having the given 'uid'. If the optional
2.884 - 'recurrenceid' is specified, a specific instance or occurrence of an
2.885 - event is uncancelled.
2.886 - """
2.887 -
2.888 - filename = self.get_event_filename(user, uid, recurrenceid)
2.889 - cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
2.890 -
2.891 - if filename and cancelled_filename and isfile(cancelled_filename):
2.892 - return self.move_object(cancelled_filename, filename)
2.893 -
2.894 - return False
2.895 -
2.896 - def remove_cancellations(self, user, uid, recurrenceid=None):
2.897 -
2.898 - """
2.899 - Remove cancellations for 'user' for any event having the given 'uid'. If
2.900 - the optional 'recurrenceid' is specified, a specific instance or
2.901 - occurrence of an event is affected.
2.902 - """
2.903 -
2.904 - # Remove all recurrence cancellations if a general event is indicated.
2.905 -
2.906 - if not recurrenceid:
2.907 - for _recurrenceid in self.get_cancelled_recurrences(user, uid):
2.908 - self.remove_cancellation(user, uid, _recurrenceid)
2.909 -
2.910 - return self.remove_cancellation(user, uid, recurrenceid)
2.911 -
2.912 - def remove_cancellation(self, user, uid, recurrenceid=None):
2.913 -
2.914 - """
2.915 - Remove a cancellation for 'user' for the event having the given 'uid'.
2.916 - If the optional 'recurrenceid' is specified, a specific instance or
2.917 - occurrence of an event is affected.
2.918 - """
2.919 -
2.920 - # Remove any parent event cancellation or a specific recurrence
2.921 - # cancellation if indicated.
2.922 -
2.923 - filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
2.924 -
2.925 - if filename and isfile(filename):
2.926 - return self._remove_object(filename)
2.927 -
2.928 - return False
2.929 -
2.930 -class FilePublisher(FileBase):
2.931 -
2.932 - "A publisher of objects."
2.933 -
2.934 - def __init__(self, store_dir=None):
2.935 - FileBase.__init__(self, store_dir or PUBLISH_DIR)
2.936 -
2.937 - def set_freebusy(self, user, freebusy):
2.938 -
2.939 - "For the given 'user', set 'freebusy' details."
2.940 -
2.941 - filename = self.get_object_in_store(user, "freebusy")
2.942 - if not filename:
2.943 - return False
2.944 -
2.945 - record = []
2.946 - rwrite = record.append
2.947 -
2.948 - rwrite(("ORGANIZER", {}, user))
2.949 - rwrite(("UID", {}, user))
2.950 - rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
2.951 -
2.952 - for fb in freebusy:
2.953 - if not fb.transp or fb.transp == "OPAQUE":
2.954 - rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
2.955 - map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
2.956 -
2.957 - f = open(filename, "wb")
2.958 - try:
2.959 - to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
2.960 - finally:
2.961 - f.close()
2.962 - fix_permissions(filename)
2.963 -
2.964 - return True
2.965 -
2.966 -class FileJournal(FileStoreBase):
2.967 -
2.968 - "A journal system to support quotas."
2.969 -
2.970 - def __init__(self, store_dir=None):
2.971 - FileBase.__init__(self, store_dir or JOURNAL_DIR)
2.972 -
2.973 - # Quota and user identity/group discovery.
2.974 -
2.975 - def get_quotas(self):
2.976 -
2.977 - "Return a list of quotas."
2.978 -
2.979 - return listdir(self.store_dir)
2.980 -
2.981 - def get_quota_users(self, quota):
2.982 -
2.983 - "Return a list of quota users."
2.984 -
2.985 - filename = self.get_object_in_store(quota, "journal")
2.986 - if not filename or not isdir(filename):
2.987 - return []
2.988 -
2.989 - return listdir(filename)
2.990 -
2.991 - # Groups of users sharing quotas.
2.992 -
2.993 - def get_groups(self, quota):
2.994 -
2.995 - "Return the identity mappings for the given 'quota' as a dictionary."
2.996 -
2.997 - filename = self.get_object_in_store(quota, "groups")
2.998 - if not filename or not isfile(filename):
2.999 - return {}
2.1000 -
2.1001 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
2.1002 -
2.1003 - def get_limits(self, quota):
2.1004 -
2.1005 - """
2.1006 - Return the limits for the 'quota' as a dictionary mapping identities or
2.1007 - groups to durations.
2.1008 - """
2.1009 -
2.1010 - filename = self.get_object_in_store(quota, "limits")
2.1011 - if not filename or not isfile(filename):
2.1012 - return None
2.1013 -
2.1014 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
2.1015 -
2.1016 - # Free/busy period access for users within quota groups.
2.1017 -
2.1018 - def get_freebusy(self, quota, user):
2.1019 -
2.1020 - "Get free/busy details for the given 'quota' and 'user'."
2.1021 -
2.1022 - filename = self.get_object_in_store(quota, "freebusy", user)
2.1023 -
2.1024 - if not filename or not isfile(filename):
2.1025 - periods = []
2.1026 - else:
2.1027 - periods = map(lambda t: FreeBusyPeriod(*t),
2.1028 - self._get_table_atomic(quota, filename))
2.1029 -
2.1030 - return FreeBusyCollection(periods)
2.1031 -
2.1032 - def set_freebusy(self, quota, user, freebusy):
2.1033 -
2.1034 - "For the given 'quota' and 'user', set 'freebusy' details."
2.1035 -
2.1036 - filename = self.get_object_in_store(quota, "freebusy", user)
2.1037 - if not filename:
2.1038 - return False
2.1039 -
2.1040 - self._set_table_atomic(quota, filename,
2.1041 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
2.1042 - return True
2.1043 -
2.1044 - # Journal entry methods.
2.1045 -
2.1046 - def get_entries(self, quota, group):
2.1047 -
2.1048 - """
2.1049 - Return a list of journal entries for the given 'quota' for the indicated
2.1050 - 'group'.
2.1051 - """
2.1052 -
2.1053 - filename = self.get_object_in_store(quota, "journal", group)
2.1054 -
2.1055 - if not filename or not isfile(filename):
2.1056 - periods = []
2.1057 - else:
2.1058 - periods = map(lambda t: FreeBusyPeriod(*t),
2.1059 - self._get_table_atomic(quota, filename))
2.1060 -
2.1061 - return FreeBusyCollection(periods)
2.1062 -
2.1063 - def set_entries(self, quota, group, entries):
2.1064 -
2.1065 - """
2.1066 - For the given 'quota' and indicated 'group', set the list of journal
2.1067 - 'entries'.
2.1068 - """
2.1069 -
2.1070 - filename = self.get_object_in_store(quota, "journal", group)
2.1071 - if not filename:
2.1072 - return False
2.1073 -
2.1074 - self._set_table_atomic(quota, filename,
2.1075 - map(lambda fb: fb.as_tuple(strings_only=True), entries.periods))
2.1076 - return True
2.1077 -
2.1078 -# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/imiptools/__init__.py Sat Mar 05 23:45:47 2016 +0100
3.2 +++ b/imiptools/__init__.py Sun Mar 06 00:46:26 2016 +0100
3.3 @@ -25,7 +25,7 @@
3.4 from imiptools.content import handle_itip_part
3.5 from imiptools.data import get_address, get_addresses, get_uri
3.6 from imiptools.mail import Messenger
3.7 -import imip_store
3.8 +import imiptools.stores.file
3.9 import sys, os
3.10
3.11 # Postfix exit codes.
3.12 @@ -63,13 +63,13 @@
3.13 self.debug = False
3.14
3.15 def get_store(self):
3.16 - return imip_store.FileStore(self.store_dir)
3.17 + return imiptools.stores.file.FileStore(self.store_dir)
3.18
3.19 def get_publisher(self):
3.20 - return self.publishing_dir and imip_store.FilePublisher(self.publishing_dir) or None
3.21 + return self.publishing_dir and imiptools.stores.file.FilePublisher(self.publishing_dir) or None
3.22
3.23 def get_journal(self):
3.24 - return imip_store.FileJournal(self.journal_dir)
3.25 + return imiptools.stores.file.FileJournal(self.journal_dir)
3.26
3.27 def process(self, f, original_recipients):
3.28
4.1 --- a/imiptools/client.py Sat Mar 05 23:45:47 2016 +0100
4.2 +++ b/imiptools/client.py Sun Mar 06 00:46:26 2016 +0100
4.3 @@ -28,7 +28,7 @@
4.4 get_duration, get_timestamp
4.5 from imiptools.i18n import get_translator
4.6 from imiptools.profile import Preferences
4.7 -import imip_store
4.8 +import imiptools.stores.file
4.9
4.10 class Client:
4.11
4.12 @@ -48,11 +48,11 @@
4.13
4.14 self.user = user
4.15 self.messenger = messenger
4.16 - self.store = store or imip_store.FileStore()
4.17 - self.journal = journal or imip_store.FileJournal()
4.18 + self.store = store or imiptools.stores.file.FileStore()
4.19 + self.journal = journal or imiptools.stores.file.FileJournal()
4.20
4.21 try:
4.22 - self.publisher = publisher or imip_store.FilePublisher()
4.23 + self.publisher = publisher or imiptools.stores.file.FilePublisher()
4.24 except OSError:
4.25 self.publisher = None
4.26
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
5.2 +++ b/imiptools/stores/__init__.py Sun Mar 06 00:46:26 2016 +0100
5.3 @@ -0,0 +1,529 @@
5.4 +#!/usr/bin/env python
5.5 +
5.6 +"""
5.7 +General support for calendar data storage.
5.8 +
5.9 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
5.10 +
5.11 +This program is free software; you can redistribute it and/or modify it under
5.12 +the terms of the GNU General Public License as published by the Free Software
5.13 +Foundation; either version 3 of the License, or (at your option) any later
5.14 +version.
5.15 +
5.16 +This program is distributed in the hope that it will be useful, but WITHOUT
5.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
5.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
5.19 +details.
5.20 +
5.21 +You should have received a copy of the GNU General Public License along with
5.22 +this program. If not, see <http://www.gnu.org/licenses/>.
5.23 +"""
5.24 +
5.25 +class StoreBase:
5.26 +
5.27 + "The core operations of a data store."
5.28 +
5.29 + def acquire_lock(self, user, timeout=None):
5.30 + pass
5.31 +
5.32 + def release_lock(self, user):
5.33 + pass
5.34 +
5.35 + # User discovery.
5.36 +
5.37 + def get_users(self):
5.38 +
5.39 + "Return a list of users."
5.40 +
5.41 + pass
5.42 +
5.43 + # Event and event metadata access.
5.44 +
5.45 + def get_events(self, user):
5.46 +
5.47 + "Return a list of event identifiers."
5.48 +
5.49 + pass
5.50 +
5.51 + def get_all_events(self, user):
5.52 +
5.53 + "Return a set of (uid, recurrenceid) tuples for all events."
5.54 +
5.55 + uids = self.get_events(user)
5.56 + if not uids:
5.57 + return set()
5.58 +
5.59 + all_events = set()
5.60 + for uid in uids:
5.61 + all_events.add((uid, None))
5.62 + all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
5.63 +
5.64 + return all_events
5.65 +
5.66 + def get_event(self, user, uid, recurrenceid=None, dirname=None):
5.67 +
5.68 + """
5.69 + Get the event for the given 'user' with the given 'uid'. If
5.70 + the optional 'recurrenceid' is specified, a specific instance or
5.71 + occurrence of an event is returned.
5.72 + """
5.73 +
5.74 + pass
5.75 +
5.76 + def get_complete_event(self, user, uid):
5.77 +
5.78 + "Get the event for the given 'user' with the given 'uid'."
5.79 +
5.80 + pass
5.81 +
5.82 + def set_event(self, user, uid, recurrenceid, node):
5.83 +
5.84 + """
5.85 + Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
5.86 + if the latter is specified, a specific instance or occurrence of an
5.87 + event is referenced), using the given 'node' description.
5.88 + """
5.89 +
5.90 + if recurrenceid:
5.91 + return self.set_recurrence(user, uid, recurrenceid, node)
5.92 + else:
5.93 + return self.set_complete_event(user, uid, node)
5.94 +
5.95 + def set_complete_event(self, user, uid, node):
5.96 +
5.97 + "Set an event for 'user' having the given 'uid' and 'node'."
5.98 +
5.99 + pass
5.100 +
5.101 + def remove_event(self, user, uid, recurrenceid=None):
5.102 +
5.103 + """
5.104 + Remove an event for 'user' having the given 'uid'. If the optional
5.105 + 'recurrenceid' is specified, a specific instance or occurrence of an
5.106 + event is removed.
5.107 + """
5.108 +
5.109 + if recurrenceid:
5.110 + return self.remove_recurrence(user, uid, recurrenceid)
5.111 + else:
5.112 + for recurrenceid in self.get_recurrences(user, uid) or []:
5.113 + self.remove_recurrence(user, uid, recurrenceid)
5.114 + return self.remove_complete_event(user, uid)
5.115 +
5.116 + def remove_complete_event(self, user, uid):
5.117 +
5.118 + "Remove an event for 'user' having the given 'uid'."
5.119 +
5.120 + self.remove_recurrences(user, uid)
5.121 + return self.remove_parent_event(user, uid)
5.122 +
5.123 + def remove_parent_event(self, user, uid):
5.124 +
5.125 + "Remove the parent event for 'user' having the given 'uid'."
5.126 +
5.127 + pass
5.128 +
5.129 + def get_recurrences(self, user, uid):
5.130 +
5.131 + """
5.132 + Get additional event instances for an event of the given 'user' with the
5.133 + indicated 'uid'. Both active and cancelled recurrences are returned.
5.134 + """
5.135 +
5.136 + return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
5.137 +
5.138 + def get_active_recurrences(self, user, uid):
5.139 +
5.140 + """
5.141 + Get additional event instances for an event of the given 'user' with the
5.142 + indicated 'uid'. Cancelled recurrences are not returned.
5.143 + """
5.144 +
5.145 + pass
5.146 +
5.147 + def get_cancelled_recurrences(self, user, uid):
5.148 +
5.149 + """
5.150 + Get additional event instances for an event of the given 'user' with the
5.151 + indicated 'uid'. Only cancelled recurrences are returned.
5.152 + """
5.153 +
5.154 + pass
5.155 +
5.156 + def get_recurrence(self, user, uid, recurrenceid):
5.157 +
5.158 + """
5.159 + For the event of the given 'user' with the given 'uid', return the
5.160 + specific recurrence indicated by the 'recurrenceid'.
5.161 + """
5.162 +
5.163 + pass
5.164 +
5.165 + def set_recurrence(self, user, uid, recurrenceid, node):
5.166 +
5.167 + "Set an event for 'user' having the given 'uid' and 'node'."
5.168 +
5.169 + pass
5.170 +
5.171 + def remove_recurrence(self, user, uid, recurrenceid):
5.172 +
5.173 + """
5.174 + Remove a special recurrence from an event stored by 'user' having the
5.175 + given 'uid' and 'recurrenceid'.
5.176 + """
5.177 +
5.178 + pass
5.179 +
5.180 + def remove_recurrences(self, user, uid):
5.181 +
5.182 + """
5.183 + Remove all recurrences for an event stored by 'user' having the given
5.184 + 'uid'.
5.185 + """
5.186 +
5.187 + for recurrenceid in self.get_recurrences(user, uid):
5.188 + self.remove_recurrence(user, uid, recurrenceid)
5.189 +
5.190 + return self.remove_recurrence_collection(user, uid)
5.191 +
5.192 + def remove_recurrence_collection(self, user, uid):
5.193 +
5.194 + """
5.195 + Remove the collection of recurrences stored by 'user' having the given
5.196 + 'uid'.
5.197 + """
5.198 +
5.199 + pass
5.200 +
5.201 + # Free/busy period providers, upon extension of the free/busy records.
5.202 +
5.203 + def get_freebusy_providers(self, user, dt=None):
5.204 +
5.205 + """
5.206 + Return a set of uncancelled events of the form (uid, recurrenceid)
5.207 + providing free/busy details beyond the given datetime 'dt'.
5.208 +
5.209 + If 'dt' is not specified, all events previously found to provide
5.210 + details will be returned. Otherwise, if 'dt' is earlier than the
5.211 + datetime recorded for the known providers, None is returned, indicating
5.212 + that the list of providers must be recomputed.
5.213 +
5.214 + This function returns a list of (uid, recurrenceid) tuples upon success.
5.215 + """
5.216 +
5.217 + pass
5.218 +
5.219 + def set_freebusy_providers(self, user, dt, providers):
5.220 +
5.221 + """
5.222 + Define the uncancelled events providing free/busy details beyond the
5.223 + given datetime 'dt'.
5.224 + """
5.225 +
5.226 + pass
5.227 +
5.228 + def append_freebusy_provider(self, user, provider):
5.229 +
5.230 + "For the given 'user', append the free/busy 'provider'."
5.231 +
5.232 + pass
5.233 +
5.234 + def remove_freebusy_provider(self, user, provider):
5.235 +
5.236 + "For the given 'user', remove the free/busy 'provider'."
5.237 +
5.238 + pass
5.239 +
5.240 + # Free/busy period access.
5.241 +
5.242 + def get_freebusy(self, user, name=None):
5.243 +
5.244 + "Get free/busy details for the given 'user'."
5.245 +
5.246 + pass
5.247 +
5.248 + def get_freebusy_for_other(self, user, other):
5.249 +
5.250 + "For the given 'user', get free/busy details for the 'other' user."
5.251 +
5.252 + pass
5.253 +
5.254 + def set_freebusy(self, user, freebusy, name=None):
5.255 +
5.256 + "For the given 'user', set 'freebusy' details."
5.257 +
5.258 + pass
5.259 +
5.260 + def set_freebusy_for_other(self, user, freebusy, other):
5.261 +
5.262 + "For the given 'user', set 'freebusy' details for the 'other' user."
5.263 +
5.264 + pass
5.265 +
5.266 + # Tentative free/busy periods related to countering.
5.267 +
5.268 + def get_freebusy_offers(self, user):
5.269 +
5.270 + "Get free/busy offers for the given 'user'."
5.271 +
5.272 + pass
5.273 +
5.274 + def set_freebusy_offers(self, user, freebusy):
5.275 +
5.276 + "For the given 'user', set 'freebusy' offers."
5.277 +
5.278 + return self.set_freebusy(user, freebusy, "freebusy-offers")
5.279 +
5.280 + # Requests and counter-proposals.
5.281 +
5.282 + def get_requests(self, user):
5.283 +
5.284 + "Get requests for the given 'user'."
5.285 +
5.286 + pass
5.287 +
5.288 + def set_requests(self, user, requests):
5.289 +
5.290 + "For the given 'user', set the list of queued 'requests'."
5.291 +
5.292 + pass
5.293 +
5.294 + def set_request(self, user, uid, recurrenceid=None, type=None):
5.295 +
5.296 + """
5.297 + For the given 'user', set the queued 'uid' and 'recurrenceid',
5.298 + indicating a request, along with any given 'type'.
5.299 + """
5.300 +
5.301 + pass
5.302 +
5.303 + def queue_request(self, user, uid, recurrenceid=None, type=None):
5.304 +
5.305 + """
5.306 + Queue a request for 'user' having the given 'uid'. If the optional
5.307 + 'recurrenceid' is specified, the entry refers to a specific instance
5.308 + or occurrence of an event. The 'type' parameter can be used to indicate
5.309 + a specific type of request.
5.310 + """
5.311 +
5.312 + requests = self.get_requests(user) or []
5.313 +
5.314 + if not self.have_request(requests, uid, recurrenceid):
5.315 + return self.set_request(user, uid, recurrenceid, type)
5.316 +
5.317 + return False
5.318 +
5.319 + def dequeue_request(self, user, uid, recurrenceid=None):
5.320 +
5.321 + """
5.322 + Dequeue all requests for 'user' having the given 'uid'. If the optional
5.323 + 'recurrenceid' is specified, all requests for that specific instance or
5.324 + occurrence of an event are dequeued.
5.325 + """
5.326 +
5.327 + requests = self.get_requests(user) or []
5.328 + result = []
5.329 +
5.330 + for request in requests:
5.331 + if request[:2] != (uid, recurrenceid):
5.332 + result.append(request)
5.333 +
5.334 + self.set_requests(user, result)
5.335 + return True
5.336 +
5.337 + def has_request(self, user, uid, recurrenceid=None, type=None, strict=False):
5.338 + return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict)
5.339 +
5.340 + def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):
5.341 +
5.342 + """
5.343 + Return whether 'requests' contains a request with the given 'uid' and
5.344 + any specified 'recurrenceid' and 'type'. If 'strict' is set to a true
5.345 + value, the precise type of the request must match; otherwise, any type
5.346 + of request for the identified object may be matched.
5.347 + """
5.348 +
5.349 + for request in requests:
5.350 + if request[:2] == (uid, recurrenceid) and (
5.351 + not strict or
5.352 + not request[2:] and not type or
5.353 + request[2:] and request[2] == type):
5.354 +
5.355 + return True
5.356 +
5.357 + return False
5.358 +
5.359 + def get_counters(self, user, uid, recurrenceid=None):
5.360 +
5.361 + """
5.362 + For the given 'user', return a list of users from whom counter-proposals
5.363 + have been received for the given 'uid' and optional 'recurrenceid'.
5.364 + """
5.365 +
5.366 + pass
5.367 +
5.368 + def get_counter(self, user, other, uid, recurrenceid=None):
5.369 +
5.370 + """
5.371 + For the given 'user', return the counter-proposal from 'other' for the
5.372 + given 'uid' and optional 'recurrenceid'.
5.373 + """
5.374 +
5.375 + pass
5.376 +
5.377 + def set_counter(self, user, other, node, uid, recurrenceid=None):
5.378 +
5.379 + """
5.380 + For the given 'user', store a counter-proposal received from 'other' the
5.381 + given 'node' representing that proposal for the given 'uid' and
5.382 + 'recurrenceid'.
5.383 + """
5.384 +
5.385 + pass
5.386 +
5.387 + def remove_counters(self, user, uid, recurrenceid=None):
5.388 +
5.389 + """
5.390 + For the given 'user', remove all counter-proposals associated with the
5.391 + given 'uid' and 'recurrenceid'.
5.392 + """
5.393 +
5.394 + pass
5.395 +
5.396 + def remove_counter(self, user, other, uid, recurrenceid=None):
5.397 +
5.398 + """
5.399 + For the given 'user', remove any counter-proposal from 'other'
5.400 + associated with the given 'uid' and 'recurrenceid'.
5.401 + """
5.402 +
5.403 + pass
5.404 +
5.405 + # Event cancellation.
5.406 +
5.407 + def cancel_event(self, user, uid, recurrenceid=None):
5.408 +
5.409 + """
5.410 + Cancel an event for 'user' having the given 'uid'. If the optional
5.411 + 'recurrenceid' is specified, a specific instance or occurrence of an
5.412 + event is cancelled.
5.413 + """
5.414 +
5.415 + pass
5.416 +
5.417 + def uncancel_event(self, user, uid, recurrenceid=None):
5.418 +
5.419 + """
5.420 + Uncancel an event for 'user' having the given 'uid'. If the optional
5.421 + 'recurrenceid' is specified, a specific instance or occurrence of an
5.422 + event is uncancelled.
5.423 + """
5.424 +
5.425 + pass
5.426 +
5.427 + def remove_cancellations(self, user, uid, recurrenceid=None):
5.428 +
5.429 + """
5.430 + Remove cancellations for 'user' for any event having the given 'uid'. If
5.431 + the optional 'recurrenceid' is specified, a specific instance or
5.432 + occurrence of an event is affected.
5.433 + """
5.434 +
5.435 + # Remove all recurrence cancellations if a general event is indicated.
5.436 +
5.437 + if not recurrenceid:
5.438 + for _recurrenceid in self.get_cancelled_recurrences(user, uid):
5.439 + self.remove_cancellation(user, uid, _recurrenceid)
5.440 +
5.441 + return self.remove_cancellation(user, uid, recurrenceid)
5.442 +
5.443 + def remove_cancellation(self, user, uid, recurrenceid=None):
5.444 +
5.445 + """
5.446 + Remove a cancellation for 'user' for the event having the given 'uid'.
5.447 + If the optional 'recurrenceid' is specified, a specific instance or
5.448 + occurrence of an event is affected.
5.449 + """
5.450 +
5.451 + pass
5.452 +
5.453 +class PublisherBase:
5.454 +
5.455 + "The core operations of a data publisher."
5.456 +
5.457 + def set_freebusy(self, user, freebusy):
5.458 +
5.459 + "For the given 'user', set 'freebusy' details."
5.460 +
5.461 + pass
5.462 +
5.463 +class JournalBase:
5.464 +
5.465 + "The core operations of a journal system supporting quotas."
5.466 +
5.467 + # Quota and user identity/group discovery.
5.468 +
5.469 + def get_quotas(self):
5.470 +
5.471 + "Return a list of quotas."
5.472 +
5.473 + pass
5.474 +
5.475 + def get_quota_users(self, quota):
5.476 +
5.477 + "Return a list of quota users."
5.478 +
5.479 + pass
5.480 +
5.481 + # Groups of users sharing quotas.
5.482 +
5.483 + def get_groups(self, quota):
5.484 +
5.485 + "Return the identity mappings for the given 'quota' as a dictionary."
5.486 +
5.487 + pass
5.488 +
5.489 + def get_limits(self, quota):
5.490 +
5.491 + """
5.492 + Return the limits for the 'quota' as a dictionary mapping identities or
5.493 + groups to durations.
5.494 + """
5.495 +
5.496 + pass
5.497 +
5.498 + # Free/busy period access for users within quota groups.
5.499 +
5.500 + def get_freebusy(self, quota, user):
5.501 +
5.502 + "Get free/busy details for the given 'quota' and 'user'."
5.503 +
5.504 + pass
5.505 +
5.506 + def set_freebusy(self, quota, user, freebusy):
5.507 +
5.508 + "For the given 'quota' and 'user', set 'freebusy' details."
5.509 +
5.510 + pass
5.511 +
5.512 + # Journal entry methods.
5.513 +
5.514 + def get_entries(self, quota, group):
5.515 +
5.516 + """
5.517 + Return a list of journal entries for the given 'quota' for the indicated
5.518 + 'group'.
5.519 + """
5.520 +
5.521 + pass
5.522 +
5.523 + def set_entries(self, quota, group, entries):
5.524 +
5.525 + """
5.526 + For the given 'quota' and indicated 'group', set the list of journal
5.527 + 'entries'.
5.528 + """
5.529 +
5.530 + pass
5.531 +
5.532 +# vim: tabstop=4 expandtab shiftwidth=4
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
6.2 +++ b/imiptools/stores/file.py Sun Mar 06 00:46:26 2016 +0100
6.3 @@ -0,0 +1,956 @@
6.4 +#!/usr/bin/env python
6.5 +
6.6 +"""
6.7 +A simple filesystem-based store of calendar data.
6.8 +
6.9 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
6.10 +
6.11 +This program is free software; you can redistribute it and/or modify it under
6.12 +the terms of the GNU General Public License as published by the Free Software
6.13 +Foundation; either version 3 of the License, or (at your option) any later
6.14 +version.
6.15 +
6.16 +This program is distributed in the hope that it will be useful, but WITHOUT
6.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
6.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
6.19 +details.
6.20 +
6.21 +You should have received a copy of the GNU General Public License along with
6.22 +this program. If not, see <http://www.gnu.org/licenses/>.
6.23 +"""
6.24 +
6.25 +from imiptools.stores import StoreBase, PublisherBase, JournalBase
6.26 +
6.27 +from datetime import datetime
6.28 +from imiptools.config import STORE_DIR, PUBLISH_DIR, JOURNAL_DIR
6.29 +from imiptools.data import make_calendar, parse_object, to_stream
6.30 +from imiptools.dates import format_datetime, get_datetime, to_timezone
6.31 +from imiptools.filesys import fix_permissions, FileBase
6.32 +from imiptools.period import FreeBusyPeriod, FreeBusyCollection
6.33 +from imiptools.text import parse_line
6.34 +from os.path import isdir, isfile, join
6.35 +from os import listdir, remove, rmdir
6.36 +import codecs
6.37 +
6.38 +class FileStoreBase(FileBase):
6.39 +
6.40 + "A file store supporting user-specific locking and tabular data."
6.41 +
6.42 + def acquire_lock(self, user, timeout=None):
6.43 + FileBase.acquire_lock(self, timeout, user)
6.44 +
6.45 + def release_lock(self, user):
6.46 + FileBase.release_lock(self, user)
6.47 +
6.48 + # Utility methods.
6.49 +
6.50 + def _set_defaults(self, t, empty_defaults):
6.51 + for i, default in empty_defaults:
6.52 + if i >= len(t):
6.53 + t += [None] * (i - len(t) + 1)
6.54 + if not t[i]:
6.55 + t[i] = default
6.56 + return t
6.57 +
6.58 + def _get_table(self, user, filename, empty_defaults=None, tab_separated=True):
6.59 +
6.60 + """
6.61 + From the file for the given 'user' having the given 'filename', return
6.62 + a list of tuples representing the file's contents.
6.63 +
6.64 + The 'empty_defaults' is a list of (index, value) tuples indicating the
6.65 + default value where a column either does not exist or provides an empty
6.66 + value.
6.67 +
6.68 + If 'tab_separated' is specified and is a false value, line parsing using
6.69 + the imiptools.text.parse_line function will be performed instead of
6.70 + splitting each line of the file using tab characters as separators.
6.71 + """
6.72 +
6.73 + f = codecs.open(filename, "rb", encoding="utf-8")
6.74 + try:
6.75 + l = []
6.76 + for line in f.readlines():
6.77 + line = line.strip(" \r\n")
6.78 + if tab_separated:
6.79 + t = line.split("\t")
6.80 + else:
6.81 + t = parse_line(line)
6.82 + if empty_defaults:
6.83 + t = self._set_defaults(t, empty_defaults)
6.84 + l.append(tuple(t))
6.85 + return l
6.86 + finally:
6.87 + f.close()
6.88 +
6.89 + def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True):
6.90 +
6.91 + """
6.92 + From the file for the given 'user' having the given 'filename', return
6.93 + a list of tuples representing the file's contents.
6.94 +
6.95 + The 'empty_defaults' is a list of (index, value) tuples indicating the
6.96 + default value where a column either does not exist or provides an empty
6.97 + value.
6.98 +
6.99 + If 'tab_separated' is specified and is a false value, line parsing using
6.100 + the imiptools.text.parse_line function will be performed instead of
6.101 + splitting each line of the file using tab characters as separators.
6.102 + """
6.103 +
6.104 + self.acquire_lock(user)
6.105 + try:
6.106 + return self._get_table(user, filename, empty_defaults, tab_separated)
6.107 + finally:
6.108 + self.release_lock(user)
6.109 +
6.110 + def _set_table(self, user, filename, items, empty_defaults=None):
6.111 +
6.112 + """
6.113 + For the given 'user', write to the file having the given 'filename' the
6.114 + 'items'.
6.115 +
6.116 + The 'empty_defaults' is a list of (index, value) tuples indicating the
6.117 + default value where a column either does not exist or provides an empty
6.118 + value.
6.119 + """
6.120 +
6.121 + f = codecs.open(filename, "wb", encoding="utf-8")
6.122 + try:
6.123 + for item in items:
6.124 + self._set_table_item(f, item, empty_defaults)
6.125 + finally:
6.126 + f.close()
6.127 + fix_permissions(filename)
6.128 +
6.129 + def _set_table_item(self, f, item, empty_defaults=None):
6.130 +
6.131 + "Set in table 'f' the given 'item', using any 'empty_defaults'."
6.132 +
6.133 + if empty_defaults:
6.134 + item = self._set_defaults(list(item), empty_defaults)
6.135 + f.write("\t".join(item) + "\n")
6.136 +
6.137 + def _set_table_atomic(self, user, filename, items, empty_defaults=None):
6.138 +
6.139 + """
6.140 + For the given 'user', write to the file having the given 'filename' the
6.141 + 'items'.
6.142 +
6.143 + The 'empty_defaults' is a list of (index, value) tuples indicating the
6.144 + default value where a column either does not exist or provides an empty
6.145 + value.
6.146 + """
6.147 +
6.148 + self.acquire_lock(user)
6.149 + try:
6.150 + self._set_table(user, filename, items, empty_defaults)
6.151 + finally:
6.152 + self.release_lock(user)
6.153 +
6.154 +class FileStore(FileStoreBase, StoreBase):
6.155 +
6.156 + "A file store of tabular free/busy data and objects."
6.157 +
6.158 + def __init__(self, store_dir=None):
6.159 + FileBase.__init__(self, store_dir or STORE_DIR)
6.160 +
6.161 + # Store object access.
6.162 +
6.163 + def _get_object(self, user, filename):
6.164 +
6.165 + """
6.166 + Return the parsed object for the given 'user' having the given
6.167 + 'filename'.
6.168 + """
6.169 +
6.170 + self.acquire_lock(user)
6.171 + try:
6.172 + f = open(filename, "rb")
6.173 + try:
6.174 + return parse_object(f, "utf-8")
6.175 + finally:
6.176 + f.close()
6.177 + finally:
6.178 + self.release_lock(user)
6.179 +
6.180 + def _set_object(self, user, filename, node):
6.181 +
6.182 + """
6.183 + Set an object for the given 'user' having the given 'filename', using
6.184 + 'node' to define the object.
6.185 + """
6.186 +
6.187 + self.acquire_lock(user)
6.188 + try:
6.189 + f = open(filename, "wb")
6.190 + try:
6.191 + to_stream(f, node)
6.192 + finally:
6.193 + f.close()
6.194 + fix_permissions(filename)
6.195 + finally:
6.196 + self.release_lock(user)
6.197 +
6.198 + return True
6.199 +
6.200 + def _remove_object(self, filename):
6.201 +
6.202 + "Remove the object with the given 'filename'."
6.203 +
6.204 + try:
6.205 + remove(filename)
6.206 + except OSError:
6.207 + return False
6.208 +
6.209 + return True
6.210 +
6.211 + def _remove_collection(self, filename):
6.212 +
6.213 + "Remove the collection with the given 'filename'."
6.214 +
6.215 + try:
6.216 + rmdir(filename)
6.217 + except OSError:
6.218 + return False
6.219 +
6.220 + return True
6.221 +
6.222 + # User discovery.
6.223 +
6.224 + def get_users(self):
6.225 +
6.226 + "Return a list of users."
6.227 +
6.228 + return listdir(self.store_dir)
6.229 +
6.230 + # Event and event metadata access.
6.231 +
6.232 + def get_events(self, user):
6.233 +
6.234 + "Return a list of event identifiers."
6.235 +
6.236 + filename = self.get_object_in_store(user, "objects")
6.237 + if not filename or not isdir(filename):
6.238 + return None
6.239 +
6.240 + return [name for name in listdir(filename) if isfile(join(filename, name))]
6.241 +
6.242 + def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
6.243 +
6.244 + """
6.245 + Get the filename providing the event for the given 'user' with the given
6.246 + 'uid'. If the optional 'recurrenceid' is specified, a specific instance
6.247 + or occurrence of an event is returned.
6.248 +
6.249 + Where 'dirname' is specified, the given directory name is used as the
6.250 + base of the location within which any filename will reside.
6.251 + """
6.252 +
6.253 + if recurrenceid:
6.254 + return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
6.255 + else:
6.256 + return self.get_complete_event_filename(user, uid, dirname, username)
6.257 +
6.258 + def get_event(self, user, uid, recurrenceid=None, dirname=None):
6.259 +
6.260 + """
6.261 + Get the event for the given 'user' with the given 'uid'. If
6.262 + the optional 'recurrenceid' is specified, a specific instance or
6.263 + occurrence of an event is returned.
6.264 + """
6.265 +
6.266 + filename = self.get_event_filename(user, uid, recurrenceid, dirname)
6.267 + if not filename or not isfile(filename):
6.268 + return None
6.269 +
6.270 + return filename and self._get_object(user, filename)
6.271 +
6.272 + def get_complete_event_filename(self, user, uid, dirname=None, username=None):
6.273 +
6.274 + """
6.275 + Get the filename providing the event for the given 'user' with the given
6.276 + 'uid'.
6.277 +
6.278 + Where 'dirname' is specified, the given directory name is used as the
6.279 + base of the location within which any filename will reside.
6.280 +
6.281 + Where 'username' is specified, the event details will reside in a file
6.282 + bearing that name within a directory having 'uid' as its name.
6.283 + """
6.284 +
6.285 + return self.get_object_in_store(user, dirname, "objects", uid, username)
6.286 +
6.287 + def get_complete_event(self, user, uid):
6.288 +
6.289 + "Get the event for the given 'user' with the given 'uid'."
6.290 +
6.291 + filename = self.get_complete_event_filename(user, uid)
6.292 + if not filename or not isfile(filename):
6.293 + return None
6.294 +
6.295 + return filename and self._get_object(user, filename)
6.296 +
6.297 + def set_complete_event(self, user, uid, node):
6.298 +
6.299 + "Set an event for 'user' having the given 'uid' and 'node'."
6.300 +
6.301 + filename = self.get_object_in_store(user, "objects", uid)
6.302 + if not filename:
6.303 + return False
6.304 +
6.305 + return self._set_object(user, filename, node)
6.306 +
6.307 + def remove_parent_event(self, user, uid):
6.308 +
6.309 + "Remove the parent event for 'user' having the given 'uid'."
6.310 +
6.311 + filename = self.get_object_in_store(user, "objects", uid)
6.312 + if not filename:
6.313 + return False
6.314 +
6.315 + return self._remove_object(filename)
6.316 +
6.317 + def get_recurrences(self, user, uid):
6.318 +
6.319 + """
6.320 + Get additional event instances for an event of the given 'user' with the
6.321 + indicated 'uid'. Both active and cancelled recurrences are returned.
6.322 + """
6.323 +
6.324 + return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
6.325 +
6.326 + def get_active_recurrences(self, user, uid):
6.327 +
6.328 + """
6.329 + Get additional event instances for an event of the given 'user' with the
6.330 + indicated 'uid'. Cancelled recurrences are not returned.
6.331 + """
6.332 +
6.333 + filename = self.get_object_in_store(user, "recurrences", uid)
6.334 + if not filename or not isdir(filename):
6.335 + return []
6.336 +
6.337 + return [name for name in listdir(filename) if isfile(join(filename, name))]
6.338 +
6.339 + def get_cancelled_recurrences(self, user, uid):
6.340 +
6.341 + """
6.342 + Get additional event instances for an event of the given 'user' with the
6.343 + indicated 'uid'. Only cancelled recurrences are returned.
6.344 + """
6.345 +
6.346 + filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
6.347 + if not filename or not isdir(filename):
6.348 + return []
6.349 +
6.350 + return [name for name in listdir(filename) if isfile(join(filename, name))]
6.351 +
6.352 + def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
6.353 +
6.354 + """
6.355 + For the event of the given 'user' with the given 'uid', return the
6.356 + filename providing the recurrence with the given 'recurrenceid'.
6.357 +
6.358 + Where 'dirname' is specified, the given directory name is used as the
6.359 + base of the location within which any filename will reside.
6.360 +
6.361 + Where 'username' is specified, the event details will reside in a file
6.362 + bearing that name within a directory having 'uid' as its name.
6.363 + """
6.364 +
6.365 + return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
6.366 +
6.367 + def get_recurrence(self, user, uid, recurrenceid):
6.368 +
6.369 + """
6.370 + For the event of the given 'user' with the given 'uid', return the
6.371 + specific recurrence indicated by the 'recurrenceid'.
6.372 + """
6.373 +
6.374 + filename = self.get_recurrence_filename(user, uid, recurrenceid)
6.375 + if not filename or not isfile(filename):
6.376 + return None
6.377 +
6.378 + return filename and self._get_object(user, filename)
6.379 +
6.380 + def set_recurrence(self, user, uid, recurrenceid, node):
6.381 +
6.382 + "Set an event for 'user' having the given 'uid' and 'node'."
6.383 +
6.384 + filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
6.385 + if not filename:
6.386 + return False
6.387 +
6.388 + return self._set_object(user, filename, node)
6.389 +
6.390 + def remove_recurrence(self, user, uid, recurrenceid):
6.391 +
6.392 + """
6.393 + Remove a special recurrence from an event stored by 'user' having the
6.394 + given 'uid' and 'recurrenceid'.
6.395 + """
6.396 +
6.397 + filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
6.398 + if not filename:
6.399 + return False
6.400 +
6.401 + return self._remove_object(filename)
6.402 +
6.403 + def remove_recurrence_collection(self, user, uid):
6.404 +
6.405 + """
6.406 + Remove the collection of recurrences stored by 'user' having the given
6.407 + 'uid'.
6.408 + """
6.409 +
6.410 + recurrences = self.get_object_in_store(user, "recurrences", uid)
6.411 + if recurrences:
6.412 + return self._remove_collection(recurrences)
6.413 +
6.414 + return True
6.415 +
6.416 + # Free/busy period providers, upon extension of the free/busy records.
6.417 +
6.418 + def _get_freebusy_providers(self, user):
6.419 +
6.420 + """
6.421 + Return the free/busy providers for the given 'user'.
6.422 +
6.423 + This function returns any stored datetime and a list of providers as a
6.424 + 2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
6.425 + """
6.426 +
6.427 + filename = self.get_object_in_store(user, "freebusy-providers")
6.428 + if not filename or not isfile(filename):
6.429 + return None
6.430 +
6.431 + # Attempt to read providers, with a declaration of the datetime
6.432 + # from which such providers are considered as still being active.
6.433 +
6.434 + t = self._get_table_atomic(user, filename, [(1, None)])
6.435 + try:
6.436 + dt_string = t[0][0]
6.437 + except IndexError:
6.438 + return None
6.439 +
6.440 + return dt_string, t[1:]
6.441 +
6.442 + def get_freebusy_providers(self, user, dt=None):
6.443 +
6.444 + """
6.445 + Return a set of uncancelled events of the form (uid, recurrenceid)
6.446 + providing free/busy details beyond the given datetime 'dt'.
6.447 +
6.448 + If 'dt' is not specified, all events previously found to provide
6.449 + details will be returned. Otherwise, if 'dt' is earlier than the
6.450 + datetime recorded for the known providers, None is returned, indicating
6.451 + that the list of providers must be recomputed.
6.452 +
6.453 + This function returns a list of (uid, recurrenceid) tuples upon success.
6.454 + """
6.455 +
6.456 + t = self._get_freebusy_providers(user)
6.457 + if not t:
6.458 + return None
6.459 +
6.460 + dt_string, t = t
6.461 +
6.462 + # If the requested datetime is earlier than the stated datetime, the
6.463 + # providers will need to be recomputed.
6.464 +
6.465 + if dt:
6.466 + providers_dt = get_datetime(dt_string)
6.467 + if not providers_dt or providers_dt > dt:
6.468 + return None
6.469 +
6.470 + # Otherwise, return the providers.
6.471 +
6.472 + return t[1:]
6.473 +
6.474 + def _set_freebusy_providers(self, user, dt_string, t):
6.475 +
6.476 + "Set the given provider timestamp 'dt_string' and table 't'."
6.477 +
6.478 + filename = self.get_object_in_store(user, "freebusy-providers")
6.479 + if not filename:
6.480 + return False
6.481 +
6.482 + t.insert(0, (dt_string,))
6.483 + self._set_table_atomic(user, filename, t, [(1, "")])
6.484 + return True
6.485 +
6.486 + def set_freebusy_providers(self, user, dt, providers):
6.487 +
6.488 + """
6.489 + Define the uncancelled events providing free/busy details beyond the
6.490 + given datetime 'dt'.
6.491 + """
6.492 +
6.493 + t = []
6.494 +
6.495 + for obj in providers:
6.496 + t.append((obj.get_uid(), obj.get_recurrenceid()))
6.497 +
6.498 + return self._set_freebusy_providers(user, format_datetime(dt), t)
6.499 +
6.500 + def append_freebusy_provider(self, user, provider):
6.501 +
6.502 + "For the given 'user', append the free/busy 'provider'."
6.503 +
6.504 + t = self._get_freebusy_providers(user)
6.505 + if not t:
6.506 + return False
6.507 +
6.508 + dt_string, t = t
6.509 + t.append((provider.get_uid(), provider.get_recurrenceid()))
6.510 +
6.511 + return self._set_freebusy_providers(user, dt_string, t)
6.512 +
6.513 + def remove_freebusy_provider(self, user, provider):
6.514 +
6.515 + "For the given 'user', remove the free/busy 'provider'."
6.516 +
6.517 + t = self._get_freebusy_providers(user)
6.518 + if not t:
6.519 + return False
6.520 +
6.521 + dt_string, t = t
6.522 + try:
6.523 + t.remove((provider.get_uid(), provider.get_recurrenceid()))
6.524 + except ValueError:
6.525 + return False
6.526 +
6.527 + return self._set_freebusy_providers(user, dt_string, t)
6.528 +
6.529 + # Free/busy period access.
6.530 +
6.531 + def get_freebusy(self, user, name=None):
6.532 +
6.533 + "Get free/busy details for the given 'user'."
6.534 +
6.535 + filename = self.get_object_in_store(user, name or "freebusy")
6.536 +
6.537 + if not filename or not isfile(filename):
6.538 + periods = []
6.539 + else:
6.540 + periods = map(lambda t: FreeBusyPeriod(*t),
6.541 + self._get_table_atomic(user, filename))
6.542 +
6.543 + return FreeBusyCollection(periods)
6.544 +
6.545 + def get_freebusy_for_other(self, user, other):
6.546 +
6.547 + "For the given 'user', get free/busy details for the 'other' user."
6.548 +
6.549 + filename = self.get_object_in_store(user, "freebusy-other", other)
6.550 +
6.551 + if not filename or not isfile(filename):
6.552 + periods = []
6.553 + else:
6.554 + periods = map(lambda t: FreeBusyPeriod(*t),
6.555 + self._get_table_atomic(user, filename))
6.556 +
6.557 + return FreeBusyCollection(periods)
6.558 +
6.559 + def set_freebusy(self, user, freebusy, name=None):
6.560 +
6.561 + "For the given 'user', set 'freebusy' details."
6.562 +
6.563 + filename = self.get_object_in_store(user, name or "freebusy")
6.564 + if not filename:
6.565 + return False
6.566 +
6.567 + self._set_table_atomic(user, filename,
6.568 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
6.569 + return True
6.570 +
6.571 + def set_freebusy_for_other(self, user, freebusy, other):
6.572 +
6.573 + "For the given 'user', set 'freebusy' details for the 'other' user."
6.574 +
6.575 + filename = self.get_object_in_store(user, "freebusy-other", other)
6.576 + if not filename:
6.577 + return False
6.578 +
6.579 + self._set_table_atomic(user, filename,
6.580 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
6.581 + return True
6.582 +
6.583 + # Tentative free/busy periods related to countering.
6.584 +
6.585 + def get_freebusy_offers(self, user):
6.586 +
6.587 + "Get free/busy offers for the given 'user'."
6.588 +
6.589 + offers = []
6.590 + expired = []
6.591 + now = to_timezone(datetime.utcnow(), "UTC")
6.592 +
6.593 + # Expire old offers and save the collection if modified.
6.594 +
6.595 + self.acquire_lock(user)
6.596 + try:
6.597 + l = self.get_freebusy(user, "freebusy-offers")
6.598 + for fb in l:
6.599 + if fb.expires and get_datetime(fb.expires) <= now:
6.600 + expired.append(fb)
6.601 + else:
6.602 + offers.append(fb)
6.603 +
6.604 + if expired:
6.605 + self.set_freebusy_offers(user, offers)
6.606 + finally:
6.607 + self.release_lock(user)
6.608 +
6.609 + return FreeBusyCollection(offers)
6.610 +
6.611 + def set_freebusy_offers(self, user, freebusy):
6.612 +
6.613 + "For the given 'user', set 'freebusy' offers."
6.614 +
6.615 + return self.set_freebusy(user, freebusy, "freebusy-offers")
6.616 +
6.617 + # Requests and counter-proposals.
6.618 +
6.619 + def _get_requests(self, user, queue):
6.620 +
6.621 + "Get requests for the given 'user' from the given 'queue'."
6.622 +
6.623 + filename = self.get_object_in_store(user, queue)
6.624 + if not filename or not isfile(filename):
6.625 + return None
6.626 +
6.627 + return self._get_table_atomic(user, filename, [(1, None), (2, None)])
6.628 +
6.629 + def get_requests(self, user):
6.630 +
6.631 + "Get requests for the given 'user'."
6.632 +
6.633 + return self._get_requests(user, "requests")
6.634 +
6.635 + def _set_requests(self, user, requests, queue):
6.636 +
6.637 + """
6.638 + For the given 'user', set the list of queued 'requests' in the given
6.639 + 'queue'.
6.640 + """
6.641 +
6.642 + filename = self.get_object_in_store(user, queue)
6.643 + if not filename:
6.644 + return False
6.645 +
6.646 + self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
6.647 + return True
6.648 +
6.649 + def set_requests(self, user, requests):
6.650 +
6.651 + "For the given 'user', set the list of queued 'requests'."
6.652 +
6.653 + return self._set_requests(user, requests, "requests")
6.654 +
6.655 + def _set_request(self, user, request, queue):
6.656 +
6.657 + """
6.658 + For the given 'user', set the given 'request' in the given 'queue'.
6.659 + """
6.660 +
6.661 + filename = self.get_object_in_store(user, queue)
6.662 + if not filename:
6.663 + return False
6.664 +
6.665 + self.acquire_lock(user)
6.666 + try:
6.667 + f = codecs.open(filename, "ab", encoding="utf-8")
6.668 + try:
6.669 + self._set_table_item(f, request, [(1, ""), (2, "")])
6.670 + finally:
6.671 + f.close()
6.672 + fix_permissions(filename)
6.673 + finally:
6.674 + self.release_lock(user)
6.675 +
6.676 + return True
6.677 +
6.678 + def set_request(self, user, uid, recurrenceid=None, type=None):
6.679 +
6.680 + """
6.681 + For the given 'user', set the queued 'uid' and 'recurrenceid',
6.682 + indicating a request, along with any given 'type'.
6.683 + """
6.684 +
6.685 + return self._set_request(user, (uid, recurrenceid, type), "requests")
6.686 +
6.687 + def get_counters(self, user, uid, recurrenceid=None):
6.688 +
6.689 + """
6.690 + For the given 'user', return a list of users from whom counter-proposals
6.691 + have been received for the given 'uid' and optional 'recurrenceid'.
6.692 + """
6.693 +
6.694 + filename = self.get_event_filename(user, uid, recurrenceid, "counters")
6.695 + if not filename or not isdir(filename):
6.696 + return False
6.697 +
6.698 + return [name for name in listdir(filename) if isfile(join(filename, name))]
6.699 +
6.700 + def get_counter(self, user, other, uid, recurrenceid=None):
6.701 +
6.702 + """
6.703 + For the given 'user', return the counter-proposal from 'other' for the
6.704 + given 'uid' and optional 'recurrenceid'.
6.705 + """
6.706 +
6.707 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.708 + if not filename:
6.709 + return False
6.710 +
6.711 + return self._get_object(user, filename)
6.712 +
6.713 + def set_counter(self, user, other, node, uid, recurrenceid=None):
6.714 +
6.715 + """
6.716 + For the given 'user', store a counter-proposal received from 'other' the
6.717 + given 'node' representing that proposal for the given 'uid' and
6.718 + 'recurrenceid'.
6.719 + """
6.720 +
6.721 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.722 + if not filename:
6.723 + return False
6.724 +
6.725 + return self._set_object(user, filename, node)
6.726 +
6.727 + def remove_counters(self, user, uid, recurrenceid=None):
6.728 +
6.729 + """
6.730 + For the given 'user', remove all counter-proposals associated with the
6.731 + given 'uid' and 'recurrenceid'.
6.732 + """
6.733 +
6.734 + filename = self.get_event_filename(user, uid, recurrenceid, "counters")
6.735 + if not filename or not isdir(filename):
6.736 + return False
6.737 +
6.738 + removed = False
6.739 +
6.740 + for other in listdir(filename):
6.741 + counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.742 + removed = removed or self._remove_object(counter_filename)
6.743 +
6.744 + return removed
6.745 +
6.746 + def remove_counter(self, user, other, uid, recurrenceid=None):
6.747 +
6.748 + """
6.749 + For the given 'user', remove any counter-proposal from 'other'
6.750 + associated with the given 'uid' and 'recurrenceid'.
6.751 + """
6.752 +
6.753 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.754 + if not filename or not isfile(filename):
6.755 + return False
6.756 +
6.757 + return self._remove_object(filename)
6.758 +
6.759 + # Event cancellation.
6.760 +
6.761 + def cancel_event(self, user, uid, recurrenceid=None):
6.762 +
6.763 + """
6.764 + Cancel an event for 'user' having the given 'uid'. If the optional
6.765 + 'recurrenceid' is specified, a specific instance or occurrence of an
6.766 + event is cancelled.
6.767 + """
6.768 +
6.769 + filename = self.get_event_filename(user, uid, recurrenceid)
6.770 + cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
6.771 +
6.772 + if filename and cancelled_filename and isfile(filename):
6.773 + return self.move_object(filename, cancelled_filename)
6.774 +
6.775 + return False
6.776 +
6.777 + def uncancel_event(self, user, uid, recurrenceid=None):
6.778 +
6.779 + """
6.780 + Uncancel an event for 'user' having the given 'uid'. If the optional
6.781 + 'recurrenceid' is specified, a specific instance or occurrence of an
6.782 + event is uncancelled.
6.783 + """
6.784 +
6.785 + filename = self.get_event_filename(user, uid, recurrenceid)
6.786 + cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
6.787 +
6.788 + if filename and cancelled_filename and isfile(cancelled_filename):
6.789 + return self.move_object(cancelled_filename, filename)
6.790 +
6.791 + return False
6.792 +
6.793 + def remove_cancellation(self, user, uid, recurrenceid=None):
6.794 +
6.795 + """
6.796 + Remove a cancellation for 'user' for the event having the given 'uid'.
6.797 + If the optional 'recurrenceid' is specified, a specific instance or
6.798 + occurrence of an event is affected.
6.799 + """
6.800 +
6.801 + # Remove any parent event cancellation or a specific recurrence
6.802 + # cancellation if indicated.
6.803 +
6.804 + filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
6.805 +
6.806 + if filename and isfile(filename):
6.807 + return self._remove_object(filename)
6.808 +
6.809 + return False
6.810 +
6.811 +class FilePublisher(FileBase, PublisherBase):
6.812 +
6.813 + "A publisher of objects."
6.814 +
6.815 + def __init__(self, store_dir=None):
6.816 + FileBase.__init__(self, store_dir or PUBLISH_DIR)
6.817 +
6.818 + def set_freebusy(self, user, freebusy):
6.819 +
6.820 + "For the given 'user', set 'freebusy' details."
6.821 +
6.822 + filename = self.get_object_in_store(user, "freebusy")
6.823 + if not filename:
6.824 + return False
6.825 +
6.826 + record = []
6.827 + rwrite = record.append
6.828 +
6.829 + rwrite(("ORGANIZER", {}, user))
6.830 + rwrite(("UID", {}, user))
6.831 + rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
6.832 +
6.833 + for fb in freebusy:
6.834 + if not fb.transp or fb.transp == "OPAQUE":
6.835 + rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
6.836 + map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
6.837 +
6.838 + f = open(filename, "wb")
6.839 + try:
6.840 + to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
6.841 + finally:
6.842 + f.close()
6.843 + fix_permissions(filename)
6.844 +
6.845 + return True
6.846 +
6.847 +class FileJournal(FileStoreBase, JournalBase):
6.848 +
6.849 + "A journal system to support quotas."
6.850 +
6.851 + def __init__(self, store_dir=None):
6.852 + FileBase.__init__(self, store_dir or JOURNAL_DIR)
6.853 +
6.854 + # Quota and user identity/group discovery.
6.855 +
6.856 + def get_quotas(self):
6.857 +
6.858 + "Return a list of quotas."
6.859 +
6.860 + return listdir(self.store_dir)
6.861 +
6.862 + def get_quota_users(self, quota):
6.863 +
6.864 + "Return a list of quota users."
6.865 +
6.866 + filename = self.get_object_in_store(quota, "journal")
6.867 + if not filename or not isdir(filename):
6.868 + return []
6.869 +
6.870 + return listdir(filename)
6.871 +
6.872 + # Groups of users sharing quotas.
6.873 +
6.874 + def get_groups(self, quota):
6.875 +
6.876 + "Return the identity mappings for the given 'quota' as a dictionary."
6.877 +
6.878 + filename = self.get_object_in_store(quota, "groups")
6.879 + if not filename or not isfile(filename):
6.880 + return {}
6.881 +
6.882 + return dict(self._get_table_atomic(quota, filename, tab_separated=False))
6.883 +
6.884 + def get_limits(self, quota):
6.885 +
6.886 + """
6.887 + Return the limits for the 'quota' as a dictionary mapping identities or
6.888 + groups to durations.
6.889 + """
6.890 +
6.891 + filename = self.get_object_in_store(quota, "limits")
6.892 + if not filename or not isfile(filename):
6.893 + return None
6.894 +
6.895 + return dict(self._get_table_atomic(quota, filename, tab_separated=False))
6.896 +
6.897 + # Free/busy period access for users within quota groups.
6.898 +
6.899 + def get_freebusy(self, quota, user):
6.900 +
6.901 + "Get free/busy details for the given 'quota' and 'user'."
6.902 +
6.903 + filename = self.get_object_in_store(quota, "freebusy", user)
6.904 +
6.905 + if not filename or not isfile(filename):
6.906 + periods = []
6.907 + else:
6.908 + periods = map(lambda t: FreeBusyPeriod(*t),
6.909 + self._get_table_atomic(quota, filename))
6.910 +
6.911 + return FreeBusyCollection(periods)
6.912 +
6.913 + def set_freebusy(self, quota, user, freebusy):
6.914 +
6.915 + "For the given 'quota' and 'user', set 'freebusy' details."
6.916 +
6.917 + filename = self.get_object_in_store(quota, "freebusy", user)
6.918 + if not filename:
6.919 + return False
6.920 +
6.921 + self._set_table_atomic(quota, filename,
6.922 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
6.923 + return True
6.924 +
6.925 + # Journal entry methods.
6.926 +
6.927 + def get_entries(self, quota, group):
6.928 +
6.929 + """
6.930 + Return a list of journal entries for the given 'quota' for the indicated
6.931 + 'group'.
6.932 + """
6.933 +
6.934 + filename = self.get_object_in_store(quota, "journal", group)
6.935 +
6.936 + if not filename or not isfile(filename):
6.937 + periods = []
6.938 + else:
6.939 + periods = map(lambda t: FreeBusyPeriod(*t),
6.940 + self._get_table_atomic(quota, filename))
6.941 +
6.942 + return FreeBusyCollection(periods)
6.943 +
6.944 + def set_entries(self, quota, group, entries):
6.945 +
6.946 + """
6.947 + For the given 'quota' and indicated 'group', set the list of journal
6.948 + 'entries'.
6.949 + """
6.950 +
6.951 + filename = self.get_object_in_store(quota, "journal", group)
6.952 + if not filename:
6.953 + return False
6.954 +
6.955 + self._set_table_atomic(quota, filename,
6.956 + map(lambda fb: fb.as_tuple(strings_only=True), entries.periods))
6.957 + return True
6.958 +
6.959 +# vim: tabstop=4 expandtab shiftwidth=4
7.1 --- a/imipweb/resource.py Sat Mar 05 23:45:47 2016 +0100
7.2 +++ b/imipweb/resource.py Sun Mar 06 00:46:26 2016 +0100
7.3 @@ -28,7 +28,6 @@
7.4 from imipweb.env import CGIEnvironment
7.5 from urllib import urlencode
7.6 import babel.dates
7.7 -import imip_store
7.8 import markup
7.9 import pytz
7.10
8.1 --- a/tests/test_handle.py Sat Mar 05 23:45:47 2016 +0100
8.2 +++ b/tests/test_handle.py Sun Mar 06 00:46:26 2016 +0100
8.3 @@ -24,7 +24,7 @@
8.4 from imiptools.dates import get_datetime, to_timezone
8.5 from imiptools.mail import Messenger
8.6 from imiptools.period import RecurringPeriod
8.7 -import imip_store
8.8 +import imiptools.stores.file
8.9 import sys
8.10
8.11 class TestClient(ClientForObject):
8.12 @@ -120,8 +120,8 @@
8.13 """
8.14 sys.exit(1)
8.15
8.16 - store = imip_store.FileStore(store_dir)
8.17 - journal = imip_store.FileJournal(journal_dir)
8.18 + store = imiptools.stores.file.FileStore(store_dir)
8.19 + journal = imiptools.stores.file.FileJournal(journal_dir)
8.20
8.21 if uid is not None:
8.22 fragment = store.get_event(user, uid, recurrenceid)
9.1 --- a/tools/install.sh Sat Mar 05 23:45:47 2016 +0100
9.2 +++ b/tools/install.sh Sun Mar 06 00:46:26 2016 +0100
9.3 @@ -17,7 +17,7 @@
9.4 # Agents and modules.
9.5
9.6 AGENTS="imip_person.py imip_person_outgoing.py imip_resource.py"
9.7 -MODULES="markup.py imip_store.py vCalendar.py vContent.py vRecurrence.py"
9.8 +MODULES="markup.py vCalendar.py vContent.py vRecurrence.py"
9.9
9.10 if [ ! -e "$INSTALL_DIR" ]; then
9.11 mkdir -p "$INSTALL_DIR"
9.12 @@ -27,6 +27,7 @@
9.13 cp $MODULES "$INSTALL_DIR"
9.14
9.15 for DIR in "$INSTALL_DIR/imiptools" \
9.16 + "$INSTALL_DIR/imiptools/stores" \
9.17 "$INSTALL_DIR/imiptools/handlers" \
9.18 "$INSTALL_DIR/imiptools/handlers/scheduling" ; do
9.19 if [ ! -e "$DIR" ]; then
9.20 @@ -43,6 +44,7 @@
9.21 # Copy modules into the installation directory.
9.22
9.23 cp imiptools/*.py "$INSTALL_DIR/imiptools/"
9.24 +cp imiptools/stores/*.py "$INSTALL_DIR/imiptools/stores/"
9.25 cp imiptools/handlers/*.py "$INSTALL_DIR/imiptools/handlers/"
9.26 cp imiptools/handlers/scheduling/*.py "$INSTALL_DIR/imiptools/handlers/scheduling/"
9.27
9.28 @@ -52,6 +54,10 @@
9.29 rm "$INSTALL_DIR/imiptools/handlers/scheduling.py"*
9.30 fi
9.31
9.32 +if [ -e "$INSTALL_DIR/imip_store.py" ]; then
9.33 + rm "$INSTALL_DIR/imip_store.py"*
9.34 +fi
9.35 +
9.36 # Install the config module in a more appropriate location.
9.37
9.38 if [ ! -e "$CONFIG_DIR" ]; then
10.1 --- a/tools/make_freebusy.py Sat Mar 05 23:45:47 2016 +0100
10.2 +++ b/tools/make_freebusy.py Sun Mar 06 00:46:26 2016 +0100
10.3 @@ -38,7 +38,7 @@
10.4 from imiptools.data import get_window_end, Object
10.5 from imiptools.dates import get_default_timezone, to_utc_datetime
10.6 from imiptools.period import FreeBusyCollection
10.7 -from imip_store import FileStore, FilePublisher, FileJournal
10.8 +from imiptools.stores.file import FileStore, FilePublisher, FileJournal
10.9
10.10 def make_freebusy(client, participant, store_and_publish, include_needs_action,
10.11 reset_updated_list, verbose):
11.1 --- a/tools/update_quotas.py Sat Mar 05 23:45:47 2016 +0100
11.2 +++ b/tools/update_quotas.py Sun Mar 06 00:46:26 2016 +0100
11.3 @@ -34,7 +34,7 @@
11.4 from codecs import getwriter
11.5 from imiptools.dates import get_datetime, get_default_timezone, get_time, \
11.6 to_utc_datetime
11.7 -from imip_store import FileJournal
11.8 +from imiptools.stores.file import FileJournal
11.9
11.10 def remove_expired_entries(entries, expiry):
11.11