# HG changeset patch # User Paul Boddie # Date 1454886893 -3600 # Node ID a12150034cbd9b2d4f160c9e3f332ab2b738f849 # Parent ed312d199bb75ff53a37cbeb31b091ddd93e02e3 Added a journal storage area, maintaining quota and collective scheduling data for scheduling decisions. Introduced confirmation and retraction functions for resource scheduling so that quotas and collective schedules can be maintained and thus queried by scheduling functions. Updated the documentation, tools and tests. diff -r ed312d199bb7 -r a12150034cbd docs/wiki/Administration --- a/docs/wiki/Administration Sun Feb 07 23:35:20 2016 +0100 +++ b/docs/wiki/Administration Mon Feb 08 00:14:53 2016 +0100 @@ -114,10 +114,11 @@ identity in [[../MailIntegration/Simple|lists of addresses]] or by adjusting [[../MailIntegration/LDAP|queries yielding calendar users]]. -== Adding Scheduling Functions == +== Adding Scheduling-Related Functions == -The `scheduling_function` setting employs functions that reside within modules in -the `imiptools.handlers.scheduling` package. Extra modules can be installed in +The `confirmation_function`, `retraction_function` and `scheduling_function` +settings employ functions that reside within modules in the +`imiptools.handlers.scheduling` package. Extra modules can be installed in this package by adding files to the `scheduling` directory within the software installation. @@ -129,4 +130,5 @@ It is envisaged that the installation of additional scheduling modules and the use of this tool will be performed by the packaging system provided by an -operating system distribution. +operating system distribution. The `tools/install.sh` script runs the above +tool as part of the installation process. diff -r ed312d199bb7 -r a12150034cbd docs/wiki/FilesystemUsage --- a/docs/wiki/FilesystemUsage Sun Feb 07 23:35:20 2016 +0100 +++ b/docs/wiki/FilesystemUsage Mon Feb 08 00:14:53 2016 +0100 @@ -7,31 +7,78 @@ {{{#!table '''Resource''' || '''Default Location''' || '''Purpose''' == -Store || `/var/lib/imip-agent/store` -|| Per-user directories containing calendar objects and scheduling information +Free/busy || `/var/www/imip-agent/static` +|| Per-user directories containing [[../FreeBusyPublishing|free/busy resources]] +.. for publication over the Web +== +Journal || `/var/lib/imip-agent/journal` +|| Per-quota directories containing journal information recording +.. [[../Resources|resource]] usage == Preferences || `/var/lib/imip-agent/preferences` || Per-user directories containing [[../Preferences|preferences]] controlling .. each user's experience of the software == -Free/busy || `/var/www/imip-agent/static` -|| Per-user directories containing [[../FreeBusyPublishing|free/busy resources]] -.. for publication over the Web +Store || `/var/lib/imip-agent/store` +|| Per-user directories containing calendar objects and scheduling information }}} Note that the free/busy resources are located in `/var/www` as opposed to -`/var/lib`. +`/var/lib` since they are intended to be published on the Web. + +== Journal Structure == + +Within the journal directory are a collection of subdirectories, each of which +represent a distinct quota group for one or more resources. When a user attempts +to reserve a resource in such a group, their ability to schedule that resource +will depend on how much they are using the other resources in that group. + +The directory for each quota group contains the following entries: + +{{{{#!table +'''Entry''' || '''Purpose''' +== +`freebusy` +|| A directory containing files, one per user, each containing period descriptions +.. for reservations made by that user, in chronological order, structured +.. similarly to the `freebusy` file found in each user's own store; the record is +.. consolidated for all resources in a quota group, but it is not consolidated for +.. groups of users +{{{ +freebusy/USER +}}} +== +`groups` +|| A mapping from user identities to group identifiers indicating the sharing +.. of a quota across a number of users +== +`journal` +|| A directory containing transaction files, one per user or user group, +.. describing confirmed reservations and retracted (cancelled) reservations for +.. that user or group +{{{ +journal/GROUP +journal/USER +}}} +== +`limits` +|| A mapping from user identities or group identifiers to quota limits +}}}} == Store Structure == +Within the store directory are a collection of user-specific subdirectories +acting as each user's own store directory containing various files and further +subdirectories. + The store directory for each user is considered in isolation from all other -users' directories: imip-agent does not go looking for information belonging to -other users when processing information on behalf of a particular user. +users' directories: imip-agent ''does not'' go looking for information belonging +to other users when processing information on behalf of a particular user. -The following subdirectories are defined within a user's store directory: +The following entries are defined within a user's own store directory: {{{{#!table -'''Directory''' || '''Purpose''' +'''Entry''' || '''Purpose''' == `cancellations` || Retains cancelled event details in `objects` and `recurrences` structures @@ -65,6 +112,9 @@ || A collection of files, one per user, each containing period descriptions .. received or deduced for that user in chronological order, structured similarly .. to the store user's own `freebusy` file +{{{ +freebusy-other/USER +}}} == `freebusy-providers` || A file containing details of [[../EventRecurrences|recurring events]] for which diff -r ed312d199bb7 -r a12150034cbd docs/wiki/Preferences --- a/docs/wiki/Preferences Sun Feb 07 23:35:20 2016 +0100 +++ b/docs/wiki/Preferences Mon Feb 08 00:14:53 2016 +0100 @@ -110,6 +110,24 @@ .. all event details }}} +=== confirmation_function === + + Default:: (none) + Alternatives:: (see below) + +Indicates the confirmation functions used by [[../Resources|resources]] to be +invoked when an event is scheduled. Such functions support certain scheduling +functions that require a record of scheduling activity. + +The `imiptools.handlers.scheduling` module contains the built-in confirmation +functions which include the following: + +{{{#!table +`add_to_quota` || add the details of an event to quota records +}}} + +See also `retraction_function` and `scheduling_function`. + === event_refreshing === Default:: `never` @@ -267,6 +285,24 @@ minute values that correspond to permitted values in this participant's own time zone. +=== retraction_function === + + Default:: (none) + Alternatives:: (see below) + +Indicates the retraction functions used by [[../Resources|resources]] to be +invoked when an event is cancelled. Such functions support certain scheduling +functions that require a record of scheduling activity. + +The `imiptools.handlers.scheduling` module contains the built-in retraction +functions which include the following: + +{{{#!table +`remove_from_quota` || remove the details of an event from quota records +}}} + +See also `confirmation_function` and `scheduling_function`. + === scheduling_function === Default:: `schedule_in_freebusy` @@ -277,7 +313,7 @@ scheduling request appearing on a separate line, optionally accompanied by arguments controlling the behaviour of the function. -The imiptools.handlers.scheduling module contains the built-in scheduling +The `imiptools.handlers.scheduling` module contains the built-in scheduling functions which include the following: {{{#!table @@ -286,6 +322,10 @@ .. the invitation according to the indicated rules .. (described in the [[../Resources|resources guide]]) == +`check_quota` || accept an invitation only if the organiser does not + .. exceed their quota allowance for bookings + .. (described in the [[../Resources|resources guide]]) +== `same_domain_only` || accept an invitation only if the organiser employs an .. address in the same domain as the resource == @@ -309,3 +349,5 @@ The scheduling mechanism can be extended by implementing additional scheduling functions or by extending the handler framework directly. + +See also `confirmation_function` and `retraction_function`. diff -r ed312d199bb7 -r a12150034cbd docs/wiki/Resources --- a/docs/wiki/Resources Sun Feb 07 23:35:20 2016 +0100 +++ b/docs/wiki/Resources Mon Feb 08 00:14:53 2016 +0100 @@ -17,6 +17,92 @@ <> +== Confirmation and Retraction Functions == + +Confirmation and retraction functions are used to update other resources and +systems with the details of scheduled events. When an event is successfully +scheduled (according to the scheduling functions), all registered confirmation +functions are invoked to perform such notifications. Similarly, when an event +is cancelled, all registered retraction functions are invoked to inform other +components of the removal of the event from schedules. + +The [[../Preferences#confirmation_function|confirmation_function]] and +[[../Preferences#retraction_function|retraction_function]] settings indicate +the behaviour of a resource when such circumstances arise. By themselves, +these settings do no more than keep a kind of journal of scheduling events, +but certain scheduling functions may build upon such journals to make +scheduling decisions. For example: + +{{{{#!table +'''All Functions''' || '''Decision Process''' +== + + +Scheduling functions: + +{{{ +check_quota +}}} + +Confirmation functions: + +{{{ +add_to_quota +}}} + +Retraction functions: + +{{{ +remove_from_quota +}}} + +|| + +{{{#!graphviz +//format=svg +//transform=notugly +digraph scheduling_decisions { + node [shape=box,fontsize="13.0",fontname="Helvetica",tooltip="Scheduling decisions"]; + edge [tooltip="Scheduling decisions"]; + + subgraph { + rank=same; + mail [label="Incoming mail\nfrom vincent.vole@example.com",shape=folder,style=filled,fillcolor=cyan]; + cancel [label="Incoming cancellation",shape=folder,style=filled,fillcolor=cyan]; + } + + subgraph { + rank=same; + check_quota [label="Is allowed by quota?",shape=ellipse,style=filled,fillcolor=gold]; + quota [label="Quota for resource",shape=folder]; + quota_for_vole [label="...applying to\nvincent.vole@example.com",shape=folder]; + } + + schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } + + add_to_quota [label="Add to quota",shape=ellipse,style=filled,fillcolor=darkorange]; + remove_from_quota [label="Remove from quota",shape=ellipse,style=filled,fillcolor=darkorange]; + + mail -> check_quota -> schedule -> accept; + check_quota -> decline [style=dashed]; + schedule -> add_to_quota -> quota; + quota -> quota_for_vole -> check_quota; + + cancel -> remove_from_quota -> quota; +} +}}} + +}}}} + +See the [[#Quota_Controls|quota controls]] documentation for more information +about applying quotas to resources. + == Scheduling Functions == The [[../Preferences#scheduling_function|scheduling_function]] setting @@ -51,8 +137,12 @@ } schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; - accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; - decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } mail -> schedule_in_freebusy -> schedule -> accept; schedule_in_freebusy -> decline [style=dashed]; @@ -109,8 +199,12 @@ same_domain_only [label="Organiser has resource domain?",shape=ellipse,style=filled,fillcolor=gold]; schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; - accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; - decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } mail -> schedule_in_freebusy -> same_domain_only -> schedule -> accept; schedule_in_freebusy -> decline [style=dashed]; @@ -174,8 +268,12 @@ end_acl [label="end",shape=ellipse,style=filled,fillcolor=darkorange]; schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; - accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; - decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } mail -> schedule_in_freebusy -> access_control_list -> accept_default -> end_acl -> schedule -> accept; end_acl -> decline [style=dashed]; @@ -252,8 +350,12 @@ end_acl [label="end",shape=ellipse,style=filled,fillcolor=darkorange]; schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; - accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; - decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } mail -> schedule_in_freebusy -> access_control_list -> accept_default -> decline_attendee -> end_acl -> schedule -> accept; end_acl -> decline [style=dashed]; @@ -317,8 +419,12 @@ end_acl [label="end",shape=ellipse,style=filled,fillcolor=darkorange]; schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; - accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; - decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } mail -> schedule_in_freebusy -> access_control_list -> accept_default -> decline_attendee -> accept_organiser -> end_acl -> schedule -> accept; end_acl -> decline [style=dashed]; @@ -332,3 +438,243 @@ Here, the stated organiser may still arrange a booking of the resource where the previously-mentioned attendee is involved. + +=== Quota Controls === + +The [[#Confirmation_and_Retraction_Functions|confirmation and retraction functions]] +section provides an example of applying quotas to event participants. However, +this section describes the operation of the quota system in more detail. + +In contrast to each user's stored information which consolidates information +related to that user's own schedule, the quota system consolidates information +related to the schedules of one or more resources, thus enabling observations +to be made about their collective usage. + +First, consider a resource such as a car where an organiser of an event may be +booking the car for travel purposes. A quota prevents the organiser from +booking the resource too much and denying other users access to it. + +Now consider a number of separate car resources. An organiser might attempt to +get around any individual resource quota by booking a number of different cars. +By grouping the resources together, the organiser will exhaust a quota set on +the group of resources as reservations are made for the different members of +the group. + +==== Initialising Quotas ==== + +Within the journal storage area (described in the [[../FilesystemUsage|filesystem guide]]), +a quota group directory must be initialised with a `limits` file indicating +the amount of time that can be occupied by the cumulative total of all events +scheduled by an individual user or a group of which they are a member. For +example: + +{{{ +mailto:vincent.vole@example.com PT10H +}}} + +This indicates that the given user may only reserve 10 hours of events or less. +Attempts to schedule more time will be declined. + +To impose a general quota, the special `*` identity can be used: + +{{{ +* PT10H +}}} + +When a user identity is not listed and no general quota is defined, that +particular user will be unable to reserve the resource unless defined as a +member of a group listed in the `limits` file, as described below. + +==== Sharing Quotas Across Users ==== + +When the use of resources is to be shared between users in such a way that +groups of users will be sharing a single quota, the `groups` file in the +quota directory must be defined, mapping each user identity to the group to +which they will belong. For example: + +{{{ +mailto:vincent.vole@example.com developers +mailto:harvey.horse@example.com developers +mailto:paul.boddie@example.com developers +mailto:simon.skunk@example.com testers +}}} + +The group identity can then be employed in the `limits` file: + +{{{ +developers PT10H +testers PT20H +}}} + +Limits apply to individuals, then to groups, then the general quota applies. +Thus, when a group is not listed, the general quota applies; without a general +quota (and without matching individually), a group member will be unable to +reserve the resource. + +==== Individual Resource Quotas ==== + +The trivial case of applying quotas is to give a resource its own quota. This +is achieved by not specifying any arguments to the `check_quota` scheduling +function or to the `add_to_quota` and `remove_from_quota` functions. See the +[[#Confirmation_and_Retraction_Functions|confirmation and retraction functions]] +section for an example of this. + +==== Common Resource Quotas ==== + +By indicating an argument to the different functions, a common quota can be +employed. In the following example, both resources would employ the given +function invocations to pool their knowledge about their schedules. + +{{{{#!table +'''All Functions''' || '''Decision Process''' +== + + +Scheduling functions: + +{{{ +check_quota cars +}}} + +Confirmation functions: + +{{{ +add_to_quota cars +}}} + +Retraction functions: + +{{{ +remove_from_quota cars +}}} + +|| + +{{{#!graphviz +//format=svg +//transform=notugly +digraph scheduling_decisions { + node [shape=box,fontsize="13.0",fontname="Helvetica",tooltip="Scheduling decisions"]; + edge [tooltip="Scheduling decisions"]; + + subgraph { + rank=same; + mail_cadillac [label="Incoming mail\nfrom vincent.vole@example.com\nto resource-car-cadillac@example.com",shape=folder,style=filled,fillcolor=cyan]; + mail_pontiac [label="Incoming mail\nfrom vincent.vole@example.com\nto resource-car-pontiac@example.com",shape=folder,style=filled,fillcolor=cyan]; + cancel [label="Incoming cancellation",shape=folder,style=filled,fillcolor=cyan]; + } + + subgraph { + rank=same; + check_quota [label="Is allowed by quota?",shape=ellipse,style=filled,fillcolor=gold]; + quota_cars [label="Quota for cars",shape=folder]; + quota_cars_vole [label="...applying to\nvincent.vole@example.com",shape=folder]; + } + + schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } + + add_to_quota [label="Add to quota",shape=ellipse,style=filled,fillcolor=darkorange]; + remove_from_quota [label="Remove from quota",shape=ellipse,style=filled,fillcolor=darkorange]; + + mail_cadillac -> check_quota; + mail_pontiac -> check_quota -> schedule -> accept; + check_quota -> decline [style=dashed]; + schedule -> add_to_quota -> quota_cars; + quota_cars -> quota_cars_vole -> check_quota; + + cancel -> remove_from_quota -> quota_cars; +} +}}} + +}}}} + +==== Collective Scheduling ==== + +Consider two separate resources: both may be reserved at the same time by the +same organiser; neither resource would normally decline the reservation on the +basis of schedule availability, should the period concerned be free. However, +it may be undesirable for one organiser to occupy both resources at the same +time. + +Consequently, a mechanism is required to pool the resource schedules in such a +way that any reservation performed for one resource at a given point in time +prohibits another reservation performed for a related resource at the same +point in time by the same user. + +The free/busy records held for a given quota group permit such collective +scheduling decisions and are employed as follows: + +{{{{#!table +'''All Functions''' || '''Decision Process''' +== + + +Scheduling functions: + +{{{ +schedule_within_quota cars +}}} + +Confirmation functions: + +{{{ +add_to_quota_freebusy cars +}}} + +Retraction functions: + +{{{ +remove_from_quota_freebusy cars +}}} + +|| + +{{{#!graphviz +//format=svg +//transform=notugly +digraph scheduling_decisions { + node [shape=box,fontsize="13.0",fontname="Helvetica",tooltip="Scheduling decisions"]; + edge [tooltip="Scheduling decisions"]; + + subgraph { + rank=same; + mail_cadillac [label="Incoming mail\nfrom vincent.vole@example.com\nto resource-car-cadillac@example.com",shape=folder,style=filled,fillcolor=cyan]; + mail_pontiac [label="Incoming mail\nfrom vincent.vole@example.com\nto resource-car-pontiac@example.com",shape=folder,style=filled,fillcolor=cyan]; + cancel [label="Incoming cancellation",shape=folder,style=filled,fillcolor=cyan]; + } + + subgraph { + rank=same; + schedule_across_quota [label="Can be scheduled within the quota?",shape=ellipse,style=filled,fillcolor=gold]; + quota_cars [label="Quota for cars",shape=folder]; + freebusy_cars_vole [label="...recording schedule for\nvincent.vole@example.com",shape=folder]; + } + + schedule [label="Schedule event for resource",shape=ellipse,style=filled,fillcolor=gold]; + + subgraph { + rank=same; + accept [label="Accept",shape=folder,style=filled,fillcolor=cyan]; + decline [label="Decline",shape=folder,style=filled,fillcolor=cyan]; + } + + add_to_quota_freebusy [label="Add to quota free/busy",shape=ellipse,style=filled,fillcolor=darkorange]; + remove_from_quota_freebusy [label="Remove from quota free/busy",shape=ellipse,style=filled,fillcolor=darkorange]; + + mail_cadillac -> schedule_across_quota; + mail_pontiac -> schedule_across_quota -> schedule -> accept; + schedule_across_quota -> decline [style=dashed]; + schedule -> add_to_quota_freebusy -> quota_cars -> freebusy_cars_vole; + freebusy_cars_vole -> schedule_across_quota; + + cancel -> remove_from_quota_freebusy -> freebusy_cars_vole; +} +}}} + +}}}} diff -r ed312d199bb7 -r a12150034cbd imip_store.py --- a/imip_store.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imip_store.py Mon Feb 08 00:14:53 2016 +0100 @@ -3,7 +3,7 @@ """ A simple filesystem-based store of calendar data. -Copyright (C) 2014, 2015 Paul Boddie +Copyright (C) 2014, 2015, 2016 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -20,7 +20,7 @@ """ from datetime import datetime -from imiptools.config import STORE_DIR, PUBLISH_DIR +from imiptools.config import STORE_DIR, PUBLISH_DIR, JOURNAL_DIR from imiptools.data import make_calendar, parse_object, to_stream from imiptools.dates import format_datetime, get_datetime, to_timezone from imiptools.filesys import fix_permissions, FileBase @@ -30,12 +30,9 @@ from time import sleep import codecs -class FileStore(FileBase): +class FileStoreBase(FileBase): - "A file store of tabular free/busy data and objects." - - def __init__(self, store_dir=None): - FileBase.__init__(self, store_dir or STORE_DIR) + "A file store supporting user-specific locking and tabular data." def acquire_lock(self, user, timeout=None): FileBase.acquire_lock(self, timeout, user) @@ -137,6 +134,13 @@ finally: self.release_lock(user) +class FileStore(FileStoreBase): + + "A file store of tabular free/busy data and objects." + + def __init__(self, store_dir=None): + FileBase.__init__(self, store_dir or STORE_DIR) + # Store object access. def _get_object(self, user, filename): @@ -937,4 +941,90 @@ return True +class FileJournal(FileStoreBase): + + "A journal system to support quotas." + + def __init__(self, store_dir=None): + FileBase.__init__(self, store_dir or JOURNAL_DIR) + + # Groups of users sharing quotas. + + def get_groups(self, quota): + + "Return the identity mappings for the given 'quota' as a dictionary." + + filename = self.get_object_in_store(quota, "groups") + if not filename or not isfile(filename): + return {} + + return dict(self._get_table_atomic(quota, filename)) + + def get_limits(self, quota): + + """ + Return the limits for the 'quota' as a dictionary mapping identities or + groups to durations. + """ + + filename = self.get_object_in_store(quota, "limits") + if not filename or not isfile(filename): + return None + + return dict(self._get_table_atomic(quota, filename)) + + # Free/busy period access. + + def get_freebusy(self, quota, user, get_table=None): + + "Get free/busy details for the given 'quota' and 'user'." + + filename = self.get_object_in_store(quota, "freebusy", user) + if not filename or not isfile(filename): + return [] + else: + return map(lambda t: FreeBusyPeriod(*t), + (get_table or self._get_table_atomic)(quota, filename, [(4, None)])) + + def set_freebusy(self, quota, user, freebusy, set_table=None): + + "For the given 'quota' and 'user', set 'freebusy' details." + + filename = self.get_object_in_store(quota, "freebusy", user) + if not filename: + return False + + (set_table or self._set_table_atomic)(quota, filename, + map(lambda fb: fb.as_tuple(strings_only=True), freebusy)) + return True + + # Journal entry methods. + + def get_entries(self, quota, group): + + """ + Return a list of journal entries for the given 'quota' for the indicated + 'group'. + """ + + filename = self.get_object_in_store(quota, "journal", group) + if not filename or not isfile(filename): + return [] + + return self._get_table_atomic(quota, filename, [(1, None)]) + + def set_entries(self, quota, group, entries): + + """ + For the given 'quota' and indicated 'group', set the list of journal + 'entries'. + """ + + filename = self.get_object_in_store(quota, "journal", group) + if not filename: + return False + + self._set_table_atomic(quota, filename, entries, [(1, "")]) + return True + # vim: tabstop=4 expandtab shiftwidth=4 diff -r ed312d199bb7 -r a12150034cbd imiptools/__init__.py --- a/imiptools/__init__.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/__init__.py Mon Feb 08 00:14:53 2016 +0100 @@ -3,7 +3,7 @@ """ A processing framework for iMIP content. -Copyright (C) 2014, 2015 Paul Boddie +Copyright (C) 2014, 2015, 2016 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -58,6 +58,7 @@ self.lmtp_socket = None self.store_dir = None self.publishing_dir = None + self.journal_dir = None self.preferences_dir = None self.debug = False @@ -67,6 +68,9 @@ def get_publisher(self): return self.publishing_dir and imip_store.FilePublisher(self.publishing_dir) or None + def get_journal(self): + return imip_store.FileJournal(self.journal_dir) + def process(self, f, original_recipients): """ @@ -80,6 +84,7 @@ messenger = self.messenger store = self.get_store() publisher = self.get_publisher() + journal = self.get_journal() preferences_dir = self.preferences_dir # Handle messages with iTIP parts. @@ -89,7 +94,7 @@ if not self.outgoing_only: original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or []) for recipient in original_recipients: - Recipient(get_uri(recipient), messenger, store, publisher, preferences_dir, self.handlers, self.outgoing_only, self.debug + Recipient(get_uri(recipient), messenger, store, publisher, journal, preferences_dir, self.handlers, self.outgoing_only, self.debug ).process(msg, senders) # However, outgoing messages do not usually presume anything about the @@ -101,7 +106,7 @@ else: senders = [sender for sender in get_addresses(get_all_values(msg, "From") or []) if sender != config.MESSAGE_SENDER] - Recipient(senders and senders[0] or None, messenger, store, publisher, preferences_dir, self.handlers, self.outgoing_only, self.debug + Recipient(senders and senders[0] or None, messenger, store, publisher, journal, preferences_dir, self.handlers, self.outgoing_only, self.debug ).process(msg, senders) def process_args(self, args, stream): @@ -120,6 +125,7 @@ store_dir = [] publishing_dir = [] preferences_dir = [] + journal_dir = [] local_smtp = False l = [] @@ -161,6 +167,11 @@ elif arg == "-p": l = preferences_dir + # Switch to getting the journal directory. + + elif arg == "-j": + l = journal_dir + # Ignore debugging options. elif arg == "-d": @@ -172,6 +183,7 @@ self.store_dir = store_dir and store_dir[0] or None self.publishing_dir = publishing_dir and publishing_dir[0] or None self.preferences_dir = preferences_dir and preferences_dir[0] or None + self.journal_dir = journal_dir and journal_dir[0] or None self.process(stream, original_recipients) def __call__(self): @@ -187,7 +199,7 @@ print >>sys.stderr, """\ Usage: %s [ -o ... ] [-s ... ] [ -l | -L ] \\ [ -S ] [ -P ] \\ - [ -p ] [ -d ] + [ -p ] [ -j ] [ -d ] Address options: @@ -208,10 +220,11 @@ Configuration options: +-j Indicates the location of quota-related journal information +-P Indicates the location of published free/busy resources +-p Indicates the location of user preference directories -S Indicates the location of the calendar data store containing user storage directories --P Indicates the location of published free/busy resources --p Indicates the location of user preference directories Output options: @@ -244,15 +257,16 @@ "A processor acting as a client on behalf of a recipient." - def __init__(self, user, messenger, store, publisher, preferences_dir, handlers, outgoing_only, debug): + def __init__(self, user, messenger, store, publisher, journal, preferences_dir, + handlers, outgoing_only, debug): """ Initialise the recipient with the given 'user' identity, 'messenger', - 'store', 'publisher', 'preferences_dir', 'handlers', 'outgoing_only' and - 'debug' status. + 'store', 'publisher', 'journal', 'preferences_dir', 'handlers', + 'outgoing_only' and 'debug' status. """ - Client.__init__(self, user, messenger, store, publisher, preferences_dir) + Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir) self.handlers = handlers self.outgoing_only = outgoing_only self.debug = debug @@ -269,7 +283,7 @@ handlers = dict([(name, cls(senders, self.user and get_address(self.user), self.messenger, self.store, self.publisher, - self.preferences_dir)) + self.journal, self.preferences_dir)) for name, cls in self.handlers]) handled = False diff -r ed312d199bb7 -r a12150034cbd imiptools/client.py --- a/imiptools/client.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/client.py Mon Feb 08 00:14:53 2016 +0100 @@ -3,7 +3,7 @@ """ Common calendar client utilities. -Copyright (C) 2014, 2015 Paul Boddie +Copyright (C) 2014, 2015, 2016 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -40,17 +40,19 @@ default_window_size = 100 organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST" - def __init__(self, user, messenger=None, store=None, publisher=None, preferences_dir=None): + def __init__(self, user, messenger=None, store=None, publisher=None, journal=None, + preferences_dir=None): """ Initialise a calendar client with the current 'user', plus any - 'messenger', 'store' and 'publisher' objects, indicating any specific - 'preferences_dir'. + 'messenger', 'store', 'publisher' and 'journal' objects, indicating any + specific 'preferences_dir'. """ self.user = user self.messenger = messenger self.store = store or imip_store.FileStore() + self.journal = journal or imip_store.FileJournal() try: self.publisher = publisher or imip_store.FilePublisher() @@ -71,6 +73,9 @@ def get_publisher(self): return self.publisher + def get_journal(self): + return self.journal + # Store-related methods. def acquire_lock(self): @@ -213,23 +218,6 @@ # Common operations on calendar data. - def update_senders(self, obj=None): - - """ - Update sender details in 'obj', or the current object if not indicated, - removing SENT-BY attributes for attendees other than the current user if - those attributes give the URI of the calendar system. - """ - - obj = obj or self.obj - calendar_uri = self.messenger and get_uri(self.messenger.sender) - for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")): - if attendee != self.user: - if attendee_attr.get("SENT-BY") == calendar_uri: - del attendee_attr["SENT-BY"] - else: - attendee_attr["SENT-BY"] = calendar_uri - def update_sender(self, attr): "Update the SENT-BY attribute of the 'attr' sender metadata." @@ -237,21 +225,6 @@ if self.messenger and self.messenger.sender != get_address(self.user): attr["SENT-BY"] = get_uri(self.messenger.sender) - def get_sending_attendee(self): - - "Return the attendee who sent the current object." - - # Search for the sender of the message or the calendar system address. - - senders = self.senders or self.messenger and [self.messenger.sender] or [] - - for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")): - if get_address(attendee) in senders or \ - get_address(attendee_attr.get("SENT-BY")) in senders: - return get_uri(attendee) - - return None - def get_periods(self, obj, explicit_only=False): """ @@ -371,31 +344,13 @@ return methods, responses - def get_unscheduled_parts(self, periods): - - "Return message parts describing unscheduled 'periods'." - - unscheduled_parts = [] - - if periods: - obj = self.obj.copy() - obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) - - for p in periods: - if not p.origin: - continue - obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())] - obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())] - unscheduled_parts.append(obj.to_part("CANCEL")) - - return unscheduled_parts - class ClientForObject(Client): "A client maintaining a specific object." - def __init__(self, obj, user, messenger=None, store=None, publisher=None, preferences_dir=None): - Client.__init__(self, user, messenger, store, publisher, preferences_dir) + def __init__(self, obj, user, messenger=None, store=None, publisher=None, + journal=None, preferences_dir=None): + Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir) self.set_object(obj) def set_object(self, obj): @@ -433,6 +388,59 @@ return get_uri(self.obj.get_value("ORGANIZER")) == self.user + # Common operations on calendar data. + + def update_senders(self, obj=None): + + """ + Update sender details in 'obj', or the current object if not indicated, + removing SENT-BY attributes for attendees other than the current user if + those attributes give the URI of the calendar system. + """ + + obj = obj or self.obj + calendar_uri = self.messenger and get_uri(self.messenger.sender) + for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")): + if attendee != self.user: + if attendee_attr.get("SENT-BY") == calendar_uri: + del attendee_attr["SENT-BY"] + else: + attendee_attr["SENT-BY"] = calendar_uri + + def get_sending_attendee(self): + + "Return the attendee who sent the current object." + + # Search for the sender of the message or the calendar system address. + + senders = self.senders or self.messenger and [self.messenger.sender] or [] + + for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")): + if get_address(attendee) in senders or \ + get_address(attendee_attr.get("SENT-BY")) in senders: + return get_uri(attendee) + + return None + + def get_unscheduled_parts(self, periods): + + "Return message parts describing unscheduled 'periods'." + + unscheduled_parts = [] + + if periods: + obj = self.obj.copy() + obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) + + for p in periods: + if not p.origin: + continue + obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())] + obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())] + unscheduled_parts.append(obj.to_part("CANCEL")) + + return unscheduled_parts + # Object update methods. def update_recurrenceid(self): diff -r ed312d199bb7 -r a12150034cbd imiptools/config.py --- a/imiptools/config.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/config.py Mon Feb 08 00:14:53 2016 +0100 @@ -26,6 +26,10 @@ PREFERENCES_DIR = "/var/lib/imip-agent/preferences" +# The location of quota-related journal information. + +JOURNAL_DIR = "/var/lib/imip-agent/journal" + # Permissions for files. # This is meant to ensure that both the agent and Web users can access files. diff -r ed312d199bb7 -r a12150034cbd imiptools/handlers/__init__.py --- a/imiptools/handlers/__init__.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/handlers/__init__.py Mon Feb 08 00:14:53 2016 +0100 @@ -3,7 +3,7 @@ """ General handler support for incoming calendar objects. -Copyright (C) 2014, 2015 Paul Boddie +Copyright (C) 2014, 2015, 2016 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -44,7 +44,7 @@ "General handler support." def __init__(self, senders=None, recipient=None, messenger=None, store=None, - publisher=None, preferences_dir=None): + publisher=None, journal=None, preferences_dir=None): """ Initialise the handler with any specifically indicated 'senders' and @@ -53,11 +53,12 @@ The optional 'messenger' provides a means of interacting with the mail system. - The optional 'store' and 'publisher' can be specified to override the - default store and publisher objects. + The optional 'store', 'publisher' and 'journal' can be specified to + override the default store and publisher objects. """ - ClientForObject.__init__(self, None, recipient and get_uri(recipient), messenger, store, publisher, preferences_dir) + ClientForObject.__init__(self, None, recipient and get_uri(recipient), + messenger, store, publisher, journal, preferences_dir) self.senders = senders and set(map(get_address, senders)) self.recipient = recipient and get_address(recipient) diff -r ed312d199bb7 -r a12150034cbd imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/handlers/resource.py Mon Feb 08 00:14:53 2016 +0100 @@ -22,7 +22,8 @@ from imiptools.data import get_address, to_part, uri_dict from imiptools.handlers import Handler from imiptools.handlers.common import CommonFreebusy, CommonEvent -from imiptools.handlers.scheduling import apply_scheduling_functions +from imiptools.handlers.scheduling import apply_scheduling_functions, \ + confirm_scheduling, retract_scheduling class ResourceHandler(CommonEvent, Handler): @@ -77,6 +78,10 @@ self.update_event_in_freebusy(for_organiser=False) + # Confirm the scheduling of the recurrence. + + self.confirm_scheduling() + def _schedule_for_attendee(self): "Attempt to schedule the current object for the current user." @@ -108,6 +113,11 @@ else: self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) + # Confirm any scheduling. + + if scheduled == "ACCEPTED": + self.confirm_scheduling() + # For countered proposals, record the offer in the resource's # free/busy collection. @@ -145,6 +155,10 @@ self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) self.store.cancel_event(self.user, self.uid, self.recurrenceid) + # Retract the scheduling of the event. + + self.retract_scheduling() + def _revoke_for_attendee(self): "Revoke any counter-proposal recorded as a free/busy offer." @@ -169,6 +183,24 @@ return apply_scheduling_functions(functions, self) + def confirm_scheduling(self): + + "Confirm that this event has been scheduled." + + functions = self.get_preferences().get("confirmation_function") + + if functions: + confirm_scheduling(functions.split("\n"), self) + + def retract_scheduling(self): + + "Retract this event from scheduling records." + + functions = self.get_preferences().get("retraction_function") + + if functions: + retract_scheduling(functions.split("\n"), self) + class Event(ResourceHandler): "An event handler." diff -r ed312d199bb7 -r a12150034cbd imiptools/handlers/scheduling/__init__.py --- a/imiptools/handlers/scheduling/__init__.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/handlers/scheduling/__init__.py Mon Feb 08 00:14:53 2016 +0100 @@ -20,8 +20,11 @@ """ from imiptools.text import parse_line -from imiptools.handlers.scheduling.manifest import scheduling_functions -import re +from imiptools.handlers.scheduling.manifest import confirmation_functions, \ + retraction_functions, \ + scheduling_functions + +# Function application/invocation. def apply_scheduling_functions(functions, handler): @@ -32,7 +35,7 @@ # Obtain the actual scheduling functions with arguments. - functions = get_scheduling_function_calls(functions) + functions = get_function_calls(functions, scheduling_functions) response = "ACCEPTED" @@ -62,7 +65,49 @@ return response -def get_scheduling_function_calls(lines): +def confirm_scheduling(functions, handler): + + """ + Confirm scheduling using the given listener 'functions' for the current + object of the given 'handler'. + """ + + # Obtain the actual listener functions with arguments. + + functions = get_function_calls(functions, confirmation_functions) + apply_functions(functions, handler) + +def retract_scheduling(functions, handler): + + """ + Retract scheduling using the given listener 'functions' for the current + object of the given 'handler'. + """ + + # Obtain the actual listener functions with arguments. + + functions = get_function_calls(functions, retraction_functions) + apply_functions(functions, handler) + +def apply_functions(functions, handler): + + """ + Apply the given notification 'functions' for the current object of the given + 'handler'. + """ + + for fn, args in functions: + + # NOTE: Should signal an error for incorrectly configured resources. + + if not fn: + continue + + fn(handler, args) + +# Function lookup. + +def get_function_calls(lines, registry): """ Parse the given 'lines', returning a list of (function, arguments) tuples, @@ -72,13 +117,16 @@ Each of the 'lines' should employ the function name and argument strings separated by whitespace, with any whitespace inside arguments quoted using single or double quotes. + + The given 'registry' indicates the mapping from function names to actual + functions. """ functions = [] for line in lines: parts = parse_line(line) - functions.append((scheduling_functions.get(parts[0]), parts[1:])) + functions.append((registry.get(parts[0]), parts[1:])) return functions diff -r ed312d199bb7 -r a12150034cbd imiptools/handlers/scheduling/access.py --- a/imiptools/handlers/scheduling/access.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/handlers/scheduling/access.py Mon Feb 08 00:14:53 2016 +0100 @@ -132,4 +132,9 @@ "same_domain_only" : same_domain_only, } +# Registries of listener functions. + +confirmation_functions = {} +retraction_functions = {} + # vim: tabstop=4 expandtab shiftwidth=4 diff -r ed312d199bb7 -r a12150034cbd imiptools/handlers/scheduling/freebusy.py --- a/imiptools/handlers/scheduling/freebusy.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/handlers/scheduling/freebusy.py Mon Feb 08 00:14:53 2016 +0100 @@ -42,8 +42,8 @@ periods = handler.get_periods(handler.obj) - freebusy = freebusy or handler.store.get_freebusy(handler.user) - offers = handler.store.get_freebusy_offers(handler.user) + freebusy = freebusy or handler.get_store().get_freebusy(handler.user) + offers = handler.get_store().get_freebusy_offers(handler.user) # Check the periods against any scheduled events and against # any outstanding offers. @@ -105,7 +105,7 @@ # There should already be free/busy information for the user. - user_freebusy = handler.store.get_freebusy(handler.user) + user_freebusy = handler.get_store().get_freebusy(handler.user) busy = user_freebusy # Subtract any periods from this event from the free/busy collections. @@ -116,7 +116,7 @@ for attendee in uri_values(handler.obj.get_values("ATTENDEE")): if attendee != handler.user: - freebusy = handler.store.get_freebusy_for_other(handler.user, attendee) + freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee) if freebusy: remove_periods(freebusy, event_periods) busy += freebusy @@ -217,4 +217,9 @@ "schedule_next_available_in_freebusy" : schedule_next_available_in_freebusy, } +# Registries of listener functions. + +confirmation_functions = {} +retraction_functions = {} + # vim: tabstop=4 expandtab shiftwidth=4 diff -r ed312d199bb7 -r a12150034cbd imiptools/handlers/scheduling/manifest.py --- a/imiptools/handlers/scheduling/manifest.py Sun Feb 07 23:35:20 2016 +0100 +++ b/imiptools/handlers/scheduling/manifest.py Mon Feb 08 00:14:53 2016 +0100 @@ -1,5 +1,31 @@ +confirmation_functions = {} +retraction_functions = {} scheduling_functions = {} -from imiptools.handlers.scheduling.freebusy import scheduling_functions as l -scheduling_functions.update(l) -from imiptools.handlers.scheduling.access import scheduling_functions as l -scheduling_functions.update(l) + +from imiptools.handlers.scheduling.quota import ( + confirmation_functions as c, + retraction_functions as r, + scheduling_functions as s) + +confirmation_functions.update(c) +retraction_functions.update(r) +scheduling_functions.update(s) + +from imiptools.handlers.scheduling.freebusy import ( + confirmation_functions as c, + retraction_functions as r, + scheduling_functions as s) + +confirmation_functions.update(c) +retraction_functions.update(r) +scheduling_functions.update(s) + +from imiptools.handlers.scheduling.access import ( + confirmation_functions as c, + retraction_functions as r, + scheduling_functions as s) + +confirmation_functions.update(c) +retraction_functions.update(r) +scheduling_functions.update(s) + diff -r ed312d199bb7 -r a12150034cbd imiptools/handlers/scheduling/quota.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imiptools/handlers/scheduling/quota.py Mon Feb 08 00:14:53 2016 +0100 @@ -0,0 +1,322 @@ +#!/usr/bin/env python + +""" +Quota-related scheduling functionality. + +Copyright (C) 2016 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from imiptools.dates import format_duration, get_duration +from imiptools.data import get_uri +from imiptools.period import Endless +from datetime import timedelta + +# Quota maintenance. + +def check_quota(handler, args): + + """ + Check the current object of the given 'handler' against the applicable + quota. + """ + + quota, group = _get_quota_and_group(handler, args) + + # Obtain the journal entries and check the balance. + + journal = handler.get_journal() + entries = journal.get_entries(quota, group) + limits = journal.get_limits(quota) + + # Obtain a limit for the group or any general limit. + # Decline invitations if no limit has been set. + + limit = limits.get(group) or limits.get("*") + if not limit: + return "DECLINED" + + # Decline events whose durations exceed the balance. + + total = _get_duration(handler) + + if total == Endless(): + return "DECLINED" + + balance = get_duration(limit) - _get_usage(entries) + + if total > balance: + return "DECLINED" + else: + return "ACCEPTED" + +def add_to_quota(handler, args): + + """ + Record details of the current object of the given 'handler' in the + applicable quota. + """ + + quota, group = _get_quota_and_group(handler, args) + + total = _get_duration(handler) + + # Obtain the journal entries and limits. + + journal = handler.get_journal() + journal.acquire_lock(quota) + + try: + entries = journal.get_entries(quota, group) + if _add_to_entries(entries, handler.obj.get_uid(), handler.obj.get_recurrenceid(), format_duration(total)): + journal.set_entries(quota, group, entries) + + finally: + journal.release_lock(quota) + +def remove_from_quota(handler, args): + + """ + Remove details of the current object of the given 'handler' from the + applicable quota. + """ + + quota, group = _get_quota_and_group(handler, args) + + total = _get_duration(handler) + + # Obtain the journal entries and limits. + + journal = handler.get_journal() + journal.acquire_lock(quota) + + try: + entries = journal.get_entries(quota, group) + if _remove_from_entries(entries, handler.obj.get_uid(), handler.obj.get_recurrenceid(), format_duration(total)): + journal.set_entries(quota, group, entries) + + finally: + journal.release_lock(quota) + +def _get_quota_and_group(handler, args): + + """ + Combine information about the current object from the 'handler' with the + given 'args' to return a tuple containing the quota group and the user + identity or group involved. + """ + + quota = args and args[0] or handler.user + + # Obtain the identity to whom the quota will apply. + + organiser = get_uri(handler.obj.get_value("ORGANIZER")) + + # Obtain any user group to which the quota will apply instead. + + journal = handler.get_journal() + groups = journal.get_groups(quota) + + return quota, groups.get(organiser) or organiser + +def _get_duration(handler): + + "Return the duration of the current object provided by the 'handler'." + + # Count only explicit periods. + # NOTE: Should reject indefinitely recurring events. + + total = timedelta(0) + + for period in handler.get_periods(handler.obj, explicit_only=True): + duration = period.get_duration() + + # Decline events whose period durations are endless. + + if duration == Endless(): + return duration + else: + total += duration + + return total + +def _get_usage(entries): + + "Return the usage total according to the given 'entries'." + + total = timedelta(0) + + for found_uid, found_recurrenceid, found_duration in entries: + retraction = found_duration.startswith("-") + multiplier = retraction and -1 or 1 + total += multiplier * get_duration(found_duration[retraction and 1 or 0:]) + + return total + +def _add_to_entries(entries, uid, recurrenceid, duration): + + """ + Add to 'entries' an entry for the event having the given 'uid' and + 'recurrenceid' with the given 'duration'. + """ + + confirmed = _find_applicable_entry(entries, uid, recurrenceid, duration) + + # Where a previous entry still applies, retract it if different. + + if confirmed: + found_uid, found_recurrenceid, found_duration = confirmed + if found_duration != duration: + entries.append((found_uid, found_recurrenceid, "-%s" % found_duration)) + else: + return False + + # Without an applicable previous entry, add a new entry. + + entries.append((uid, recurrenceid, duration)) + return True + +def _remove_from_entries(entries, uid, recurrenceid, duration): + + """ + Remove from the given 'entries' any entry for the event having the given + 'uid' and 'recurrenceid' with the given 'duration'. + """ + + confirmed = _find_applicable_entry(entries, uid, recurrenceid, duration) + + # Where a previous entry still applies, retract it. + + if confirmed: + found_uid, found_recurrenceid, found_duration = confirmed + entries.append((found_uid, found_recurrenceid, "-%s" % found_duration)) + return found_duration == duration + + return False + +def _find_applicable_entry(entries, uid, recurrenceid, duration): + + """ + Within 'entries', find any applicable previous entry for this event, + using the 'uid', 'recurrenceid' and 'duration'. + """ + + confirmed = None + + for found_uid, found_recurrenceid, found_duration in entries: + if uid == found_uid and recurrenceid == found_recurrenceid: + if found_duration.startswith("-"): + confirmed = None + else: + confirmed = found_uid, found_recurrenceid, found_duration + + return confirmed + +# Collective free/busy maintenance. + +def schedule_across_quota(handler, args): + + """ + Check the current object of the given 'handler' against the schedules + managed by the quota. + """ + + quota, organiser = _get_quota_and_identity(handler, args) + + # If newer than any old version, discard old details from the + # free/busy record and check for suitability. + + periods = handler.get_periods(handler.obj) + freebusy = handler.get_journal().get_freebusy(quota, organiser) + scheduled = handler.can_schedule(freebusy, periods) + + return scheduled and "ACCEPTED" or "DECLINED" + +def add_to_quota_freebusy(handler, args): + + """ + Record details of the current object of the 'handler' in the applicable + free/busy resource. + """ + + quota, organiser = _get_quota_and_identity(handler, args) + + journal = handler.get_journal() + journal.acquire_lock(quota) + + try: + freebusy = journal.get_freebusy(quota, organiser) + handler.update_freebusy(freebusy, organiser, True) + journal.set_freebusy(quota, organiser, freebusy) + + finally: + journal.release_lock(quota) + +def remove_from_quota_freebusy(handler, args): + + """ + Remove details of the current object of the 'handler' from the applicable + free/busy resource. + """ + + quota, organiser = _get_quota_and_identity(handler, args) + + journal = handler.get_journal() + journal.acquire_lock(quota) + + try: + freebusy = journal.get_freebusy(quota, organiser) + handler.remove_from_freebusy(freebusy) + journal.set_freebusy(quota, organiser, freebusy) + + finally: + journal.release_lock(quota) + +def _get_quota_and_identity(handler, args): + + """ + Combine information about the current object from the 'handler' with the + given 'args' to return a tuple containing the quota group and the user + identity involved. + """ + + quota = args and args[0] or handler.user + + # Obtain the identity for whom the scheduling will apply. + + organiser = get_uri(handler.obj.get_value("ORGANIZER")) + + return quota, organiser + +# Registry of scheduling functions. + +scheduling_functions = { + "check_quota" : check_quota, + "schedule_across_quota" : schedule_across_quota, + } + +# Registries of listener functions. + +confirmation_functions = { + "add_to_quota" : add_to_quota, + "add_to_quota_freebusy" : add_to_quota_freebusy, + } + +retraction_functions = { + "remove_from_quota" : remove_from_quota, + "remove_from_quota_freebusy" : remove_from_quota_freebusy, + } + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r ed312d199bb7 -r a12150034cbd tests/common.sh --- a/tests/common.sh Sun Feb 07 23:35:20 2016 +0100 +++ b/tests/common.sh Mon Feb 08 00:14:53 2016 +0100 @@ -6,8 +6,9 @@ STORE=/tmp/store STATIC=/tmp/static PREFS=/tmp/prefs +JOURNAL=/tmp/journal -ARGS="-S $STORE -P $STATIC -p $PREFS -d" +ARGS="-S $STORE -P $STATIC -p $PREFS -j $JOURNAL -d" ACCEPT_SCRIPT="$THIS_DIR/test_handle.py" ACCEPT_ARGS="accept $STORE $PREFS" @@ -38,8 +39,9 @@ PYTHONPATH="$BASE_DIR" export PYTHONPATH -rm -rf $STORE -rm -rf $STATIC -rm -rf $PREFS -rm -f $ERROR +rm -rf "$STORE" +rm -rf "$STATIC" +rm -rf "$PREFS" +rm -rf "$JOURNAL" +rm -f "$ERROR" rm -f out*.tmp diff -r ed312d199bb7 -r a12150034cbd tests/templates/event-cancel-car.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-cancel-car.txt Mon Feb 08 00:14:53 2016 +0100 @@ -0,0 +1,36 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-car-porsche911@example.com +Subject: Cancellation! + +Cancel the event completely. + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="CANCEL" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:CANCEL +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-car-porsche911@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T160000 +DTEND;TZID=Europe/Oslo:20141126T170000 +SUMMARY:Test drive +UID:event21@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r ed312d199bb7 -r a12150034cbd tests/templates/event-request-car-conflict.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-car-conflict.txt Mon Feb 08 00:14:53 2016 +0100 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-car-fiat500@example.com +Subject: Invitation! + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-car-fiat500@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T163000 +DTEND;TZID=Europe/Oslo:20141126T173000 +SUMMARY:Test drive +UID:event22@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r ed312d199bb7 -r a12150034cbd tests/templates/event-request-car-moved.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-car-moved.txt Mon Feb 08 00:14:53 2016 +0100 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-car-porsche911@example.com +Subject: Invitation! + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-car-porsche911@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T153000 +DTEND;TZID=Europe/Oslo:20141126T163000 +SUMMARY:Test drive +UID:event21@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r ed312d199bb7 -r a12150034cbd tests/templates/event-request-car.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-car.txt Mon Feb 08 00:14:53 2016 +0100 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-car-porsche911@example.com +Subject: Invitation! + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-car-porsche911@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T160000 +DTEND;TZID=Europe/Oslo:20141126T170000 +SUMMARY:Test drive +UID:event21@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r ed312d199bb7 -r a12150034cbd tests/templates/fb-request-car-other.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/fb-request-car-other.txt Mon Feb 08 00:14:53 2016 +0100 @@ -0,0 +1,31 @@ +Content-Type: multipart/alternative; boundary="===============0945993647==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-car-fiat500@example.com + +--===============0945993647== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains a free/busy request. +--===============0945993647== +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VFREEBUSY +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE:mailto:resource-car-fiat500@example.com +DTSTAMP:20141125T164400Z +DTSTART:20141126T150000Z +DTEND:20141126T180000Z +UID:fb6@example.com +END:VFREEBUSY +END:VCALENDAR + +--===============0945993647==-- diff -r ed312d199bb7 -r a12150034cbd tests/test_resource_invitation_constraints_quota.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_resource_invitation_constraints_quota.sh Mon Feb 08 00:14:53 2016 +0100 @@ -0,0 +1,305 @@ +#!/bin/sh + +. "`dirname \"$0\"`/common.sh" + +USER1="mailto:resource-car-porsche911@example.com" +USER2="mailto:resource-car-fiat500@example.com" +SENDER="mailto:paul.boddie@example.com" +FBFILE1="$STORE/$USER1/freebusy" +FBFILE2="$STORE/$USER2/freebusy" +FBSENDERFILE="$STORE/$SENDER/freebusy" +QUOTA=cars +JOURNALFILE="$JOURNAL/$QUOTA/journal/$SENDER" + +mkdir -p "$PREFS/$USER1" +echo 'Europe/Oslo' > "$PREFS/$USER1/TZID" +echo 'share' > "$PREFS/$USER1/freebusy_sharing" +cat > "$PREFS/$USER1/scheduling_function" < "$PREFS/$USER1/confirmation_function" < "$PREFS/$USER1/retraction_function" < "$PREFS/$USER2/TZID" +echo 'share' > "$PREFS/$USER2/freebusy_sharing" +cat > "$PREFS/$USER2/scheduling_function" < "$PREFS/$USER2/confirmation_function" < "$PREFS/$USER2/retraction_function" < "$JOURNAL/$QUOTA/limits" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out0.tmp + + grep -q 'METHOD:REPLY' out0.tmp \ +&& ! grep -q '^FREEBUSY' out0.tmp \ +&& echo "Success" \ +|| echo "Failed" + +# Attempt to schedule an event. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR + + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR \ +| tee out1r.tmp \ +| "$SHOWMAIL" \ +> out1.tmp + + grep -q 'METHOD:REPLY' out1.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out1.tmp \ +&& echo "Success" \ +|| echo "Failed" + + [ -e "$FBFILE1" ] \ +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is confirmed). + + [ -e "$JOURNALFILE" ] \ +&& grep -q "event21@example.com" "$JOURNALFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Attempt to schedule another event. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-conflict.txt" 2>> $ERROR + + grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-conflict.txt" 2>> $ERROR \ +| tee out2r.tmp \ +| "$SHOWMAIL" \ +> out2.tmp + + grep -q 'METHOD:REPLY' out2.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out2.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! [ -e "$FBFILE2" ] \ +|| ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBFILE2" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is not confirmed). + + [ -e "$JOURNALFILE" ] \ +&& grep -q "event21@example.com" "$JOURNALFILE" \ +&& ! grep -q "event22@example.com" "$JOURNALFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Increase the quota. + +echo '*\tPT2H' > "$JOURNAL/$QUOTA/limits" + +# Attempt to schedule the event again. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-conflict.txt" 2>> $ERROR + + grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-conflict.txt" 2>> $ERROR \ +| tee out3r.tmp \ +| "$SHOWMAIL" \ +> out3.tmp + + grep -q 'METHOD:REPLY' out3.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out3.tmp \ +&& echo "Success" \ +|| echo "Failed" + + [ -e "$FBFILE2" ] \ +&& grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBFILE2" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is confirmed). + + [ -e "$JOURNALFILE" ] \ +&& grep -q "event21@example.com" "$JOURNALFILE" \ +&& grep -q "event22@example.com" "$JOURNALFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Cancel the first event. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-car.txt" 2>> $ERROR + + ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-car.txt" 2>> $ERROR \ +| tee out4r.tmp \ +| "$SHOWMAIL" \ +> out4.tmp + + ! grep -q 'METHOD:REPLY' out4.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is retracted). + + [ -e "$JOURNALFILE" ] \ +&& [ `grep "event21@example.com" "$JOURNALFILE" | wc -l` = '2' ] \ +&& grep -q "event22@example.com" "$JOURNALFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Add collective scheduling tests. + +cat > "$PREFS/$USER1/scheduling_function" < "$PREFS/$USER1/confirmation_function" < "$PREFS/$USER1/retraction_function" < "$PREFS/$USER2/scheduling_function" < "$PREFS/$USER2/confirmation_function" < "$PREFS/$USER2/retraction_function" <> $ERROR \ +| tee out5r.tmp \ +| "$SHOWMAIL" \ +> out5.tmp + + grep -q 'METHOD:REPLY' out5.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out5.tmp \ +&& echo "Success" \ +|| echo "Failed" + + [ -e "$FBFILE2" ] \ +&& grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBFILE2" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is still confirmed). + + [ -e "$JOURNALFILE" ] \ +&& [ `grep "event21@example.com" "$JOURNALFILE" | wc -l` = '2' ] \ +&& [ `grep "event22@example.com" "$JOURNALFILE" | wc -l` = '1' ] \ +&& echo "Success" \ +|| echo "Failed" + +# Attempt to schedule the first event. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR + + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR \ +| tee out6r.tmp \ +| "$SHOWMAIL" \ +> out6.tmp + + grep -q 'METHOD:REPLY' out6.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out6.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is still retracted and not newly confirmed). + + [ -e "$JOURNALFILE" ] \ +&& [ `grep "event21@example.com" "$JOURNALFILE" | wc -l` = '2' ] \ +&& [ `grep "event22@example.com" "$JOURNALFILE" | wc -l` = '1' ] \ +&& echo "Success" \ +|| echo "Failed" + +# Attempt to schedule the first event moved earlier. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-moved.txt" 2>> $ERROR + + ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \ +&& grep -q "^20141126T143000Z${TAB}20141126T153000Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-moved.txt" 2>> $ERROR \ +| tee out7r.tmp \ +| "$SHOWMAIL" \ +> out7.tmp + + grep -q 'METHOD:REPLY' out7.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out7.tmp \ +&& echo "Success" \ +|| echo "Failed" + + grep -q "^20141126T143000Z${TAB}20141126T153000Z" "$FBFILE1" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is newly confirmed). + + [ -e "$JOURNALFILE" ] \ +&& [ `grep "event21@example.com" "$JOURNALFILE" | wc -l` = '3' ] \ +&& [ `grep "event22@example.com" "$JOURNALFILE" | wc -l` = '1' ] \ +&& echo "Success" \ +|| echo "Failed" diff -r ed312d199bb7 -r a12150034cbd tools/fix.sh --- a/tools/fix.sh Sun Feb 07 23:35:20 2016 +0100 +++ b/tools/fix.sh Mon Feb 08 00:14:53 2016 +0100 @@ -32,7 +32,8 @@ chown -R "$USER" "$INSTALL_DIR" chgrp -R "$GROUP" "$INSTALL_DIR" -for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static ; do +for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static \ + "$INSTALL_DIR"/journal ; do chown -R "$USER" "$DIR" chgrp -R "$GROUP" "$DIR" chmod -R g+w "$DIR" diff -r ed312d199bb7 -r a12150034cbd tools/init.sh --- a/tools/init.sh Sun Feb 07 23:35:20 2016 +0100 +++ b/tools/init.sh Mon Feb 08 00:14:53 2016 +0100 @@ -23,8 +23,9 @@ Within the stored data directory (using $INSTALL_DIR as an example), the following directories are created: + * $INSTALL_DIR/journal + * $INSTALL_DIR/preferences * $INSTALL_DIR/store - * $INSTALL_DIR/preferences Within the published data directory (using $WEB_INSTALL_DIR as an example), the following directory is created: @@ -39,7 +40,8 @@ USER=${3:-$IMIP_AGENT_USER} GROUP=${4:-$IMIP_AGENT_GROUP} -for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static ; do +for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static \ + "$INSTALL_DIR"/journal ; do mkdir -p "$DIR" chown "$USER" "$DIR" chgrp "$GROUP" "$DIR" diff -r ed312d199bb7 -r a12150034cbd tools/init_user.sh --- a/tools/init_user.sh Sun Feb 07 23:35:20 2016 +0100 +++ b/tools/init_user.sh Mon Feb 08 00:14:53 2016 +0100 @@ -29,7 +29,8 @@ WEB_INSTALL_DIR=${3:-$WEB_INSTALL_DIR} USER=${4:-$IMIP_AGENT_USER} -for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static ; do +for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static \ + "$INSTALL_DIR"/journal ; do mkdir -p "$DIR/$CALENDAR_USER" chown "$USER" "$DIR/$CALENDAR_USER" chmod g+ws "$DIR/$CALENDAR_USER" diff -r ed312d199bb7 -r a12150034cbd tools/update_scheduling_modules.py --- a/tools/update_scheduling_modules.py Sun Feb 07 23:35:20 2016 +0100 +++ b/tools/update_scheduling_modules.py Mon Feb 08 00:14:53 2016 +0100 @@ -44,11 +44,26 @@ f = open(join(dirname, "manifest.py"), "w") try: - print >>f, "scheduling_functions = {}" + print >>f, """\ +confirmation_functions = {} +retraction_functions = {} +scheduling_functions = {} +""" + for filename in filenames: module = splitext(filename)[0] - print >>f, "from imiptools.handlers.scheduling.%s import scheduling_functions as l" % module - print >>f, "scheduling_functions.update(l)" + + print >>f, """\ +from imiptools.handlers.scheduling.%s import ( + confirmation_functions as c, + retraction_functions as r, + scheduling_functions as s) + +confirmation_functions.update(c) +retraction_functions.update(r) +scheduling_functions.update(s) +""" % module + finally: f.close()