1.1 --- a/docs/wiki/Developing Sun Mar 06 00:11:29 2016 +0100
1.2 +++ b/docs/wiki/Developing Sun Mar 06 00:44:42 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 Sun Mar 06 00:11:29 2016 +0100
2.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2.3 @@ -1,1076 +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
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 -import codecs
2.35 -
2.36 -class FileStoreBase(FileBase):
2.37 -
2.38 - "A file store supporting user-specific locking and tabular data."
2.39 -
2.40 - def acquire_lock(self, user, timeout=None):
2.41 - FileBase.acquire_lock(self, timeout, user)
2.42 -
2.43 - def release_lock(self, user):
2.44 - FileBase.release_lock(self, user)
2.45 -
2.46 - # Utility methods.
2.47 -
2.48 - def _set_defaults(self, t, empty_defaults):
2.49 - for i, default in empty_defaults:
2.50 - if i >= len(t):
2.51 - t += [None] * (i - len(t) + 1)
2.52 - if not t[i]:
2.53 - t[i] = default
2.54 - return t
2.55 -
2.56 - def _get_table(self, user, filename, empty_defaults=None, tab_separated=True):
2.57 -
2.58 - """
2.59 - From the file for the given 'user' having the given 'filename', return
2.60 - a list of tuples representing the file's contents.
2.61 -
2.62 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.63 - default value where a column either does not exist or provides an empty
2.64 - value.
2.65 -
2.66 - If 'tab_separated' is specified and is a false value, line parsing using
2.67 - the imiptools.text.parse_line function will be performed instead of
2.68 - splitting each line of the file using tab characters as separators.
2.69 - """
2.70 -
2.71 - f = codecs.open(filename, "rb", encoding="utf-8")
2.72 - try:
2.73 - l = []
2.74 - for line in f.readlines():
2.75 - line = line.strip(" \r\n")
2.76 - if tab_separated:
2.77 - t = line.split("\t")
2.78 - else:
2.79 - t = parse_line(line)
2.80 - if empty_defaults:
2.81 - t = self._set_defaults(t, empty_defaults)
2.82 - l.append(tuple(t))
2.83 - return l
2.84 - finally:
2.85 - f.close()
2.86 -
2.87 - def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True):
2.88 -
2.89 - """
2.90 - From the file for the given 'user' having the given 'filename', return
2.91 - a list of tuples representing the file's contents.
2.92 -
2.93 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.94 - default value where a column either does not exist or provides an empty
2.95 - value.
2.96 -
2.97 - If 'tab_separated' is specified and is a false value, line parsing using
2.98 - the imiptools.text.parse_line function will be performed instead of
2.99 - splitting each line of the file using tab characters as separators.
2.100 - """
2.101 -
2.102 - self.acquire_lock(user)
2.103 - try:
2.104 - return self._get_table(user, filename, empty_defaults, tab_separated)
2.105 - finally:
2.106 - self.release_lock(user)
2.107 -
2.108 - def _set_table(self, user, filename, items, empty_defaults=None):
2.109 -
2.110 - """
2.111 - For the given 'user', write to the file having the given 'filename' the
2.112 - 'items'.
2.113 -
2.114 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.115 - default value where a column either does not exist or provides an empty
2.116 - value.
2.117 - """
2.118 -
2.119 - f = codecs.open(filename, "wb", encoding="utf-8")
2.120 - try:
2.121 - for item in items:
2.122 - self._set_table_item(f, item, empty_defaults)
2.123 - finally:
2.124 - f.close()
2.125 - fix_permissions(filename)
2.126 -
2.127 - def _set_table_item(self, f, item, empty_defaults=None):
2.128 -
2.129 - "Set in table 'f' the given 'item', using any 'empty_defaults'."
2.130 -
2.131 - if empty_defaults:
2.132 - item = self._set_defaults(list(item), empty_defaults)
2.133 - f.write("\t".join(item) + "\n")
2.134 -
2.135 - def _set_table_atomic(self, user, filename, items, empty_defaults=None):
2.136 -
2.137 - """
2.138 - For the given 'user', write to the file having the given 'filename' the
2.139 - 'items'.
2.140 -
2.141 - The 'empty_defaults' is a list of (index, value) tuples indicating the
2.142 - default value where a column either does not exist or provides an empty
2.143 - value.
2.144 - """
2.145 -
2.146 - self.acquire_lock(user)
2.147 - try:
2.148 - self._set_table(user, filename, items, empty_defaults)
2.149 - finally:
2.150 - self.release_lock(user)
2.151 -
2.152 -class FileStore(FileStoreBase):
2.153 -
2.154 - "A file store of tabular free/busy data and objects."
2.155 -
2.156 - def __init__(self, store_dir=None):
2.157 - FileBase.__init__(self, store_dir or STORE_DIR)
2.158 -
2.159 - # Store object access.
2.160 -
2.161 - def _get_object(self, user, filename):
2.162 -
2.163 - """
2.164 - Return the parsed object for the given 'user' having the given
2.165 - 'filename'.
2.166 - """
2.167 -
2.168 - self.acquire_lock(user)
2.169 - try:
2.170 - f = open(filename, "rb")
2.171 - try:
2.172 - return parse_object(f, "utf-8")
2.173 - finally:
2.174 - f.close()
2.175 - finally:
2.176 - self.release_lock(user)
2.177 -
2.178 - def _set_object(self, user, filename, node):
2.179 -
2.180 - """
2.181 - Set an object for the given 'user' having the given 'filename', using
2.182 - 'node' to define the object.
2.183 - """
2.184 -
2.185 - self.acquire_lock(user)
2.186 - try:
2.187 - f = open(filename, "wb")
2.188 - try:
2.189 - to_stream(f, node)
2.190 - finally:
2.191 - f.close()
2.192 - fix_permissions(filename)
2.193 - finally:
2.194 - self.release_lock(user)
2.195 -
2.196 - return True
2.197 -
2.198 - def _remove_object(self, filename):
2.199 -
2.200 - "Remove the object with the given 'filename'."
2.201 -
2.202 - try:
2.203 - remove(filename)
2.204 - except OSError:
2.205 - return False
2.206 -
2.207 - return True
2.208 -
2.209 - def _remove_collection(self, filename):
2.210 -
2.211 - "Remove the collection with the given 'filename'."
2.212 -
2.213 - try:
2.214 - rmdir(filename)
2.215 - except OSError:
2.216 - return False
2.217 -
2.218 - return True
2.219 -
2.220 - # User discovery.
2.221 -
2.222 - def get_users(self):
2.223 -
2.224 - "Return a list of users."
2.225 -
2.226 - return listdir(self.store_dir)
2.227 -
2.228 - # Event and event metadata access.
2.229 -
2.230 - def get_events(self, user):
2.231 -
2.232 - "Return a list of event identifiers."
2.233 -
2.234 - filename = self.get_object_in_store(user, "objects")
2.235 - if not filename or not isdir(filename):
2.236 - return None
2.237 -
2.238 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.239 -
2.240 - def get_all_events(self, user):
2.241 -
2.242 - "Return a set of (uid, recurrenceid) tuples for all events."
2.243 -
2.244 - uids = self.get_events(user)
2.245 - if not uids:
2.246 - return set()
2.247 -
2.248 - all_events = set()
2.249 - for uid in uids:
2.250 - all_events.add((uid, None))
2.251 - all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
2.252 -
2.253 - return all_events
2.254 -
2.255 - def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
2.256 -
2.257 - """
2.258 - Get the filename providing the event for the given 'user' with the given
2.259 - 'uid'. If the optional 'recurrenceid' is specified, a specific instance
2.260 - or occurrence of an event is returned.
2.261 -
2.262 - Where 'dirname' is specified, the given directory name is used as the
2.263 - base of the location within which any filename will reside.
2.264 - """
2.265 -
2.266 - if recurrenceid:
2.267 - return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
2.268 - else:
2.269 - return self.get_complete_event_filename(user, uid, dirname, username)
2.270 -
2.271 - def get_event(self, user, uid, recurrenceid=None, dirname=None):
2.272 -
2.273 - """
2.274 - Get the event for the given 'user' with the given 'uid'. If
2.275 - the optional 'recurrenceid' is specified, a specific instance or
2.276 - occurrence of an event is returned.
2.277 - """
2.278 -
2.279 - filename = self.get_event_filename(user, uid, recurrenceid, dirname)
2.280 - if not filename or not isfile(filename):
2.281 - return None
2.282 -
2.283 - return filename and self._get_object(user, filename)
2.284 -
2.285 - def get_complete_event_filename(self, user, uid, dirname=None, username=None):
2.286 -
2.287 - """
2.288 - Get the filename providing the event for the given 'user' with the given
2.289 - 'uid'.
2.290 -
2.291 - Where 'dirname' is specified, the given directory name is used as the
2.292 - base of the location within which any filename will reside.
2.293 -
2.294 - Where 'username' is specified, the event details will reside in a file
2.295 - bearing that name within a directory having 'uid' as its name.
2.296 - """
2.297 -
2.298 - return self.get_object_in_store(user, dirname, "objects", uid, username)
2.299 -
2.300 - def get_complete_event(self, user, uid):
2.301 -
2.302 - "Get the event for the given 'user' with the given 'uid'."
2.303 -
2.304 - filename = self.get_complete_event_filename(user, uid)
2.305 - if not filename or not isfile(filename):
2.306 - return None
2.307 -
2.308 - return filename and self._get_object(user, filename)
2.309 -
2.310 - def set_event(self, user, uid, recurrenceid, node):
2.311 -
2.312 - """
2.313 - Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
2.314 - if the latter is specified, a specific instance or occurrence of an
2.315 - event is referenced), using the given 'node' description.
2.316 - """
2.317 -
2.318 - if recurrenceid:
2.319 - return self.set_recurrence(user, uid, recurrenceid, node)
2.320 - else:
2.321 - return self.set_complete_event(user, uid, node)
2.322 -
2.323 - def set_complete_event(self, user, uid, node):
2.324 -
2.325 - "Set an event for 'user' having the given 'uid' and 'node'."
2.326 -
2.327 - filename = self.get_object_in_store(user, "objects", uid)
2.328 - if not filename:
2.329 - return False
2.330 -
2.331 - return self._set_object(user, filename, node)
2.332 -
2.333 - def remove_event(self, user, uid, recurrenceid=None):
2.334 -
2.335 - """
2.336 - Remove an event for 'user' having the given 'uid'. If the optional
2.337 - 'recurrenceid' is specified, a specific instance or occurrence of an
2.338 - event is removed.
2.339 - """
2.340 -
2.341 - if recurrenceid:
2.342 - return self.remove_recurrence(user, uid, recurrenceid)
2.343 - else:
2.344 - for recurrenceid in self.get_recurrences(user, uid) or []:
2.345 - self.remove_recurrence(user, uid, recurrenceid)
2.346 - return self.remove_complete_event(user, uid)
2.347 -
2.348 - def remove_complete_event(self, user, uid):
2.349 -
2.350 - "Remove an event for 'user' having the given 'uid'."
2.351 -
2.352 - self.remove_recurrences(user, uid)
2.353 - return self.remove_parent_event(user, uid)
2.354 -
2.355 - def remove_parent_event(self, user, uid):
2.356 -
2.357 - "Remove the parent event for 'user' having the given 'uid'."
2.358 -
2.359 - filename = self.get_object_in_store(user, "objects", uid)
2.360 - if not filename:
2.361 - return False
2.362 -
2.363 - return self._remove_object(filename)
2.364 -
2.365 - def get_recurrences(self, user, uid):
2.366 -
2.367 - """
2.368 - Get additional event instances for an event of the given 'user' with the
2.369 - indicated 'uid'. Both active and cancelled recurrences are returned.
2.370 - """
2.371 -
2.372 - return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
2.373 -
2.374 - def get_active_recurrences(self, user, uid):
2.375 -
2.376 - """
2.377 - Get additional event instances for an event of the given 'user' with the
2.378 - indicated 'uid'. Cancelled recurrences are not returned.
2.379 - """
2.380 -
2.381 - filename = self.get_object_in_store(user, "recurrences", uid)
2.382 - if not filename or not isdir(filename):
2.383 - return []
2.384 -
2.385 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.386 -
2.387 - def get_cancelled_recurrences(self, user, uid):
2.388 -
2.389 - """
2.390 - Get additional event instances for an event of the given 'user' with the
2.391 - indicated 'uid'. Only cancelled recurrences are returned.
2.392 - """
2.393 -
2.394 - filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
2.395 - if not filename or not isdir(filename):
2.396 - return []
2.397 -
2.398 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.399 -
2.400 - def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
2.401 -
2.402 - """
2.403 - For the event of the given 'user' with the given 'uid', return the
2.404 - filename providing the recurrence with the given 'recurrenceid'.
2.405 -
2.406 - Where 'dirname' is specified, the given directory name is used as the
2.407 - base of the location within which any filename will reside.
2.408 -
2.409 - Where 'username' is specified, the event details will reside in a file
2.410 - bearing that name within a directory having 'uid' as its name.
2.411 - """
2.412 -
2.413 - return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
2.414 -
2.415 - def get_recurrence(self, user, uid, recurrenceid):
2.416 -
2.417 - """
2.418 - For the event of the given 'user' with the given 'uid', return the
2.419 - specific recurrence indicated by the 'recurrenceid'.
2.420 - """
2.421 -
2.422 - filename = self.get_recurrence_filename(user, uid, recurrenceid)
2.423 - if not filename or not isfile(filename):
2.424 - return None
2.425 -
2.426 - return filename and self._get_object(user, filename)
2.427 -
2.428 - def set_recurrence(self, user, uid, recurrenceid, node):
2.429 -
2.430 - "Set an event for 'user' having the given 'uid' and 'node'."
2.431 -
2.432 - filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
2.433 - if not filename:
2.434 - return False
2.435 -
2.436 - return self._set_object(user, filename, node)
2.437 -
2.438 - def remove_recurrence(self, user, uid, recurrenceid):
2.439 -
2.440 - """
2.441 - Remove a special recurrence from an event stored by 'user' having the
2.442 - given 'uid' and 'recurrenceid'.
2.443 - """
2.444 -
2.445 - filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
2.446 - if not filename:
2.447 - return False
2.448 -
2.449 - return self._remove_object(filename)
2.450 -
2.451 - def remove_recurrences(self, user, uid):
2.452 -
2.453 - """
2.454 - Remove all recurrences for an event stored by 'user' having the given
2.455 - 'uid'.
2.456 - """
2.457 -
2.458 - for recurrenceid in self.get_recurrences(user, uid):
2.459 - self.remove_recurrence(user, uid, recurrenceid)
2.460 -
2.461 - return self.remove_recurrence_collection(user, uid)
2.462 -
2.463 - def remove_recurrence_collection(self, user, uid):
2.464 -
2.465 - """
2.466 - Remove the collection of recurrences stored by 'user' having the given
2.467 - 'uid'.
2.468 - """
2.469 -
2.470 - recurrences = self.get_object_in_store(user, "recurrences", uid)
2.471 - if recurrences:
2.472 - return self._remove_collection(recurrences)
2.473 -
2.474 - return True
2.475 -
2.476 - # Free/busy period providers, upon extension of the free/busy records.
2.477 -
2.478 - def _get_freebusy_providers(self, user):
2.479 -
2.480 - """
2.481 - Return the free/busy providers for the given 'user'.
2.482 -
2.483 - This function returns any stored datetime and a list of providers as a
2.484 - 2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
2.485 - """
2.486 -
2.487 - filename = self.get_object_in_store(user, "freebusy-providers")
2.488 - if not filename or not isfile(filename):
2.489 - return None
2.490 -
2.491 - # Attempt to read providers, with a declaration of the datetime
2.492 - # from which such providers are considered as still being active.
2.493 -
2.494 - t = self._get_table_atomic(user, filename, [(1, None)])
2.495 - try:
2.496 - dt_string = t[0][0]
2.497 - except IndexError:
2.498 - return None
2.499 -
2.500 - return dt_string, t[1:]
2.501 -
2.502 - def get_freebusy_providers(self, user, dt=None):
2.503 -
2.504 - """
2.505 - Return a set of uncancelled events of the form (uid, recurrenceid)
2.506 - providing free/busy details beyond the given datetime 'dt'.
2.507 -
2.508 - If 'dt' is not specified, all events previously found to provide
2.509 - details will be returned. Otherwise, if 'dt' is earlier than the
2.510 - datetime recorded for the known providers, None is returned, indicating
2.511 - that the list of providers must be recomputed.
2.512 -
2.513 - This function returns a list of (uid, recurrenceid) tuples upon success.
2.514 - """
2.515 -
2.516 - t = self._get_freebusy_providers(user)
2.517 - if not t:
2.518 - return None
2.519 -
2.520 - dt_string, t = t
2.521 -
2.522 - # If the requested datetime is earlier than the stated datetime, the
2.523 - # providers will need to be recomputed.
2.524 -
2.525 - if dt:
2.526 - providers_dt = get_datetime(dt_string)
2.527 - if not providers_dt or providers_dt > dt:
2.528 - return None
2.529 -
2.530 - # Otherwise, return the providers.
2.531 -
2.532 - return t[1:]
2.533 -
2.534 - def _set_freebusy_providers(self, user, dt_string, t):
2.535 -
2.536 - "Set the given provider timestamp 'dt_string' and table 't'."
2.537 -
2.538 - filename = self.get_object_in_store(user, "freebusy-providers")
2.539 - if not filename:
2.540 - return False
2.541 -
2.542 - t.insert(0, (dt_string,))
2.543 - self._set_table_atomic(user, filename, t, [(1, "")])
2.544 - return True
2.545 -
2.546 - def set_freebusy_providers(self, user, dt, providers):
2.547 -
2.548 - """
2.549 - Define the uncancelled events providing free/busy details beyond the
2.550 - given datetime 'dt'.
2.551 - """
2.552 -
2.553 - t = []
2.554 -
2.555 - for obj in providers:
2.556 - t.append((obj.get_uid(), obj.get_recurrenceid()))
2.557 -
2.558 - return self._set_freebusy_providers(user, format_datetime(dt), t)
2.559 -
2.560 - def append_freebusy_provider(self, user, provider):
2.561 -
2.562 - "For the given 'user', append 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 - t.append((provider.get_uid(), provider.get_recurrenceid()))
2.570 -
2.571 - return self._set_freebusy_providers(user, dt_string, t)
2.572 -
2.573 - def remove_freebusy_provider(self, user, provider):
2.574 -
2.575 - "For the given 'user', remove the free/busy 'provider'."
2.576 -
2.577 - t = self._get_freebusy_providers(user)
2.578 - if not t:
2.579 - return False
2.580 -
2.581 - dt_string, t = t
2.582 - try:
2.583 - t.remove((provider.get_uid(), provider.get_recurrenceid()))
2.584 - except ValueError:
2.585 - return False
2.586 -
2.587 - return self._set_freebusy_providers(user, dt_string, t)
2.588 -
2.589 - # Free/busy period access.
2.590 -
2.591 - def get_freebusy(self, user, name=None):
2.592 -
2.593 - "Get free/busy details for the given 'user'."
2.594 -
2.595 - filename = self.get_object_in_store(user, name or "freebusy")
2.596 - if not filename or not isfile(filename):
2.597 - return []
2.598 - else:
2.599 - return map(lambda t: FreeBusyPeriod(*t),
2.600 - self._get_table_atomic(user, filename))
2.601 -
2.602 - def get_freebusy_for_other(self, user, other):
2.603 -
2.604 - "For the given 'user', get free/busy details for the 'other' user."
2.605 -
2.606 - filename = self.get_object_in_store(user, "freebusy-other", other)
2.607 - if not filename or not isfile(filename):
2.608 - return []
2.609 - else:
2.610 - return map(lambda t: FreeBusyPeriod(*t),
2.611 - self._get_table_atomic(user, filename))
2.612 -
2.613 - def set_freebusy(self, user, freebusy, name=None):
2.614 -
2.615 - "For the given 'user', set 'freebusy' details."
2.616 -
2.617 - filename = self.get_object_in_store(user, name or "freebusy")
2.618 - if not filename:
2.619 - return False
2.620 -
2.621 - self._set_table_atomic(user, filename,
2.622 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
2.623 - return True
2.624 -
2.625 - def set_freebusy_for_other(self, user, freebusy, other):
2.626 -
2.627 - "For the given 'user', set 'freebusy' details for the 'other' user."
2.628 -
2.629 - filename = self.get_object_in_store(user, "freebusy-other", other)
2.630 - if not filename:
2.631 - return False
2.632 -
2.633 - self._set_table_atomic(user, filename,
2.634 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
2.635 - return True
2.636 -
2.637 - # Tentative free/busy periods related to countering.
2.638 -
2.639 - def get_freebusy_offers(self, user):
2.640 -
2.641 - "Get free/busy offers for the given 'user'."
2.642 -
2.643 - offers = []
2.644 - expired = []
2.645 - now = to_timezone(datetime.utcnow(), "UTC")
2.646 -
2.647 - # Expire old offers and save the collection if modified.
2.648 -
2.649 - self.acquire_lock(user)
2.650 - try:
2.651 - l = self.get_freebusy(user, "freebusy-offers")
2.652 - for fb in l:
2.653 - if fb.expires and get_datetime(fb.expires) <= now:
2.654 - expired.append(fb)
2.655 - else:
2.656 - offers.append(fb)
2.657 -
2.658 - if expired:
2.659 - self.set_freebusy_offers(user, offers)
2.660 - finally:
2.661 - self.release_lock(user)
2.662 -
2.663 - return offers
2.664 -
2.665 - def set_freebusy_offers(self, user, freebusy):
2.666 -
2.667 - "For the given 'user', set 'freebusy' offers."
2.668 -
2.669 - return self.set_freebusy(user, freebusy, "freebusy-offers")
2.670 -
2.671 - # Requests and counter-proposals.
2.672 -
2.673 - def _get_requests(self, user, queue):
2.674 -
2.675 - "Get requests for the given 'user' from the given 'queue'."
2.676 -
2.677 - filename = self.get_object_in_store(user, queue)
2.678 - if not filename or not isfile(filename):
2.679 - return None
2.680 -
2.681 - return self._get_table_atomic(user, filename, [(1, None), (2, None)])
2.682 -
2.683 - def get_requests(self, user):
2.684 -
2.685 - "Get requests for the given 'user'."
2.686 -
2.687 - return self._get_requests(user, "requests")
2.688 -
2.689 - def _set_requests(self, user, requests, queue):
2.690 -
2.691 - """
2.692 - For the given 'user', set the list of queued 'requests' in the given
2.693 - 'queue'.
2.694 - """
2.695 -
2.696 - filename = self.get_object_in_store(user, queue)
2.697 - if not filename:
2.698 - return False
2.699 -
2.700 - self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
2.701 - return True
2.702 -
2.703 - def set_requests(self, user, requests):
2.704 -
2.705 - "For the given 'user', set the list of queued 'requests'."
2.706 -
2.707 - return self._set_requests(user, requests, "requests")
2.708 -
2.709 - def _set_request(self, user, request, queue):
2.710 -
2.711 - """
2.712 - For the given 'user', set the given 'request' in the given 'queue'.
2.713 - """
2.714 -
2.715 - filename = self.get_object_in_store(user, queue)
2.716 - if not filename:
2.717 - return False
2.718 -
2.719 - self.acquire_lock(user)
2.720 - try:
2.721 - f = codecs.open(filename, "ab", encoding="utf-8")
2.722 - try:
2.723 - self._set_table_item(f, request, [(1, ""), (2, "")])
2.724 - finally:
2.725 - f.close()
2.726 - fix_permissions(filename)
2.727 - finally:
2.728 - self.release_lock(user)
2.729 -
2.730 - return True
2.731 -
2.732 - def set_request(self, user, uid, recurrenceid=None, type=None):
2.733 -
2.734 - """
2.735 - For the given 'user', set the queued 'uid' and 'recurrenceid',
2.736 - indicating a request, along with any given 'type'.
2.737 - """
2.738 -
2.739 - return self._set_request(user, (uid, recurrenceid, type), "requests")
2.740 -
2.741 - def queue_request(self, user, uid, recurrenceid=None, type=None):
2.742 -
2.743 - """
2.744 - Queue a request for 'user' having the given 'uid'. If the optional
2.745 - 'recurrenceid' is specified, the entry refers to a specific instance
2.746 - or occurrence of an event. The 'type' parameter can be used to indicate
2.747 - a specific type of request.
2.748 - """
2.749 -
2.750 - requests = self.get_requests(user) or []
2.751 -
2.752 - if not self.have_request(requests, uid, recurrenceid):
2.753 - return self.set_request(user, uid, recurrenceid, type)
2.754 -
2.755 - return False
2.756 -
2.757 - def dequeue_request(self, user, uid, recurrenceid=None):
2.758 -
2.759 - """
2.760 - Dequeue all requests for 'user' having the given 'uid'. If the optional
2.761 - 'recurrenceid' is specified, all requests for that specific instance or
2.762 - occurrence of an event are dequeued.
2.763 - """
2.764 -
2.765 - requests = self.get_requests(user) or []
2.766 - result = []
2.767 -
2.768 - for request in requests:
2.769 - if request[:2] != (uid, recurrenceid):
2.770 - result.append(request)
2.771 -
2.772 - self.set_requests(user, result)
2.773 - return True
2.774 -
2.775 - def has_request(self, user, uid, recurrenceid=None, type=None, strict=False):
2.776 - return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict)
2.777 -
2.778 - def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):
2.779 -
2.780 - """
2.781 - Return whether 'requests' contains a request with the given 'uid' and
2.782 - any specified 'recurrenceid' and 'type'. If 'strict' is set to a true
2.783 - value, the precise type of the request must match; otherwise, any type
2.784 - of request for the identified object may be matched.
2.785 - """
2.786 -
2.787 - for request in requests:
2.788 - if request[:2] == (uid, recurrenceid) and (
2.789 - not strict or
2.790 - not request[2:] and not type or
2.791 - request[2:] and request[2] == type):
2.792 -
2.793 - return True
2.794 -
2.795 - return False
2.796 -
2.797 - def get_counters(self, user, uid, recurrenceid=None):
2.798 -
2.799 - """
2.800 - For the given 'user', return a list of users from whom counter-proposals
2.801 - have been received for the given 'uid' and optional 'recurrenceid'.
2.802 - """
2.803 -
2.804 - filename = self.get_event_filename(user, uid, recurrenceid, "counters")
2.805 - if not filename or not isdir(filename):
2.806 - return False
2.807 -
2.808 - return [name for name in listdir(filename) if isfile(join(filename, name))]
2.809 -
2.810 - def get_counter(self, user, other, uid, recurrenceid=None):
2.811 -
2.812 - """
2.813 - For the given 'user', return the counter-proposal from 'other' for the
2.814 - given 'uid' and optional 'recurrenceid'.
2.815 - """
2.816 -
2.817 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.818 - if not filename:
2.819 - return False
2.820 -
2.821 - return self._get_object(user, filename)
2.822 -
2.823 - def set_counter(self, user, other, node, uid, recurrenceid=None):
2.824 -
2.825 - """
2.826 - For the given 'user', store a counter-proposal received from 'other' the
2.827 - given 'node' representing that proposal for the given 'uid' and
2.828 - 'recurrenceid'.
2.829 - """
2.830 -
2.831 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.832 - if not filename:
2.833 - return False
2.834 -
2.835 - return self._set_object(user, filename, node)
2.836 -
2.837 - def remove_counters(self, user, uid, recurrenceid=None):
2.838 -
2.839 - """
2.840 - For the given 'user', remove all counter-proposals associated with the
2.841 - given 'uid' and 'recurrenceid'.
2.842 - """
2.843 -
2.844 - filename = self.get_event_filename(user, uid, recurrenceid, "counters")
2.845 - if not filename or not isdir(filename):
2.846 - return False
2.847 -
2.848 - removed = False
2.849 -
2.850 - for other in listdir(filename):
2.851 - counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.852 - removed = removed or self._remove_object(counter_filename)
2.853 -
2.854 - return removed
2.855 -
2.856 - def remove_counter(self, user, other, uid, recurrenceid=None):
2.857 -
2.858 - """
2.859 - For the given 'user', remove any counter-proposal from 'other'
2.860 - associated with the given 'uid' and 'recurrenceid'.
2.861 - """
2.862 -
2.863 - filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
2.864 - if not filename or not isfile(filename):
2.865 - return False
2.866 -
2.867 - return self._remove_object(filename)
2.868 -
2.869 - # Event cancellation.
2.870 -
2.871 - def cancel_event(self, user, uid, recurrenceid=None):
2.872 -
2.873 - """
2.874 - Cancel an event for 'user' having the given 'uid'. If the optional
2.875 - 'recurrenceid' is specified, a specific instance or occurrence of an
2.876 - event is cancelled.
2.877 - """
2.878 -
2.879 - filename = self.get_event_filename(user, uid, recurrenceid)
2.880 - cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
2.881 -
2.882 - if filename and cancelled_filename and isfile(filename):
2.883 - return self.move_object(filename, cancelled_filename)
2.884 -
2.885 - return False
2.886 -
2.887 - def uncancel_event(self, user, uid, recurrenceid=None):
2.888 -
2.889 - """
2.890 - Uncancel an event for 'user' having the given 'uid'. If the optional
2.891 - 'recurrenceid' is specified, a specific instance or occurrence of an
2.892 - event is uncancelled.
2.893 - """
2.894 -
2.895 - filename = self.get_event_filename(user, uid, recurrenceid)
2.896 - cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
2.897 -
2.898 - if filename and cancelled_filename and isfile(cancelled_filename):
2.899 - return self.move_object(cancelled_filename, filename)
2.900 -
2.901 - return False
2.902 -
2.903 - def remove_cancellations(self, user, uid, recurrenceid=None):
2.904 -
2.905 - """
2.906 - Remove cancellations for 'user' for any event having the given 'uid'. If
2.907 - the optional 'recurrenceid' is specified, a specific instance or
2.908 - occurrence of an event is affected.
2.909 - """
2.910 -
2.911 - # Remove all recurrence cancellations if a general event is indicated.
2.912 -
2.913 - if not recurrenceid:
2.914 - for _recurrenceid in self.get_cancelled_recurrences(user, uid):
2.915 - self.remove_cancellation(user, uid, _recurrenceid)
2.916 -
2.917 - return self.remove_cancellation(user, uid, recurrenceid)
2.918 -
2.919 - def remove_cancellation(self, user, uid, recurrenceid=None):
2.920 -
2.921 - """
2.922 - Remove a cancellation for 'user' for the event having the given 'uid'.
2.923 - If the optional 'recurrenceid' is specified, a specific instance or
2.924 - occurrence of an event is affected.
2.925 - """
2.926 -
2.927 - # Remove any parent event cancellation or a specific recurrence
2.928 - # cancellation if indicated.
2.929 -
2.930 - filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
2.931 -
2.932 - if filename and isfile(filename):
2.933 - return self._remove_object(filename)
2.934 -
2.935 - return False
2.936 -
2.937 -class FilePublisher(FileBase):
2.938 -
2.939 - "A publisher of objects."
2.940 -
2.941 - def __init__(self, store_dir=None):
2.942 - FileBase.__init__(self, store_dir or PUBLISH_DIR)
2.943 -
2.944 - def set_freebusy(self, user, freebusy):
2.945 -
2.946 - "For the given 'user', set 'freebusy' details."
2.947 -
2.948 - filename = self.get_object_in_store(user, "freebusy")
2.949 - if not filename:
2.950 - return False
2.951 -
2.952 - record = []
2.953 - rwrite = record.append
2.954 -
2.955 - rwrite(("ORGANIZER", {}, user))
2.956 - rwrite(("UID", {}, user))
2.957 - rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
2.958 -
2.959 - for fb in freebusy:
2.960 - if not fb.transp or fb.transp == "OPAQUE":
2.961 - rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
2.962 - map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
2.963 -
2.964 - f = open(filename, "wb")
2.965 - try:
2.966 - to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
2.967 - finally:
2.968 - f.close()
2.969 - fix_permissions(filename)
2.970 -
2.971 - return True
2.972 -
2.973 -class FileJournal(FileStoreBase):
2.974 -
2.975 - "A journal system to support quotas."
2.976 -
2.977 - def __init__(self, store_dir=None):
2.978 - FileBase.__init__(self, store_dir or JOURNAL_DIR)
2.979 -
2.980 - # Quota and user identity/group discovery.
2.981 -
2.982 - def get_quotas(self):
2.983 -
2.984 - "Return a list of quotas."
2.985 -
2.986 - return listdir(self.store_dir)
2.987 -
2.988 - def get_quota_users(self, quota):
2.989 -
2.990 - "Return a list of quota users."
2.991 -
2.992 - filename = self.get_object_in_store(quota, "journal")
2.993 - if not filename or not isdir(filename):
2.994 - return []
2.995 -
2.996 - return listdir(filename)
2.997 -
2.998 - # Groups of users sharing quotas.
2.999 -
2.1000 - def get_groups(self, quota):
2.1001 -
2.1002 - "Return the identity mappings for the given 'quota' as a dictionary."
2.1003 -
2.1004 - filename = self.get_object_in_store(quota, "groups")
2.1005 - if not filename or not isfile(filename):
2.1006 - return {}
2.1007 -
2.1008 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
2.1009 -
2.1010 - def get_limits(self, quota):
2.1011 -
2.1012 - """
2.1013 - Return the limits for the 'quota' as a dictionary mapping identities or
2.1014 - groups to durations.
2.1015 - """
2.1016 -
2.1017 - filename = self.get_object_in_store(quota, "limits")
2.1018 - if not filename or not isfile(filename):
2.1019 - return None
2.1020 -
2.1021 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
2.1022 -
2.1023 - # Free/busy period access for users within quota groups.
2.1024 -
2.1025 - def get_freebusy(self, quota, user):
2.1026 -
2.1027 - "Get free/busy details for the given 'quota' and 'user'."
2.1028 -
2.1029 - filename = self.get_object_in_store(quota, "freebusy", user)
2.1030 - if not filename or not isfile(filename):
2.1031 - return []
2.1032 -
2.1033 - return map(lambda t: FreeBusyPeriod(*t),
2.1034 - self._get_table_atomic(quota, filename))
2.1035 -
2.1036 - def set_freebusy(self, quota, user, freebusy):
2.1037 -
2.1038 - "For the given 'quota' and 'user', set 'freebusy' details."
2.1039 -
2.1040 - filename = self.get_object_in_store(quota, "freebusy", user)
2.1041 - if not filename:
2.1042 - return False
2.1043 -
2.1044 - self._set_table_atomic(quota, filename,
2.1045 - map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
2.1046 - return True
2.1047 -
2.1048 - # Journal entry methods.
2.1049 -
2.1050 - def get_entries(self, quota, group):
2.1051 -
2.1052 - """
2.1053 - Return a list of journal entries for the given 'quota' for the indicated
2.1054 - 'group'.
2.1055 - """
2.1056 -
2.1057 - filename = self.get_object_in_store(quota, "journal", group)
2.1058 - if not filename or not isfile(filename):
2.1059 - return []
2.1060 -
2.1061 - return map(lambda t: FreeBusyPeriod(*t),
2.1062 - self._get_table_atomic(quota, filename))
2.1063 -
2.1064 - def set_entries(self, quota, group, entries):
2.1065 -
2.1066 - """
2.1067 - For the given 'quota' and indicated 'group', set the list of journal
2.1068 - 'entries'.
2.1069 - """
2.1070 -
2.1071 - filename = self.get_object_in_store(quota, "journal", group)
2.1072 - if not filename:
2.1073 - return False
2.1074 -
2.1075 - self._set_table_atomic(quota, filename,
2.1076 - map(lambda fb: fb.as_tuple(strings_only=True), entries))
2.1077 - return True
2.1078 -
2.1079 -# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/imiptools/__init__.py Sun Mar 06 00:11:29 2016 +0100
3.2 +++ b/imiptools/__init__.py Sun Mar 06 00:44:42 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 Sun Mar 06 00:11:29 2016 +0100
4.2 +++ b/imiptools/client.py Sun Mar 06 00:44:42 2016 +0100
4.3 @@ -31,7 +31,7 @@
4.4 remove_additional_periods, remove_affected_period, \
4.5 update_freebusy
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 @@ -51,11 +51,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:44:42 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:44:42 2016 +0100
6.3 @@ -0,0 +1,944 @@
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
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 + if not filename or not isfile(filename):
6.537 + return []
6.538 + else:
6.539 + return map(lambda t: FreeBusyPeriod(*t),
6.540 + self._get_table_atomic(user, filename))
6.541 +
6.542 + def get_freebusy_for_other(self, user, other):
6.543 +
6.544 + "For the given 'user', get free/busy details for the 'other' user."
6.545 +
6.546 + filename = self.get_object_in_store(user, "freebusy-other", other)
6.547 + if not filename or not isfile(filename):
6.548 + return []
6.549 + else:
6.550 + return map(lambda t: FreeBusyPeriod(*t),
6.551 + self._get_table_atomic(user, filename))
6.552 +
6.553 + def set_freebusy(self, user, freebusy, name=None):
6.554 +
6.555 + "For the given 'user', set 'freebusy' details."
6.556 +
6.557 + filename = self.get_object_in_store(user, name or "freebusy")
6.558 + if not filename:
6.559 + return False
6.560 +
6.561 + self._set_table_atomic(user, filename,
6.562 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
6.563 + return True
6.564 +
6.565 + def set_freebusy_for_other(self, user, freebusy, other):
6.566 +
6.567 + "For the given 'user', set 'freebusy' details for the 'other' user."
6.568 +
6.569 + filename = self.get_object_in_store(user, "freebusy-other", other)
6.570 + if not filename:
6.571 + return False
6.572 +
6.573 + self._set_table_atomic(user, filename,
6.574 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
6.575 + return True
6.576 +
6.577 + # Tentative free/busy periods related to countering.
6.578 +
6.579 + def get_freebusy_offers(self, user):
6.580 +
6.581 + "Get free/busy offers for the given 'user'."
6.582 +
6.583 + offers = []
6.584 + expired = []
6.585 + now = to_timezone(datetime.utcnow(), "UTC")
6.586 +
6.587 + # Expire old offers and save the collection if modified.
6.588 +
6.589 + self.acquire_lock(user)
6.590 + try:
6.591 + l = self.get_freebusy(user, "freebusy-offers")
6.592 + for fb in l:
6.593 + if fb.expires and get_datetime(fb.expires) <= now:
6.594 + expired.append(fb)
6.595 + else:
6.596 + offers.append(fb)
6.597 +
6.598 + if expired:
6.599 + self.set_freebusy_offers(user, offers)
6.600 + finally:
6.601 + self.release_lock(user)
6.602 +
6.603 + return offers
6.604 +
6.605 + def set_freebusy_offers(self, user, freebusy):
6.606 +
6.607 + "For the given 'user', set 'freebusy' offers."
6.608 +
6.609 + return self.set_freebusy(user, freebusy, "freebusy-offers")
6.610 +
6.611 + # Requests and counter-proposals.
6.612 +
6.613 + def _get_requests(self, user, queue):
6.614 +
6.615 + "Get requests for the given 'user' from the given 'queue'."
6.616 +
6.617 + filename = self.get_object_in_store(user, queue)
6.618 + if not filename or not isfile(filename):
6.619 + return None
6.620 +
6.621 + return self._get_table_atomic(user, filename, [(1, None), (2, None)])
6.622 +
6.623 + def get_requests(self, user):
6.624 +
6.625 + "Get requests for the given 'user'."
6.626 +
6.627 + return self._get_requests(user, "requests")
6.628 +
6.629 + def _set_requests(self, user, requests, queue):
6.630 +
6.631 + """
6.632 + For the given 'user', set the list of queued 'requests' in the given
6.633 + 'queue'.
6.634 + """
6.635 +
6.636 + filename = self.get_object_in_store(user, queue)
6.637 + if not filename:
6.638 + return False
6.639 +
6.640 + self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
6.641 + return True
6.642 +
6.643 + def set_requests(self, user, requests):
6.644 +
6.645 + "For the given 'user', set the list of queued 'requests'."
6.646 +
6.647 + return self._set_requests(user, requests, "requests")
6.648 +
6.649 + def _set_request(self, user, request, queue):
6.650 +
6.651 + """
6.652 + For the given 'user', set the given 'request' in the given 'queue'.
6.653 + """
6.654 +
6.655 + filename = self.get_object_in_store(user, queue)
6.656 + if not filename:
6.657 + return False
6.658 +
6.659 + self.acquire_lock(user)
6.660 + try:
6.661 + f = codecs.open(filename, "ab", encoding="utf-8")
6.662 + try:
6.663 + self._set_table_item(f, request, [(1, ""), (2, "")])
6.664 + finally:
6.665 + f.close()
6.666 + fix_permissions(filename)
6.667 + finally:
6.668 + self.release_lock(user)
6.669 +
6.670 + return True
6.671 +
6.672 + def set_request(self, user, uid, recurrenceid=None, type=None):
6.673 +
6.674 + """
6.675 + For the given 'user', set the queued 'uid' and 'recurrenceid',
6.676 + indicating a request, along with any given 'type'.
6.677 + """
6.678 +
6.679 + return self._set_request(user, (uid, recurrenceid, type), "requests")
6.680 +
6.681 + def get_counters(self, user, uid, recurrenceid=None):
6.682 +
6.683 + """
6.684 + For the given 'user', return a list of users from whom counter-proposals
6.685 + have been received for the given 'uid' and optional 'recurrenceid'.
6.686 + """
6.687 +
6.688 + filename = self.get_event_filename(user, uid, recurrenceid, "counters")
6.689 + if not filename or not isdir(filename):
6.690 + return False
6.691 +
6.692 + return [name for name in listdir(filename) if isfile(join(filename, name))]
6.693 +
6.694 + def get_counter(self, user, other, uid, recurrenceid=None):
6.695 +
6.696 + """
6.697 + For the given 'user', return the counter-proposal from 'other' for the
6.698 + given 'uid' and optional 'recurrenceid'.
6.699 + """
6.700 +
6.701 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.702 + if not filename:
6.703 + return False
6.704 +
6.705 + return self._get_object(user, filename)
6.706 +
6.707 + def set_counter(self, user, other, node, uid, recurrenceid=None):
6.708 +
6.709 + """
6.710 + For the given 'user', store a counter-proposal received from 'other' the
6.711 + given 'node' representing that proposal for the given 'uid' and
6.712 + 'recurrenceid'.
6.713 + """
6.714 +
6.715 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.716 + if not filename:
6.717 + return False
6.718 +
6.719 + return self._set_object(user, filename, node)
6.720 +
6.721 + def remove_counters(self, user, uid, recurrenceid=None):
6.722 +
6.723 + """
6.724 + For the given 'user', remove all counter-proposals associated with the
6.725 + given 'uid' and 'recurrenceid'.
6.726 + """
6.727 +
6.728 + filename = self.get_event_filename(user, uid, recurrenceid, "counters")
6.729 + if not filename or not isdir(filename):
6.730 + return False
6.731 +
6.732 + removed = False
6.733 +
6.734 + for other in listdir(filename):
6.735 + counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.736 + removed = removed or self._remove_object(counter_filename)
6.737 +
6.738 + return removed
6.739 +
6.740 + def remove_counter(self, user, other, uid, recurrenceid=None):
6.741 +
6.742 + """
6.743 + For the given 'user', remove any counter-proposal from 'other'
6.744 + associated with the given 'uid' and 'recurrenceid'.
6.745 + """
6.746 +
6.747 + filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
6.748 + if not filename or not isfile(filename):
6.749 + return False
6.750 +
6.751 + return self._remove_object(filename)
6.752 +
6.753 + # Event cancellation.
6.754 +
6.755 + def cancel_event(self, user, uid, recurrenceid=None):
6.756 +
6.757 + """
6.758 + Cancel an event for 'user' having the given 'uid'. If the optional
6.759 + 'recurrenceid' is specified, a specific instance or occurrence of an
6.760 + event is cancelled.
6.761 + """
6.762 +
6.763 + filename = self.get_event_filename(user, uid, recurrenceid)
6.764 + cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
6.765 +
6.766 + if filename and cancelled_filename and isfile(filename):
6.767 + return self.move_object(filename, cancelled_filename)
6.768 +
6.769 + return False
6.770 +
6.771 + def uncancel_event(self, user, uid, recurrenceid=None):
6.772 +
6.773 + """
6.774 + Uncancel an event for 'user' having the given 'uid'. If the optional
6.775 + 'recurrenceid' is specified, a specific instance or occurrence of an
6.776 + event is uncancelled.
6.777 + """
6.778 +
6.779 + filename = self.get_event_filename(user, uid, recurrenceid)
6.780 + cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
6.781 +
6.782 + if filename and cancelled_filename and isfile(cancelled_filename):
6.783 + return self.move_object(cancelled_filename, filename)
6.784 +
6.785 + return False
6.786 +
6.787 + def remove_cancellation(self, user, uid, recurrenceid=None):
6.788 +
6.789 + """
6.790 + Remove a cancellation for 'user' for the event having the given 'uid'.
6.791 + If the optional 'recurrenceid' is specified, a specific instance or
6.792 + occurrence of an event is affected.
6.793 + """
6.794 +
6.795 + # Remove any parent event cancellation or a specific recurrence
6.796 + # cancellation if indicated.
6.797 +
6.798 + filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
6.799 +
6.800 + if filename and isfile(filename):
6.801 + return self._remove_object(filename)
6.802 +
6.803 + return False
6.804 +
6.805 +class FilePublisher(FileBase, PublisherBase):
6.806 +
6.807 + "A publisher of objects."
6.808 +
6.809 + def __init__(self, store_dir=None):
6.810 + FileBase.__init__(self, store_dir or PUBLISH_DIR)
6.811 +
6.812 + def set_freebusy(self, user, freebusy):
6.813 +
6.814 + "For the given 'user', set 'freebusy' details."
6.815 +
6.816 + filename = self.get_object_in_store(user, "freebusy")
6.817 + if not filename:
6.818 + return False
6.819 +
6.820 + record = []
6.821 + rwrite = record.append
6.822 +
6.823 + rwrite(("ORGANIZER", {}, user))
6.824 + rwrite(("UID", {}, user))
6.825 + rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
6.826 +
6.827 + for fb in freebusy:
6.828 + if not fb.transp or fb.transp == "OPAQUE":
6.829 + rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(
6.830 + map(format_datetime, [fb.get_start_point(), fb.get_end_point()]))))
6.831 +
6.832 + f = open(filename, "wb")
6.833 + try:
6.834 + to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
6.835 + finally:
6.836 + f.close()
6.837 + fix_permissions(filename)
6.838 +
6.839 + return True
6.840 +
6.841 +class FileJournal(FileStoreBase, JournalBase):
6.842 +
6.843 + "A journal system to support quotas."
6.844 +
6.845 + def __init__(self, store_dir=None):
6.846 + FileBase.__init__(self, store_dir or JOURNAL_DIR)
6.847 +
6.848 + # Quota and user identity/group discovery.
6.849 +
6.850 + def get_quotas(self):
6.851 +
6.852 + "Return a list of quotas."
6.853 +
6.854 + return listdir(self.store_dir)
6.855 +
6.856 + def get_quota_users(self, quota):
6.857 +
6.858 + "Return a list of quota users."
6.859 +
6.860 + filename = self.get_object_in_store(quota, "journal")
6.861 + if not filename or not isdir(filename):
6.862 + return []
6.863 +
6.864 + return listdir(filename)
6.865 +
6.866 + # Groups of users sharing quotas.
6.867 +
6.868 + def get_groups(self, quota):
6.869 +
6.870 + "Return the identity mappings for the given 'quota' as a dictionary."
6.871 +
6.872 + filename = self.get_object_in_store(quota, "groups")
6.873 + if not filename or not isfile(filename):
6.874 + return {}
6.875 +
6.876 + return dict(self._get_table_atomic(quota, filename, tab_separated=False))
6.877 +
6.878 + def get_limits(self, quota):
6.879 +
6.880 + """
6.881 + Return the limits for the 'quota' as a dictionary mapping identities or
6.882 + groups to durations.
6.883 + """
6.884 +
6.885 + filename = self.get_object_in_store(quota, "limits")
6.886 + if not filename or not isfile(filename):
6.887 + return None
6.888 +
6.889 + return dict(self._get_table_atomic(quota, filename, tab_separated=False))
6.890 +
6.891 + # Free/busy period access for users within quota groups.
6.892 +
6.893 + def get_freebusy(self, quota, user):
6.894 +
6.895 + "Get free/busy details for the given 'quota' and 'user'."
6.896 +
6.897 + filename = self.get_object_in_store(quota, "freebusy", user)
6.898 + if not filename or not isfile(filename):
6.899 + return []
6.900 +
6.901 + return map(lambda t: FreeBusyPeriod(*t),
6.902 + self._get_table_atomic(quota, filename))
6.903 +
6.904 + def set_freebusy(self, quota, user, freebusy):
6.905 +
6.906 + "For the given 'quota' and 'user', set 'freebusy' details."
6.907 +
6.908 + filename = self.get_object_in_store(quota, "freebusy", user)
6.909 + if not filename:
6.910 + return False
6.911 +
6.912 + self._set_table_atomic(quota, filename,
6.913 + map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
6.914 + return True
6.915 +
6.916 + # Journal entry methods.
6.917 +
6.918 + def get_entries(self, quota, group):
6.919 +
6.920 + """
6.921 + Return a list of journal entries for the given 'quota' for the indicated
6.922 + 'group'.
6.923 + """
6.924 +
6.925 + filename = self.get_object_in_store(quota, "journal", group)
6.926 + if not filename or not isfile(filename):
6.927 + return []
6.928 +
6.929 + return map(lambda t: FreeBusyPeriod(*t),
6.930 + self._get_table_atomic(quota, filename))
6.931 +
6.932 + def set_entries(self, quota, group, entries):
6.933 +
6.934 + """
6.935 + For the given 'quota' and indicated 'group', set the list of journal
6.936 + 'entries'.
6.937 + """
6.938 +
6.939 + filename = self.get_object_in_store(quota, "journal", group)
6.940 + if not filename:
6.941 + return False
6.942 +
6.943 + self._set_table_atomic(quota, filename,
6.944 + map(lambda fb: fb.as_tuple(strings_only=True), entries))
6.945 + return True
6.946 +
6.947 +# vim: tabstop=4 expandtab shiftwidth=4
7.1 --- a/imipweb/resource.py Sun Mar 06 00:11:29 2016 +0100
7.2 +++ b/imipweb/resource.py Sun Mar 06 00:44:42 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 Sun Mar 06 00:11:29 2016 +0100
8.2 +++ b/tests/test_handle.py Sun Mar 06 00:44:42 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 Sun Mar 06 00:11:29 2016 +0100
9.2 +++ b/tools/install.sh Sun Mar 06 00:44:42 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 Sun Mar 06 00:11:29 2016 +0100
10.2 +++ b/tools/make_freebusy.py Sun Mar 06 00:44:42 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 insert_period
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 Sun Mar 06 00:11:29 2016 +0100
11.2 +++ b/tools/update_quotas.py Sun Mar 06 00:44:42 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