imip-agent

Changeset

1150:52476453b1e3
2016-04-22 Paul Boddie raw files shortlog changelog graph Merged freebusy-collections into the default branch.
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/conf/postgresql/schema.sql	Fri Apr 22 16:22:58 2016 +0200
     1.3 @@ -0,0 +1,163 @@
     1.4 +-- Object store tables.
     1.5 +
     1.6 +create table objects (
     1.7 +    store_user varchar not null,
     1.8 +    object_uid varchar not null,
     1.9 +    object_text varchar not null,
    1.10 +    status varchar not null, -- 'active', 'cancelled'
    1.11 +    primary key(store_user, object_uid)
    1.12 +);
    1.13 +
    1.14 +create table countered_objects (
    1.15 +    store_user varchar not null,
    1.16 +    other varchar not null,
    1.17 +    object_uid varchar not null,
    1.18 +    object_text varchar not null,
    1.19 +    primary key(store_user, object_uid)
    1.20 +);
    1.21 +
    1.22 +create table recurrences (
    1.23 +    store_user varchar not null,
    1.24 +    object_uid varchar not null,
    1.25 +    object_recurrenceid varchar not null,
    1.26 +    object_text varchar not null,
    1.27 +    status varchar not null, -- 'active', 'cancelled'
    1.28 +    primary key(store_user, object_uid, object_recurrenceid)
    1.29 +);
    1.30 +
    1.31 +create table countered_recurrences (
    1.32 +    store_user varchar not null,
    1.33 +    other varchar not null,
    1.34 +    object_uid varchar not null,
    1.35 +    object_recurrenceid varchar not null,
    1.36 +    object_text varchar not null,
    1.37 +    primary key(store_user, object_uid, object_recurrenceid)
    1.38 +);
    1.39 +
    1.40 +-- Object store free/busy details.
    1.41 +
    1.42 +create table freebusy (
    1.43 +    store_user varchar not null,
    1.44 +    "start" varchar not null,
    1.45 +    "end" varchar not null,
    1.46 +    object_uid varchar,
    1.47 +    transp varchar,
    1.48 +    object_recurrenceid varchar,
    1.49 +    summary varchar,
    1.50 +    organiser varchar,
    1.51 +    expires varchar
    1.52 +);
    1.53 +
    1.54 +create index freebusy_start on freebusy(store_user, "start");
    1.55 +create index freebusy_end on freebusy(store_user, "end");
    1.56 +
    1.57 +create table freebusy_offers (
    1.58 +    store_user varchar not null,
    1.59 +    "start" varchar not null,
    1.60 +    "end" varchar not null,
    1.61 +    object_uid varchar,
    1.62 +    transp varchar,
    1.63 +    object_recurrenceid varchar,
    1.64 +    summary varchar,
    1.65 +    organiser varchar,
    1.66 +    expires varchar
    1.67 +);
    1.68 +
    1.69 +create index freebusy_offers_start on freebusy_offers(store_user, "start");
    1.70 +create index freebusy_offers_end on freebusy_offers(store_user, "end");
    1.71 +
    1.72 +create table freebusy_other (
    1.73 +    store_user varchar not null,
    1.74 +    other varchar not null,
    1.75 +    "start" varchar not null,
    1.76 +    "end" varchar not null,
    1.77 +    object_uid varchar,
    1.78 +    transp varchar,
    1.79 +    object_recurrenceid varchar,
    1.80 +    summary varchar,
    1.81 +    organiser varchar,
    1.82 +    expires varchar
    1.83 +);
    1.84 +
    1.85 +create index freebusy_other_start on freebusy_other(store_user, other, "start");
    1.86 +create index freebusy_other_end on freebusy_other(store_user, other, "end");
    1.87 +
    1.88 +create table freebusy_providers (
    1.89 +    store_user varchar not null,
    1.90 +    object_uid varchar not null,
    1.91 +    object_recurrenceid varchar
    1.92 +);
    1.93 +
    1.94 +create index freebusy_providers_store_user on freebusy_providers(store_user);
    1.95 +
    1.96 +create table freebusy_provider_datetimes (
    1.97 +    store_user varchar not null,
    1.98 +    "start" varchar
    1.99 +);
   1.100 +
   1.101 +create index freebusy_provider_datetimes_store_user on freebusy_provider_datetimes(store_user);
   1.102 +
   1.103 +-- Object store request details.
   1.104 +
   1.105 +create table requests (
   1.106 +    store_user varchar not null,
   1.107 +    object_uid varchar not null,
   1.108 +    object_recurrenceid varchar,
   1.109 +    request_type varchar
   1.110 +);
   1.111 +
   1.112 +create index requests_object_uid on requests(store_user, object_uid);
   1.113 +
   1.114 +
   1.115 +
   1.116 +-- Journal store tables.
   1.117 +
   1.118 +-- Journal free/busy details.
   1.119 +
   1.120 +create table quota_freebusy (
   1.121 +    quota varchar not null,
   1.122 +    user_group varchar not null,
   1.123 +    "start" varchar not null,
   1.124 +    "end" varchar not null,
   1.125 +    object_uid varchar,
   1.126 +    transp varchar,
   1.127 +    object_recurrenceid varchar,
   1.128 +    summary varchar,
   1.129 +    organiser varchar,
   1.130 +    expires varchar
   1.131 +);
   1.132 +
   1.133 +create index quota_freebusy_start on quota_freebusy(quota, user_group, "start");
   1.134 +create index quota_freebusy_end on quota_freebusy(quota, user_group, "end");
   1.135 +
   1.136 +create table user_freebusy (
   1.137 +    quota varchar not null,
   1.138 +    store_user varchar not null,
   1.139 +    "start" varchar not null,
   1.140 +    "end" varchar not null,
   1.141 +    object_uid varchar,
   1.142 +    transp varchar,
   1.143 +    object_recurrenceid varchar,
   1.144 +    summary varchar,
   1.145 +    organiser varchar,
   1.146 +    expires varchar
   1.147 +);
   1.148 +
   1.149 +create index user_freebusy_start on user_freebusy(quota, store_user, "start");
   1.150 +create index user_freebusy_end on user_freebusy(quota, store_user, "end");
   1.151 +
   1.152 +-- Journal user groups and limits.
   1.153 +
   1.154 +create table quota_limits (
   1.155 +    quota varchar not null,
   1.156 +    user_group varchar not null,
   1.157 +    quota_limit varchar not null,
   1.158 +    primary key(user_group)
   1.159 +);
   1.160 +
   1.161 +create table user_groups (
   1.162 +    quota varchar not null,
   1.163 +    store_user varchar not null,
   1.164 +    user_group varchar not null,
   1.165 +    primary key(store_user, user_group)
   1.166 +);
     2.1 --- a/docs/wiki/Administration	Tue Apr 19 21:20:57 2016 +0200
     2.2 +++ b/docs/wiki/Administration	Fri Apr 22 16:22:58 2016 +0200
     2.3 @@ -131,3 +131,38 @@
     2.4  use of this tool will be performed by the packaging system provided by an
     2.5  operating system distribution. The `tools/install.sh` script runs the above
     2.6  tool as part of the installation process.
     2.7 +
     2.8 +== Copying Stores and Changing Store Types ==
     2.9 +
    2.10 +A rudimentary tool is provided that can copy data between stores, even those of
    2.11 +different types, thus allowing the migration of data from one kind of store to
    2.12 +another. Although it does not perform the copying in the most efficient manner,
    2.13 +it provides a convenient method of copying that uses the software's own general
    2.14 +interfaces for store access and thus acts as a way of verifying that these are
    2.15 +functioning correctly.
    2.16 +
    2.17 +To copy a configured store to another filesystem location:
    2.18 +
    2.19 +{{{
    2.20 +tools/copy_store.py -t file /tmp/store /tmp/journal
    2.21 +}}}
    2.22 +
    2.23 +To copy a configured store to a database (which must have been initialised):
    2.24 +
    2.25 +{{{
    2.26 +tools/copy_store.py -t postgresql 'dbname=store' 'dbname=journal'
    2.27 +}}}
    2.28 +
    2.29 +To copy an explicitly-specified file store to another filesystem location:
    2.30 +
    2.31 +{{{
    2.32 +tools/copy_store.py \
    2.33 +  -f file /var/lib/imip-agent/store /var/lib/imip-agent/journal \
    2.34 +  -t file /tmp/store /tmp/journal
    2.35 +}}}
    2.36 +
    2.37 +The help message for the tool provides general guidance for its use:
    2.38 +
    2.39 +{{{
    2.40 +tools/copy_store.py --help
    2.41 +}}}
     3.1 --- a/docs/wiki/Configuration	Tue Apr 19 21:20:57 2016 +0200
     3.2 +++ b/docs/wiki/Configuration	Fri Apr 22 16:22:58 2016 +0200
     3.3 @@ -95,11 +95,15 @@
     3.4  
     3.5  === System-Level and Tool Configuration ===
     3.6  
     3.7 -Given a choice of [[../SystemUsers|system users and groups]], and the choices
     3.8 -made when configuring how imip-agent integrates with other components, the
     3.9 -resulting configuration must be indicated in the `config.sh` file. Since
    3.10 -the `tools/install.sh` script depends on this configuration, changes must
    3.11 -be made to the file in the `tools/config.sh` location before installation
    3.12 +The `config.sh` file must indicate choices in the following areas:
    3.13 +
    3.14 + * The [[../DataStore|data store]] type to be employed
    3.15 +
    3.16 + * The [[../SystemUsers|system users and groups]] involved with running the
    3.17 + software and storing data
    3.18 +
    3.19 +Since the `tools/install.sh` script depends on this configuration, changes
    3.20 +must be made to the file in the `tools/config.sh` location before installation
    3.21  can occur.
    3.22  
    3.23  {{{#!table
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/docs/wiki/DataStore	Fri Apr 22 16:22:58 2016 +0200
     4.3 @@ -0,0 +1,17 @@
     4.4 += Data Store =
     4.5 +
     4.6 +The data store for imip-agent holds calendar data and free/busy information.
     4.7 +The following data store types exist:
     4.8 +
     4.9 + * A [[../FileStore|file store]] employing textual files in the filesystem
    4.10 +
    4.11 + * A [[../DatabaseStore|database store]] employing database tables managed
    4.12 + by a database management system
    4.13 +
    4.14 +For simplicity, the file store is the default storage mechanism for imip-agent,
    4.15 +but the database store is provided as an alternative where different operating
    4.16 +characteristics are desired.
    4.17 +
    4.18 +The [[../Configuration|configuration]] files (`config.sh` and `config.py`) need
    4.19 +updating to reflect the choice of data store, with the directory parameters
    4.20 +set to appropriate values for the chosen store type.
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/docs/wiki/DatabaseStore	Fri Apr 22 16:22:58 2016 +0200
     5.3 @@ -0,0 +1,122 @@
     5.4 += Database Store =
     5.5 +
     5.6 +The database data store offers a mechanism for storing calendar objects and free/busy
     5.7 +details in a database system, currently focusing on relational database systems, with
     5.8 +specific support for PostgreSQL.
     5.9 +
    5.10 +Benefits of the database store include convenient and standardised interfaces for
    5.11 +querying and inspecting the data, together with various guarantees around the durability
    5.12 +of the stored data. However, more administrative knowledge is usually required to operate
    5.13 +database installations satisfactorily, and activities such as archiving require extra
    5.14 +planning and expertise.
    5.15 +
    5.16 +The [[../FileStore|file store]] offers a more convenient method of managing calendar
    5.17 +data, albeit offering arguably less scalability and less convenience when data volumes
    5.18 +become significantly large.
    5.19 +
    5.20 +== Configuration Settings ==
    5.21 +
    5.22 +The [[../Configuration|configuration]] files (`config.sh` and `config.py`) need to be
    5.23 +updated when choosing a database store.
    5.24 +
    5.25 +{{{#!table
    5.26 +'''File''' || '''Setting''' || '''Value''' || '''Description'''
    5.27 +==
    5.28 +`config.sh`
    5.29 +||<rowspan="2"> `STORE_TYPE`
    5.30 +||<rowspan="2"> `postgresql`
    5.31 +||<rowspan="2"> Selects a specific database store type; currently, only `postgresql`
    5.32 +             .. is supported
    5.33 +==
    5.34 +`config.py`
    5.35 +==
    5.36 +<rowspan="2"> `config.py`
    5.37 +|| `STORE_DIR`
    5.38 +|| `dbname=imip_agent`
    5.39 +|| Indicates a connection string for the store database
    5.40 +==
    5.41 +`JOURNAL_DIR`
    5.42 +|| `dbname=imip_agent`
    5.43 +|| Indicates a connection string for the journal database
    5.44 +}}}
    5.45 +
    5.46 +== Journal Structure ==
    5.47 +
    5.48 +The journal information is retained in a collection of tables. Unlike the structure of
    5.49 +the [[../FileStore|file store]] in the [[../FilesystemUsage|filesystem]], each table
    5.50 +contains details for all quota groups, with each query indicating the group to select
    5.51 +the appropriate information.
    5.52 +
    5.53 +{{{#!table
    5.54 +'''Table''' || '''Purpose'''
    5.55 +==
    5.56 +`quota_freebusy`
    5.57 +|| Period descriptions describing reservations for resources sharing a quota (`quota`)
    5.58 +.. made by users or groups (`user_group`), structured similarly to the `freebusy` table
    5.59 +.. in the data store
    5.60 +==
    5.61 +`quota_limits`
    5.62 +|| A mapping from user identities or group identifiers to quota limits
    5.63 +==
    5.64 +`user_freebusy`
    5.65 +|| Period descriptions for reservations made in the context of a quota (`quota`) by a
    5.66 +.. specific user (`store_user`), structured similarly to the `freebusy` table in the
    5.67 +.. data store
    5.68 +==
    5.69 +`user_groups`
    5.70 +|| A mapping from user identities to group identifiers indicating the sharing of a quota
    5.71 +.. across a number of users
    5.72 +}}}
    5.73 +
    5.74 +== Store Structure ==
    5.75 +
    5.76 +The store information is retained in a collection of tables. Unlike the structure of
    5.77 +the [[../FileStore|file store]] in the [[../FilesystemUsage|filesystem]], each table
    5.78 +contains details for all users, with each query indicating the user to select
    5.79 +the appropriate information.
    5.80 +
    5.81 +{{{#!table
    5.82 +'''Table''' || '''Purpose'''
    5.83 +==
    5.84 +`countered_objects`
    5.85 +|| Retains counter-proposals corresponding to events held in the `objects` table
    5.86 +.. received by each user (`store_user`) from another user (`other`)
    5.87 +==
    5.88 +`countered_recurrences`
    5.89 +|| Retains counter-proposals corresponding to events held in the `recurrences` table
    5.90 +.. received by each user (`store_user`) from another user (`other`)
    5.91 +==
    5.92 +`freebusy`
    5.93 +|| Period descriptions indicating start and end datetimes (in UTC), unique identifier
    5.94 +.. (`object_uid`), transparency, recurrence identifier (`object_recurrenceid`),
    5.95 +.. summary and organiser, reflecting the schedule of each user (`store_user`)
    5.96 +==
    5.97 +`freebusy_offers`
    5.98 +|| Period descriptions for scheduling offers made by counter-proposals sent by each
    5.99 +.. user (`store_user`)
   5.100 +==
   5.101 +`freebusy_other`
   5.102 +|| Period descriptions received or deduced for a user (`store_user`) structured
   5.103 +.. similarly to the user's own `freebusy` records
   5.104 +==
   5.105 +`freebusy_providers`
   5.106 +|| Details of [[../EventRecurrences|recurring events]] for which new free/busy records
   5.107 +.. must be [[../CronIntegration|periodically generated]] because these events recur
   5.108 +.. indefinitely, selectable for each user (`store_user`)
   5.109 +==
   5.110 +`freebusy_provider_datetimes`
   5.111 +|| Date/time details associated with the `freebusy_providers` information
   5.112 +==
   5.113 +`objects`
   5.114 +|| Records for each user (`store_data`) containing received event data (`object_text`)
   5.115 +==
   5.116 +`requests`
   5.117 +|| A collections of records, each belonging to a specific user (`store_user`)
   5.118 +.. consisting of a unique identifier (`object_uid`), normalised recurrence identifier
   5.119 +.. (`object_recurrenceid`) if appropriate, and (optionally) a scheduling method,
   5.120 +.. indicating the availability of an incoming scheduling request for handling by a user
   5.121 +==
   5.122 +`recurrences`
   5.123 +|| Records for each user (`store_data`) containing received recurrence event data
   5.124 +.. (`object_text`)
   5.125 +}}}
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/docs/wiki/FileStore	Fri Apr 22 16:22:58 2016 +0200
     6.3 @@ -0,0 +1,40 @@
     6.4 += File Store =
     6.5 +
     6.6 +The file data store is the default mechanism for storing calendar objects and
     6.7 +free/busy details, making use of various directories as described in the
     6.8 +[[../FilesystemUsage|filesystem usage guide]].
     6.9 +
    6.10 +Benefits of the file store include transparency and ease of administration:
    6.11 +all data is stored in text files, direct modification of certain files can be
    6.12 +performed to change the system's behaviour, archiving is possible using
    6.13 +traditional filesystem tools. However, the simple representation may make
    6.14 +certain operations costly, such as the modification of tabular data, and
    6.15 +querying of data may not always be particularly convenient.
    6.16 +
    6.17 +Thus, the [[../DatabaseStore|database store]] exists as an alternative, offering
    6.18 +different characteristics to those of the file store.
    6.19 +
    6.20 +== Configuration Settings ==
    6.21 +
    6.22 +The [[../Configuration|configuration]] files (`config.sh` and `config.py`) need to be
    6.23 +updated when choosing a file store.
    6.24 +
    6.25 +{{{#!table
    6.26 +'''File''' || '''Setting''' || '''Value''' || '''Description'''
    6.27 +==
    6.28 +`config.sh`
    6.29 +||<rowspan="2"> `STORE_TYPE`
    6.30 +||<rowspan="2"> `file`
    6.31 +||<rowspan="2"> Selects the default file storage type
    6.32 +==
    6.33 +`config.py`
    6.34 +==
    6.35 +<rowspan="2"> `config.py`
    6.36 +|| `STORE_DIR`
    6.37 +|| `/var/lib/imip-agent/store`
    6.38 +|| Indicates the filesystem location of the data store
    6.39 +==
    6.40 +`JOURNAL_DIR`
    6.41 +|| `/var/lib/imip-agent/journal`
    6.42 +|| Indicates the filesystem location of the journal
    6.43 +}}}
     7.1 --- a/docs/wiki/FilesystemUsage	Tue Apr 19 21:20:57 2016 +0200
     7.2 +++ b/docs/wiki/FilesystemUsage	Fri Apr 22 16:22:58 2016 +0200
     7.3 @@ -26,6 +26,11 @@
     7.4  Note that the free/busy resources are located in `/var/www` as opposed to
     7.5  `/var/lib` since they are intended to be published on the Web.
     7.6  
     7.7 +Meanwhile, the journal and store resources are only present in the filesystem
     7.8 +if the [[../FileStore|file store]] is in use. Where a
     7.9 +[[../DatabaseStore|database store]] is being used instead, such resources are
    7.10 +located in a database system.
    7.11 +
    7.12  == Journal Structure ==
    7.13  
    7.14  Within the journal directory are a collection of subdirectories, each of which
     8.1 --- a/docs/wiki/FrontPage	Tue Apr 19 21:20:57 2016 +0200
     8.2 +++ b/docs/wiki/FrontPage	Fri Apr 22 16:22:58 2016 +0200
     8.3 @@ -165,6 +165,7 @@
     8.4   * [[/CalendaringSupport|Calendaring Support]]
     8.5   * [[/CounterProposals|Counter-Proposals and Offers]]
     8.6   * [[/CronIntegration|Cron Task Scheduler Integration]]
     8.7 + * [[/DataStore|Data Store]]
     8.8   * [[/EventRecurrences|Event Recurrences]]
     8.9   * [[/FilesystemUsage|Filesystem Usage]]
    8.10   * [[/IncomingMessages|Incoming Messages]]
     9.1 --- a/docs/wiki/GettingStarted	Tue Apr 19 21:20:57 2016 +0200
     9.2 +++ b/docs/wiki/GettingStarted	Fri Apr 22 16:22:58 2016 +0200
     9.3 @@ -101,6 +101,10 @@
     9.4  || Postfix routing and transport configuration
     9.5  || [[../MailIntegration|E-Mail Integration]] and
     9.6  .. [[../MailboxIntegration|Mailbox Integration]]
     9.7 +==
     9.8 +`postgresql`
     9.9 +|| PostgreSQL configuration
    9.10 +|| [[../DatabaseStore|Database Store]]
    9.11  }}}
    9.12  
    9.13  In addition, a `tools` directory provides a configuration helper tool
    10.1 --- a/docs/wiki/MailIntegration--MTA	Tue Apr 19 21:20:57 2016 +0200
    10.2 +++ b/docs/wiki/MailIntegration--MTA	Fri Apr 22 16:22:58 2016 +0200
    10.3 @@ -77,3 +77,32 @@
    10.4  mailserver:example.com
    10.5  }}}
    10.6  }}}}
    10.7 +
    10.8 +== Useful Commands ==
    10.9 +
   10.10 +The following commands prove useful when troubleshooting and appear to be
   10.11 +available as shown within a Debian environment.
   10.12 +
   10.13 +{{{#!table
   10.14 +'''Task''' || '''Exim''' || '''Postfix'''
   10.15 +==
   10.16 +Check the mail queue
   10.17 +||<colspan="2"> `mailq`
   10.18 +==
   10.19 +Process the mail queue
   10.20 +|| `sendmail -q` (or `exim -q` or `runq`)
   10.21 +|| `sendmail -q` (or `postqueue`)
   10.22 +==
   10.23 +Flush the mail queue
   10.24 +|| `exim -qff`
   10.25 +|| `postqueue -f`
   10.26 +==
   10.27 +Deliver a specific message
   10.28 +|| `exim -M <identifier>`
   10.29 +|| `postqueue -i <identifier>`
   10.30 +==
   10.31 +Test delivery for an address
   10.32 +||<colspan="2"> `sendmail -bt <address>` (see also `sendmail -v -bv <address>` and `sendmail -v -bvs <address>`)
   10.33 +}}}
   10.34 +
   10.35 +See [[http://bradthemad.org/tech/notes/exim_cheatsheet.php|Exim Cheatsheet]] and [[http://www.postfix.org/DEBUG_README.html|Postfix Debugging Howto]] for more guidance.
    11.1 --- a/docs/wiki/Prerequisites	Tue Apr 19 21:20:57 2016 +0200
    11.2 +++ b/docs/wiki/Prerequisites	Fri Apr 22 16:22:58 2016 +0200
    11.3 @@ -11,6 +11,11 @@
    11.4   Python::  python
    11.5   pytz::    python-tz
    11.6  
    11.7 +If the [[../DatabaseStore|database store]] is used, the following packages are required:
    11.8 +
    11.9 + PostgreSQL:: postgresql-9.4 (version may vary between releases)
   11.10 + psycopg2:: python-psycopg2
   11.11 +
   11.12  To provide localised messages:
   11.13  
   11.14   gettext:: gettext
    12.1 --- a/docs/wiki/Resources	Tue Apr 19 21:20:57 2016 +0200
    12.2 +++ b/docs/wiki/Resources	Fri Apr 22 16:22:58 2016 +0200
    12.3 @@ -372,7 +372,8 @@
    12.4  
    12.5  ==== Initialising Quotas ====
    12.6  
    12.7 -Within the journal storage area (described in the [[../FilesystemUsage|filesystem guide]]),
    12.8 +Within the journal storage area (described in the
    12.9 +[[../FilesystemUsage|filesystem guide]] and [[../DatabaseStore|database guide]]),
   12.10  a quota group directory must be initialised with a `limits` file indicating
   12.11  the amount of time that can be occupied by the cumulative total of all events
   12.12  scheduled by an individual user or a group of which they are a member. For
   12.13 @@ -395,6 +396,35 @@
   12.14  particular user will be unable to reserve the resource unless defined as a
   12.15  member of a group listed in the `limits` file, as described below.
   12.16  
   12.17 +{{{{#!wiki tip
   12.18 +In a deployment using the [[../FileStore|file store]], files as described
   12.19 +above and below hold mappings and definitions in the given format. In a
   12.20 +deployment using the [[../DatabaseStore|database store]], database tables
   12.21 +hold such mappings with each column dedicated to a particular kind of
   12.22 +information.
   12.23 +
   12.24 +The examples here can be transcribed by just taking each
   12.25 +element and putting it in the appropriate column within a table, making
   12.26 +sure to set the `quota` column to indicate which quota is involved. For
   12.27 +example, to set the above limits in PostgreSQL, the following operations
   12.28 +may be used:
   12.29 +
   12.30 +{{{
   12.31 +insert into quota_limits (quota, user_group, quota_limit) values (
   12.32 +  'mailto:resource-car-cadillac@example.com',
   12.33 +  'mailto:vincent.vole@example.com',
   12.34 +  'PT10H');
   12.35 +insert into quota_limits (quota, user_group, quota_limit) values (
   12.36 +  'mailto:resource-car-cadillac@example.com',
   12.37 +  '*',
   12.38 +  'PT10H');
   12.39 +}}}
   12.40 +
   12.41 +Here, the `quota` column is set to `mailto:resource-car-cadillac@example.com`.
   12.42 +In a file-based journal, the equivalent `limits` file would be placed within a
   12.43 +quota directory having the name `mailto:resource-car-cadillac@example.com`.
   12.44 +}}}}
   12.45 +
   12.46  ==== Sharing Quotas Across Users ====
   12.47  
   12.48  When the use of resources is to be shared between users in such a way that
    13.1 --- a/docs/wiki/Testing	Tue Apr 19 21:20:57 2016 +0200
    13.2 +++ b/docs/wiki/Testing	Fri Apr 22 16:22:58 2016 +0200
    13.3 @@ -90,6 +90,27 @@
    13.4  are presented with message content, and testing for the desired effects of
    13.5  running those programs with such content.
    13.6  
    13.7 +{{{
    13.8 +./test_all.sh
    13.9 +}}}
   13.10 +
   13.11 +The test suite by default (or by indicating `file` as the data store type),
   13.12 +records test information in subdirectories of `/tmp`.
   13.13 +
   13.14 +To run all tests against a different data store, such as a
   13.15 +[[../DatabaseStore|database store]] instead of the [[../FileStore|file store]],
   13.16 +the `STORE_TYPE` environment variable can be specified as in the following
   13.17 +example:
   13.18 +
   13.19 +{{{
   13.20 +STORE_TYPE=postgresql ./test_all.sh
   13.21 +}}}
   13.22 +
   13.23 +The test suite records `postgresql` tests in a specially-created database
   13.24 +called `imip_agent_test` that the system user running the tests must be allowed
   13.25 +to create and drop. This may require the granting of administrative rights
   13.26 +within PostgreSQL for the system user concerned.
   13.27 +
   13.28  Individual tests may also be run directly from the topmost level of the
   13.29  source code distribution. For example:
   13.30  
   13.31 @@ -104,3 +125,50 @@
   13.32  output from various commands from the last test script invocation; the
   13.33  `err.tmp` will contain tracebacks indicating serious error conditions,
   13.34  should any have occurred.
   13.35 +
   13.36 +== Testing in the Deployment Environment ==
   13.37 +
   13.38 +Although the above testing may indicate that the software is functional,
   13.39 +it does not demonstrate that the software has been successfully integrated
   13.40 +into the deployment environment. One elementary test involves sending mail
   13.41 +to an address that should be configured to handle incoming calendar messages.
   13.42 +
   13.43 +A basic script is provided that replicates a subset of the functionality in
   13.44 +the traditional `mail` command for sending messages. It is invoked by
   13.45 +specifying the sender and recipients of a message and by passing the message
   13.46 +itself to the script's standard input. For example:
   13.47 +
   13.48 +{{{
   13.49 +tools/sendmail.py paul.boddie@example.com resource-room-confroom@example.com \
   13.50 +  < tests/templates/event-request.txt
   13.51 +}}}
   13.52 +
   13.53 +Here, the sender (`paul.boddie@example.com`) and recipient
   13.54 +(`resource-room-confroom@example.com`) must match the identities specified
   13.55 +within the supplied file (`tests/templates/event-request.txt`), and for a
   13.56 +specific deployment environment these identities will need to be changed.
   13.57 +The following lines from the supplied file would be involved:
   13.58 +
   13.59 +{{{
   13.60 +From: paul.boddie@example.com
   13.61 +To: resource-room-confroom@example.com
   13.62 +ORGANIZER:mailto:paul.boddie@example.com
   13.63 +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com
   13.64 +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com
   13.65 +}}}
   13.66 +
   13.67 +It makes most sense to choose a recipient acting as a resource so that an
   13.68 +automated response may be generated with the sender receiving this response.
   13.69 +However, other kinds of recipients may also be tested in this way.
   13.70 +
   13.71 +The result of this invocation will become known via the following sources of
   13.72 +information:
   13.73 +
   13.74 + * The sender's mailbox (if the recipient sends an automated response)
   13.75 + * The sender's data store
   13.76 + * The recipient's mailbox (if the recipient is configured to store mail)
   13.77 + * The recipient's data store
   13.78 + * Mail system logs (particularly in case of errors)
   13.79 +
   13.80 +See the [[../MailIntegration|mail integration guide]] for more information
   13.81 +about configuring and troubleshooting mail systems.
    14.1 --- a/imiptools/__init__.py	Tue Apr 19 21:20:57 2016 +0200
    14.2 +++ b/imiptools/__init__.py	Fri Apr 22 16:22:58 2016 +0200
    14.3 @@ -25,7 +25,7 @@
    14.4  from imiptools.content import handle_itip_part
    14.5  from imiptools.data import get_address, get_addresses, get_uri
    14.6  from imiptools.mail import Messenger
    14.7 -import imiptools.stores.file
    14.8 +from imiptools.stores import get_store, get_publisher, get_journal
    14.9  import sys, os
   14.10  
   14.11  # Postfix exit codes.
   14.12 @@ -56,6 +56,7 @@
   14.13          self.outgoing_only = outgoing_only
   14.14          self.messenger = None
   14.15          self.lmtp_socket = None
   14.16 +        self.store_type = None
   14.17          self.store_dir = None
   14.18          self.publishing_dir = None
   14.19          self.journal_dir = None
   14.20 @@ -63,13 +64,13 @@
   14.21          self.debug = False
   14.22  
   14.23      def get_store(self):
   14.24 -        return imiptools.stores.file.FileStore(self.store_dir)
   14.25 +        return get_store(self.store_type, self.store_dir)
   14.26  
   14.27      def get_publisher(self):
   14.28 -        return self.publishing_dir and imiptools.stores.file.FilePublisher(self.publishing_dir) or None
   14.29 +        return self.publishing_dir and get_publisher(self.publishing_dir) or None
   14.30  
   14.31      def get_journal(self):
   14.32 -        return imiptools.stores.file.FileJournal(self.journal_dir)
   14.33 +        return get_journal(self.store_type, self.journal_dir)
   14.34  
   14.35      def process(self, f, original_recipients):
   14.36  
   14.37 @@ -122,6 +123,7 @@
   14.38          recipients = []
   14.39          senders = []
   14.40          lmtp = []
   14.41 +        store_type = []
   14.42          store_dir = []
   14.43          publishing_dir = []
   14.44          preferences_dir = []
   14.45 @@ -152,6 +154,11 @@
   14.46              elif arg == "-L":
   14.47                  local_smtp = True
   14.48  
   14.49 +            # Switch to getting the store type.
   14.50 +
   14.51 +            elif arg == "-T":
   14.52 +                l = store_type
   14.53 +
   14.54              # Switch to getting the store directory.
   14.55  
   14.56              elif arg == "-S":
   14.57 @@ -179,11 +186,14 @@
   14.58              else:
   14.59                  l.append(arg)
   14.60  
   14.61 -        self.messenger = Messenger(lmtp_socket=lmtp and lmtp[0] or None, local_smtp=local_smtp, sender=senders and senders[0] or None)
   14.62 -        self.store_dir = store_dir and store_dir[0] or None
   14.63 -        self.publishing_dir = publishing_dir and publishing_dir[0] or None
   14.64 -        self.preferences_dir = preferences_dir and preferences_dir[0] or None
   14.65 -        self.journal_dir = journal_dir and journal_dir[0] or None
   14.66 +        getvalue = lambda value, default=None: value and value[0] or default
   14.67 +
   14.68 +        self.messenger = Messenger(lmtp_socket=getvalue(lmtp), local_smtp=local_smtp, sender=getvalue(senders))
   14.69 +        self.store_type = getvalue(store_type, config.STORE_TYPE)
   14.70 +        self.store_dir = getvalue(store_dir)
   14.71 +        self.publishing_dir = getvalue(publishing_dir)
   14.72 +        self.preferences_dir = getvalue(preferences_dir)
   14.73 +        self.journal_dir = getvalue(journal_dir)
   14.74          self.process(stream, original_recipients)
   14.75  
   14.76      def __call__(self):
   14.77 @@ -198,6 +208,7 @@
   14.78          if "--help" in args:
   14.79              print >>sys.stderr, """\
   14.80  Usage: %s [ -o <recipient> ... ] [-s <sender> ... ] [ -l <socket> | -L ] \\
   14.81 +         [ -T <store type ] \\
   14.82           [ -S <store directory> ] [ -P <publishing directory> ] \\
   14.83           [ -p <preferences directory> ] [ -j <journal directory> ] [ -d ]
   14.84  
   14.85 @@ -225,6 +236,7 @@
   14.86  -p  Indicates the location of user preference directories
   14.87  -S  Indicates the location of the calendar data store containing user storage
   14.88      directories
   14.89 +-T  Indicates the store and journal type (the configured value if omitted)
   14.90  
   14.91  Output options:
   14.92  
    15.1 --- a/imiptools/client.py	Tue Apr 19 21:20:57 2016 +0200
    15.2 +++ b/imiptools/client.py	Fri Apr 22 16:22:58 2016 +0200
    15.3 @@ -27,11 +27,8 @@
    15.4  from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
    15.5                              get_duration, get_timestamp
    15.6  from imiptools.i18n import get_translator
    15.7 -from imiptools.period import can_schedule, remove_event_periods, \
    15.8 -                             remove_additional_periods, remove_affected_period, \
    15.9 -                             update_freebusy
   15.10  from imiptools.profile import Preferences
   15.11 -import imiptools.stores.file
   15.12 +from imiptools.stores import get_store, get_publisher, get_journal
   15.13  
   15.14  class Client:
   15.15  
   15.16 @@ -51,11 +48,11 @@
   15.17  
   15.18          self.user = user
   15.19          self.messenger = messenger
   15.20 -        self.store = store or imiptools.stores.file.FileStore()
   15.21 -        self.journal = journal or imiptools.stores.file.FileJournal()
   15.22 +        self.store = store or get_store(config.STORE_TYPE, config.STORE_DIR)
   15.23 +        self.journal = journal or get_journal(config.STORE_TYPE, config.JOURNAL_DIR)
   15.24  
   15.25          try:
   15.26 -            self.publisher = publisher or imiptools.stores.file.FilePublisher()
   15.27 +            self.publisher = publisher or get_publisher(config.PUBLISH_DIR)
   15.28          except OSError:
   15.29              self.publisher = None
   15.30  
   15.31 @@ -288,7 +285,7 @@
   15.32          offer.
   15.33          """
   15.34  
   15.35 -        update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires)
   15.36 +        freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, expires)
   15.37  
   15.38      # Preparation of messages communicating the state of events.
   15.39  
   15.40 @@ -388,6 +385,13 @@
   15.41  
   15.42          return get_uri(self.obj.get_value("ORGANIZER")) == self.user
   15.43  
   15.44 +    def is_recurrence(self):
   15.45 +
   15.46 +        "Return whether the current object is a recurrence of its parent."
   15.47 +
   15.48 +        parent = self.get_parent_object()
   15.49 +        return parent and parent.has_recurrence(self.get_tzid(), self.obj.get_recurrenceid())
   15.50 +
   15.51      # Common operations on calendar data.
   15.52  
   15.53      def update_senders(self, obj=None):
   15.54 @@ -626,7 +630,7 @@
   15.55          # this point, so we update our copy for serialisation as the bundled
   15.56          # free/busy object.
   15.57  
   15.58 -        freebusy = self.store.get_freebusy(self.user)
   15.59 +        freebusy = self.store.get_freebusy(self.user).copy()
   15.60          self.update_freebusy(freebusy, self.user, from_organiser)
   15.61  
   15.62          # Bundle free/busy information if appropriate.
   15.63 @@ -862,7 +866,7 @@
   15.64          Indicate whether within 'freebusy' the given 'periods' can be scheduled.
   15.65          """
   15.66  
   15.67 -        return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
   15.68 +        return freebusy.can_schedule(periods, self.uid, self.recurrenceid)
   15.69  
   15.70      def have_new_object(self, strict=True):
   15.71  
   15.72 @@ -993,9 +997,9 @@
   15.73  
   15.74          "Remove this event from the given 'freebusy' collection."
   15.75  
   15.76 -        removed = remove_event_periods(freebusy, self.uid, self.recurrenceid)
   15.77 +        removed = freebusy.remove_event_periods(self.uid, self.recurrenceid)
   15.78          if not removed and self.recurrenceid:
   15.79 -            return remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))
   15.80 +            return freebusy.remove_affected_period(self.uid, self.get_recurrence_start_point(self.recurrenceid))
   15.81          else:
   15.82              return removed
   15.83  
   15.84 @@ -1011,18 +1015,18 @@
   15.85  
   15.86          if self.recurrenceid:
   15.87              recurrenceid = self.get_recurrence_start_point(self.recurrenceid)
   15.88 -            remove_affected_period(freebusy, self.uid, recurrenceid)
   15.89 +            freebusy.remove_affected_period(self.uid, recurrenceid)
   15.90          else:
   15.91              # Remove obsolete recurrence periods.
   15.92  
   15.93 -            remove_additional_periods(freebusy, self.uid, recurrenceids)
   15.94 +            freebusy.remove_additional_periods(self.uid, recurrenceids)
   15.95  
   15.96              # Remove original periods affected by additional recurrences.
   15.97  
   15.98              if recurrenceids:
   15.99                  for recurrenceid in recurrenceids:
  15.100                      recurrenceid = self.get_recurrence_start_point(recurrenceid)
  15.101 -                    remove_affected_period(freebusy, self.uid, recurrenceid)
  15.102 +                    freebusy.remove_affected_period(self.uid, recurrenceid)
  15.103  
  15.104      def update_freebusy(self, freebusy, user, as_organiser, offer=False):
  15.105  
  15.106 @@ -1137,7 +1141,7 @@
  15.107  
  15.108          self.acquire_lock()
  15.109          try:
  15.110 -            freebusy = self.store.get_freebusy_for_other(self.user, user)
  15.111 +            freebusy = self.store.get_freebusy_for_other_for_update(self.user, user)
  15.112              fn(freebusy, user, for_organiser, True)
  15.113  
  15.114              # Tidy up any obsolete recurrences.
  15.115 @@ -1192,7 +1196,7 @@
  15.116          organiser of an event if 'for_organiser' is set to a true value.
  15.117          """
  15.118  
  15.119 -        freebusy = self.store.get_freebusy(self.user)
  15.120 +        freebusy = self.store.get_freebusy_for_update(self.user)
  15.121  
  15.122          # Obtain the attendance attributes for this user, if available.
  15.123  
  15.124 @@ -1219,7 +1223,7 @@
  15.125  
  15.126          "Remove free/busy information when handling an object."
  15.127  
  15.128 -        freebusy = self.store.get_freebusy(self.user)
  15.129 +        freebusy = self.store.get_freebusy_for_update(self.user)
  15.130  
  15.131          self.remove_from_freebusy(freebusy)
  15.132          self.remove_freebusy_for_recurrences(freebusy)
  15.133 @@ -1238,7 +1242,7 @@
  15.134  
  15.135          "Update free/busy offers when handling an object."
  15.136  
  15.137 -        freebusy = self.store.get_freebusy_offers(self.user)
  15.138 +        freebusy = self.store.get_freebusy_offers_for_update(self.user)
  15.139  
  15.140          # Obtain the attendance attributes for this user, if available.
  15.141  
  15.142 @@ -1256,7 +1260,7 @@
  15.143  
  15.144          "Remove free/busy offers when handling an object."
  15.145  
  15.146 -        freebusy = self.store.get_freebusy_offers(self.user)
  15.147 +        freebusy = self.store.get_freebusy_offers_for_update(self.user)
  15.148  
  15.149          self.remove_from_freebusy(freebusy)
  15.150          self.remove_freebusy_for_recurrences(freebusy)
    16.1 --- a/imiptools/config.py	Tue Apr 19 21:20:57 2016 +0200
    16.2 +++ b/imiptools/config.py	Fri Apr 22 16:22:58 2016 +0200
    16.3 @@ -14,6 +14,10 @@
    16.4  
    16.5  OUTGOING_PREFIX = "people-outgoing"
    16.6  
    16.7 +# The store (and journal) type.
    16.8 +
    16.9 +STORE_TYPE = "file"
   16.10 +
   16.11  # The location of the stored calendar information.
   16.12  
   16.13  STORE_DIR = "/var/lib/imip-agent/store"
    17.1 --- a/imiptools/data.py	Tue Apr 19 21:20:57 2016 +0200
    17.2 +++ b/imiptools/data.py	Fri Apr 22 16:22:58 2016 +0200
    17.3 @@ -3,7 +3,7 @@
    17.4  """
    17.5  Interpretation of vCalendar content.
    17.6  
    17.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
    17.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    17.9  
   17.10  This program is free software; you can redistribute it and/or modify it under
   17.11  the terms of the GNU General Public License as published by the Free Software
   17.12 @@ -29,7 +29,7 @@
   17.13                              get_recurrence_start_point, \
   17.14                              get_time, get_tzid, to_datetime, to_timezone, \
   17.15                              to_utc_datetime
   17.16 -from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod, period_overlaps
   17.17 +from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod
   17.18  from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
   17.19  from vRecurrence import get_parameters, get_rule
   17.20  import email.utils
   17.21 @@ -44,6 +44,25 @@
   17.22      "Access to calendar structures."
   17.23  
   17.24      def __init__(self, fragment):
   17.25 +
   17.26 +        """
   17.27 +        Initialise the object with the given 'fragment'. This must be a
   17.28 +        dictionary mapping an object type (such as "VEVENT") to a tuple
   17.29 +        containing the object details and attributes, each being a dictionary
   17.30 +        itself.
   17.31 +
   17.32 +        The result of parse_object can be processed to obtain a fragment by
   17.33 +        obtaining a collection of records for an object type. For example:
   17.34 +
   17.35 +        l = parse_object(f, encoding, "VCALENDAR")
   17.36 +        events = l["VEVENT"]
   17.37 +        event = events[0]
   17.38 +
   17.39 +        Then, the specific object must be presented as follows:
   17.40 +
   17.41 +        object = Object({"VEVENT" : event})
   17.42 +        """
   17.43 +
   17.44          self.objtype, (self.details, self.attr) = fragment.items()[0]
   17.45  
   17.46      def get_uid(self):
   17.47 @@ -148,6 +167,9 @@
   17.48      def to_part(self, method):
   17.49          return to_part(method, [self.to_node()])
   17.50  
   17.51 +    def to_string(self):
   17.52 +        return to_string(self.to_node())
   17.53 +
   17.54      # Direct access to the structure.
   17.55  
   17.56      def has_key(self, name):
   17.57 @@ -216,7 +238,7 @@
   17.58  
   17.59          return (dtstart, dtstart_attr), (dtend, dtend_attr)
   17.60  
   17.61 -    def get_periods(self, tzid, end=None):
   17.62 +    def get_periods(self, tzid, end=None, inclusive=False):
   17.63  
   17.64          """
   17.65          Return periods defined by this object, employing the given 'tzid' where
   17.66 @@ -225,9 +247,34 @@
   17.67  
   17.68          If 'end' is omitted, only explicit recurrences and recurrences from
   17.69          explicitly-terminated rules will be returned.
   17.70 +
   17.71 +        If 'inclusive' is set to a true value, any period occurring at the 'end'
   17.72 +        will be included.
   17.73 +        """
   17.74 +
   17.75 +        return get_periods(self, tzid, end, inclusive)
   17.76 +
   17.77 +    def has_period(self, tzid, period):
   17.78 +
   17.79 +        """
   17.80 +        Return whether this object, employing the given 'tzid' where no time
   17.81 +        zone information is defined, has the given 'period'.
   17.82          """
   17.83  
   17.84 -        return get_periods(self, tzid, end)
   17.85 +        return period in self.get_periods(tzid, period.get_start_point(), inclusive=True)
   17.86 +
   17.87 +    def has_recurrence(self, tzid, recurrenceid):
   17.88 +
   17.89 +        """
   17.90 +        Return whether this object, employing the given 'tzid' where no time
   17.91 +        zone information is defined, has the given 'recurrenceid'.
   17.92 +        """
   17.93 +
   17.94 +        start_point = self.get_recurrence_start_point(recurrenceid, tzid)
   17.95 +        for p in self.get_periods(tzid, start_point, inclusive=True):
   17.96 +            if p.get_start_point() == start_point:
   17.97 +                return True
   17.98 +        return False
   17.99  
  17.100      def get_active_periods(self, recurrenceids, tzid, end=None):
  17.101  
  17.102 @@ -568,7 +615,7 @@
  17.103          # Get a constrained view if start and end limits are specified.
  17.104  
  17.105          if period:
  17.106 -            periods = period_overlaps(freebusy, period, True)
  17.107 +            periods = freebusy.period_overlaps(period, True)
  17.108          else:
  17.109              periods = freebusy
  17.110  
  17.111 @@ -617,6 +664,19 @@
  17.112  
  17.113      return None
  17.114  
  17.115 +def parse_string(s, encoding, objtype=None):
  17.116 +
  17.117 +    """
  17.118 +    Parse the iTIP content from 's' having the given 'encoding'. If 'objtype' is
  17.119 +    given, only objects of that type will be returned. Otherwise, the root of
  17.120 +    the content will be returned as a dictionary with a single key indicating
  17.121 +    the object type.
  17.122 +
  17.123 +    Return None if the content was not readable or suitable.
  17.124 +    """
  17.125 +
  17.126 +    return parse_object(StringIO(s), encoding, objtype)
  17.127 +
  17.128  def to_part(method, calendar):
  17.129  
  17.130      """
  17.131 @@ -636,8 +696,23 @@
  17.132          out.close()
  17.133  
  17.134  def to_stream(out, fragment, encoding="utf-8"):
  17.135 +
  17.136 +    "Write to the 'out' stream the given 'fragment'."
  17.137 +
  17.138      iterwrite(out, encoding=encoding).append(fragment)
  17.139  
  17.140 +def to_string(fragment, encoding="utf-8"):
  17.141 +
  17.142 +    "Return a string encoding the given 'fragment'."
  17.143 +
  17.144 +    out = StringIO()
  17.145 +    try:
  17.146 +        to_stream(out, fragment, encoding)
  17.147 +        return out.getvalue()
  17.148 +
  17.149 +    finally:
  17.150 +        out.close()
  17.151 +
  17.152  # Structure access functions.
  17.153  
  17.154  def get_items(d, name, all=True):
    18.1 --- a/imiptools/handlers/common.py	Tue Apr 19 21:20:57 2016 +0200
    18.2 +++ b/imiptools/handlers/common.py	Fri Apr 22 16:22:58 2016 +0200
    18.3 @@ -3,7 +3,7 @@
    18.4  """
    18.5  Common handler functionality for different entities.
    18.6  
    18.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
    18.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    18.9  
   18.10  This program is free software; you can redistribute it and/or modify it under
   18.11  the terms of the GNU General Public License as published by the Free Software
   18.12 @@ -22,7 +22,7 @@
   18.13  from imiptools.data import get_address, get_uri, make_freebusy, to_part, \
   18.14                             uri_dict
   18.15  from imiptools.dates import format_datetime
   18.16 -from imiptools.period import FreeBusyPeriod, Period, replace_overlapping
   18.17 +from imiptools.period import FreeBusyPeriod, Period
   18.18  
   18.19  class CommonFreebusy:
   18.20  
   18.21 @@ -58,8 +58,8 @@
   18.22          period = Period(dtstart, dtend, self.get_tzid())
   18.23  
   18.24          for sender, sender_attr in senders:
   18.25 -            stored_freebusy = self.store.get_freebusy_for_other(self.user, sender)
   18.26 -            replace_overlapping(stored_freebusy, period, freebusy)
   18.27 +            stored_freebusy = self.store.get_freebusy_for_other_for_update(self.user, sender)
   18.28 +            stored_freebusy.replace_overlapping(period, freebusy)
   18.29              self.store.set_freebusy_for_other(self.user, stored_freebusy, sender)
   18.30  
   18.31      def request(self):
   18.32 @@ -143,4 +143,50 @@
   18.33  
   18.34          self.add_result("REFRESH", [get_address(organiser)], obj.to_part("REFRESH"))
   18.35  
   18.36 +    def is_newly_separated_occurrence(self):
   18.37 +
   18.38 +        "Return whether the current object is a newly-separated occurrence."
   18.39 +
   18.40 +        # Obtain any stored object.
   18.41 +
   18.42 +        obj = self.get_stored_object_version()
   18.43 +
   18.44 +        # Handle any newly-separated, valid occurrence.
   18.45 +
   18.46 +        return not obj and self.is_recurrence()
   18.47 +
   18.48 +    def make_separate_occurrence(self, for_organiser=False):
   18.49 +
   18.50 +        """
   18.51 +        Set the current object as a separate occurrence and redefine free/busy
   18.52 +        records in terms of this new occurrence for other participants.
   18.53 +        """
   18.54 +
   18.55 +        parent = self.get_parent_object()
   18.56 +        if not parent:
   18.57 +            return False
   18.58 +
   18.59 +        # Transfer attendance information from the parent.
   18.60 +
   18.61 +        parent_attendees = uri_dict(parent.get_value_map("ATTENDEE"))
   18.62 +        attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))
   18.63 +
   18.64 +        for attendee, attendee_attr in parent_attendees.items():
   18.65 +            if not attendee_map.has_key(attendee):
   18.66 +                attendee_map[attendee] = attendee_attr
   18.67 +
   18.68 +        self.obj["ATTENDEE"] = attendee_map.items()
   18.69 +        self.obj.remove_all(["RDATE", "RRULE"])
   18.70 +
   18.71 +        # Create or revive the occurrence.
   18.72 +
   18.73 +        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
   18.74 +        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
   18.75 +
   18.76 +        # Update free/busy details for the current object for all attendees.
   18.77 +
   18.78 +        self.update_freebusy_from_attendees(attendee_map.keys())
   18.79 +
   18.80 +        return True
   18.81 +
   18.82  # vim: tabstop=4 expandtab shiftwidth=4
    19.1 --- a/imiptools/handlers/person.py	Tue Apr 19 21:20:57 2016 +0200
    19.2 +++ b/imiptools/handlers/person.py	Fri Apr 22 16:22:58 2016 +0200
    19.3 @@ -3,7 +3,7 @@
    19.4  """
    19.5  Handlers for a person for whom scheduling is performed.
    19.6  
    19.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
    19.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    19.9  
   19.10  This program is free software; you can redistribute it and/or modify it under
   19.11  the terms of the GNU General Public License as published by the Free Software
   19.12 @@ -205,10 +205,35 @@
   19.13  
   19.14          "As organiser, update attendance from valid attendees."
   19.15  
   19.16 -        if self.merge_attendance(attendees):
   19.17 -            self.update_freebusy_from_attendees(attendees)
   19.18 +        # Occurrences that are still part of a parent object are separated,
   19.19 +        # attendance information transferred, and the free/busy details updated.
   19.20 +
   19.21 +        if self.is_newly_separated_occurrence():
   19.22 +            if self.make_separate_occurrence(for_organiser=True):
   19.23 +
   19.24 +                # Update free/busy details for the event.
   19.25 +
   19.26 +                self.update_event_in_freebusy(for_organiser=True)
   19.27 +
   19.28 +                # Produce a REQUEST for the created occurrence for other
   19.29 +                # attendees of the parent event.
   19.30  
   19.31 -        return True
   19.32 +                obj = self.get_parent_object()
   19.33 +                stored_attendees = set(obj.get_values("ATTENDEE"))
   19.34 +                attendees = stored_attendees.difference(attendees)
   19.35 +
   19.36 +                for attendee in attendees:
   19.37 +                    methods, parts = self.get_message_parts(self.obj, "REQUEST", attendee)
   19.38 +                    self.add_results(methods, [get_address(attendee)], parts)
   19.39 +
   19.40 +                return True
   19.41 +
   19.42 +        # Merge the attendance for the received object.
   19.43 +
   19.44 +        elif self.merge_attendance(attendees):
   19.45 +            return self.update_freebusy_from_attendees(attendees)
   19.46 +
   19.47 +        return False
   19.48  
   19.49      def _refresh(self, organiser, attendees):
   19.50  
    20.1 --- a/imiptools/handlers/person_outgoing.py	Tue Apr 19 21:20:57 2016 +0200
    20.2 +++ b/imiptools/handlers/person_outgoing.py	Fri Apr 22 16:22:58 2016 +0200
    20.3 @@ -4,7 +4,7 @@
    20.4  Handlers for a person for whom scheduling is performed, inspecting outgoing
    20.5  messages to obtain scheduling done externally.
    20.6  
    20.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
    20.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    20.9  
   20.10  This program is free software; you can redistribute it and/or modify it under
   20.11  the terms of the GNU General Public License as published by the Free Software
   20.12 @@ -120,11 +120,19 @@
   20.13                  self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
   20.14  
   20.15          else:
   20.16 +            # Occurrences that are still part of a parent object are separated,
   20.17 +            # attendance information transferred, and the free/busy details
   20.18 +            # updated.
   20.19 +
   20.20 +            if self.is_newly_separated_occurrence():
   20.21 +                self.make_separate_occurrence(for_organiser=not from_organiser)
   20.22 +
   20.23              # Obtain valid attendees, merging their attendance with the stored
   20.24              # object.
   20.25  
   20.26 -            attendees = self.require_attendees(from_organiser)
   20.27 -            self.merge_attendance(attendees)
   20.28 +            else:
   20.29 +                attendees = self.require_attendees(from_organiser)
   20.30 +                self.merge_attendance(attendees)
   20.31  
   20.32          # Remove any associated request.
   20.33  
    21.1 --- a/imiptools/handlers/scheduling/freebusy.py	Tue Apr 19 21:20:57 2016 +0200
    21.2 +++ b/imiptools/handlers/scheduling/freebusy.py	Fri Apr 22 16:22:58 2016 +0200
    21.3 @@ -21,8 +21,6 @@
    21.4  
    21.5  from imiptools.data import uri_values
    21.6  from imiptools.dates import ValidityError, to_timezone
    21.7 -from imiptools.period import coalesce_freebusy, invert_freebusy, \
    21.8 -                             periods_from, remove_periods
    21.9  
   21.10  def schedule_in_freebusy(handler, args, freebusy=None):
   21.11  
   21.12 @@ -105,29 +103,34 @@
   21.13      # There should already be free/busy information for the user.
   21.14  
   21.15      user_freebusy = handler.get_store().get_freebusy(handler.user)
   21.16 -    busy = user_freebusy
   21.17 +
   21.18 +    # Maintain a separate copy of the data.
   21.19 +
   21.20 +    busy = user_freebusy.copy()
   21.21  
   21.22      # Subtract any periods from this event from the free/busy collections.
   21.23  
   21.24 -    event_periods = handler.remove_from_freebusy(user_freebusy)
   21.25 +    event_periods = handler.remove_from_freebusy(busy)
   21.26  
   21.27      # Find busy periods for the other attendees.
   21.28  
   21.29      for attendee in uri_values(handler.obj.get_values("ATTENDEE")):
   21.30          if attendee != handler.user:
   21.31 -            freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee)
   21.32 +
   21.33 +            # Get a copy of the attendee's free/busy data.
   21.34 +
   21.35 +            freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee).copy()
   21.36              if freebusy:
   21.37 -                remove_periods(freebusy, event_periods)
   21.38 +                freebusy.remove_periods(event_periods)
   21.39                  busy += freebusy
   21.40  
   21.41      # Obtain the combined busy periods.
   21.42  
   21.43 -    busy.sort()
   21.44 -    busy = coalesce_freebusy(busy)
   21.45 +    busy = busy.coalesce_freebusy()
   21.46  
   21.47      # Obtain free periods.
   21.48  
   21.49 -    free = invert_freebusy(busy)
   21.50 +    free = busy.invert_freebusy()
   21.51      permitted_values = handler.get_permitted_values()
   21.52      periods = []
   21.53  
   21.54 @@ -153,7 +156,7 @@
   21.55  
   21.56          # Get free periods from the time of each period.
   21.57  
   21.58 -        for found in periods_from(free, period):
   21.59 +        for found in free.periods_from(period):
   21.60  
   21.61              # Skip any periods before the last period.
   21.62  
    22.1 --- a/imiptools/handlers/scheduling/quota.py	Tue Apr 19 21:20:57 2016 +0200
    22.2 +++ b/imiptools/handlers/scheduling/quota.py	Fri Apr 22 16:22:58 2016 +0200
    22.3 @@ -82,7 +82,7 @@
    22.4      # Update the journal entries.
    22.5  
    22.6      journal = handler.get_journal()
    22.7 -    entries = journal.get_entries(quota, group)
    22.8 +    entries = journal.get_entries_for_update(quota, group)
    22.9      handler.update_freebusy(entries, group, False)
   22.10      journal.set_entries(quota, group, entries)
   22.11  
   22.12 @@ -105,7 +105,7 @@
   22.13      # Update the journal entries.
   22.14  
   22.15      journal = handler.get_journal()
   22.16 -    entries = journal.get_entries(quota, group)
   22.17 +    entries = journal.get_entries_for_update(quota, group)
   22.18      handler.remove_from_freebusy(entries)
   22.19      journal.set_entries(quota, group, entries)
   22.20  
   22.21 @@ -209,7 +209,7 @@
   22.22      quota, organiser = _get_quota_and_identity(handler, args)
   22.23  
   22.24      journal = handler.get_journal()
   22.25 -    freebusy = journal.get_freebusy(quota, organiser)
   22.26 +    freebusy = journal.get_freebusy_for_update(quota, organiser)
   22.27      handler.update_freebusy(freebusy, organiser, True)
   22.28      journal.set_freebusy(quota, organiser, freebusy)
   22.29  
   22.30 @@ -223,7 +223,7 @@
   22.31      quota, organiser = _get_quota_and_identity(handler, args)
   22.32  
   22.33      journal = handler.get_journal()
   22.34 -    freebusy = journal.get_freebusy(quota, organiser)
   22.35 +    freebusy = journal.get_freebusy_for_update(quota, organiser)
   22.36      handler.remove_from_freebusy(freebusy)
   22.37      journal.set_freebusy(quota, organiser, freebusy)
   22.38  
    23.1 --- a/imiptools/period.py	Tue Apr 19 21:20:57 2016 +0200
    23.2 +++ b/imiptools/period.py	Fri Apr 22 16:22:58 2016 +0200
    23.3 @@ -28,11 +28,27 @@
    23.4                              get_start_of_day, \
    23.5                              get_tzid, \
    23.6                              to_timezone, to_utc_datetime
    23.7 +from imiptools.sql import DatabaseOperations
    23.8  
    23.9  def ifnone(x, y):
   23.10      if x is None: return y
   23.11      else: return x
   23.12  
   23.13 +def from_strings(t, encoding):
   23.14 +    return tuple([from_string(s, encoding) for s in t])
   23.15 +
   23.16 +def from_string(s, encoding):
   23.17 +    if s:
   23.18 +        return unicode(s, encoding)
   23.19 +    else:
   23.20 +        return s
   23.21 +
   23.22 +def to_string(s, encoding):
   23.23 +    if s:
   23.24 +        return s.encode(encoding)
   23.25 +    else:
   23.26 +        return s
   23.27 +
   23.28  class Comparable:
   23.29  
   23.30      "A date/datetime wrapper that allows comparisons with other types."
   23.31 @@ -279,8 +295,13 @@
   23.32              return None
   23.33          d = get_recurrence_start(recurrenceid)
   23.34          dt = get_recurrence_start_point(recurrenceid, self.tzid)
   23.35 -        if self.get_start() == d or self.get_start_point() == dt:
   23.36 +
   23.37 +        # Compare the start to dates only, using the normalised start datetime
   23.38 +        # for comparisons with the start point.
   23.39 +
   23.40 +        if not isinstance(d, datetime) and self.get_start() == d or self.get_start_point() == dt:
   23.41              return recurrenceid
   23.42 +
   23.43          return None
   23.44  
   23.45      # Value correction methods.
   23.46 @@ -356,17 +377,19 @@
   23.47          self.organiser = organiser or None
   23.48          self.expires = expires or None
   23.49  
   23.50 -    def as_tuple(self, strings_only=False):
   23.51 +    def as_tuple(self, strings_only=False, string_datetimes=False):
   23.52  
   23.53          """
   23.54 -        Return the initialisation parameter tuple, converting false value
   23.55 -        parameters to strings if 'strings_only' is set to a true value.
   23.56 +        Return the initialisation parameter tuple, converting datetimes and
   23.57 +        false value parameters to strings if 'strings_only' is set to a true
   23.58 +        value. Otherwise, if 'string_datetimes' is set to a true value, only the
   23.59 +        datetime values are converted to strings.
   23.60          """
   23.61  
   23.62          null = lambda x: (strings_only and [""] or [x])[0]
   23.63          return (
   23.64 -            strings_only and format_datetime(self.get_start_point()) or self.start,
   23.65 -            strings_only and format_datetime(self.get_end_point()) or self.end,
   23.66 +            (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start,
   23.67 +            (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end,
   23.68              self.uid or null(self.uid),
   23.69              self.transp or strings_only and "OPAQUE" or None,
   23.70              self.recurrenceid or null(self.recurrenceid),
   23.71 @@ -454,280 +477,675 @@
   23.72      def make_corrected(self, start, end):
   23.73          return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
   23.74  
   23.75 -# Time and period management.
   23.76 +class FreeBusyCollectionBase:
   23.77 +
   23.78 +    "Common operations on free/busy period collections."
   23.79 +
   23.80 +    def __init__(self, mutable=True):
   23.81 +        self.mutable = mutable
   23.82 +
   23.83 +    def _check_mutable(self):
   23.84 +        if not self.mutable:
   23.85 +            raise TypeError, "Cannot mutate this collection."
   23.86 +
   23.87 +    def copy(self):
   23.88 +
   23.89 +        "Make an independent mutable copy of the collection."
   23.90  
   23.91 -def can_schedule(freebusy, periods, uid, recurrenceid):
   23.92 +        return FreeBusyCollection(list(self), True)
   23.93 +
   23.94 +    # List emulation methods.
   23.95 +
   23.96 +    def __iadd__(self, periods):
   23.97 +        for period in periods:
   23.98 +            self.insert_period(period)
   23.99 +        return self
  23.100 +
  23.101 +    def append(self, period):
  23.102 +        self.insert_period(period)
  23.103 +
  23.104 +    # Operations.
  23.105 +
  23.106 +    def can_schedule(self, periods, uid, recurrenceid):
  23.107  
  23.108 -    """
  23.109 -    Return whether the 'freebusy' list can accommodate the given 'periods'
  23.110 -    employing the specified 'uid' and 'recurrenceid'.
  23.111 -    """
  23.112 +        """
  23.113 +        Return whether the collection can accommodate the given 'periods'
  23.114 +        employing the specified 'uid' and 'recurrenceid'.
  23.115 +        """
  23.116 +
  23.117 +        for conflict in self.have_conflict(periods, True):
  23.118 +            if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
  23.119 +                return False
  23.120 +
  23.121 +        return True
  23.122 +
  23.123 +    def have_conflict(self, periods, get_conflicts=False):
  23.124  
  23.125 -    for conflict in have_conflict(freebusy, periods, True):
  23.126 -        if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
  23.127 +        """
  23.128 +        Return whether any period in the collection overlaps with the given
  23.129 +        'periods', returning a collection of such overlapping periods if
  23.130 +        'get_conflicts' is set to a true value.
  23.131 +        """
  23.132 +
  23.133 +        conflicts = set()
  23.134 +        for p in periods:
  23.135 +            overlapping = self.period_overlaps(p, get_conflicts)
  23.136 +            if overlapping:
  23.137 +                if get_conflicts:
  23.138 +                    conflicts.update(overlapping)
  23.139 +                else:
  23.140 +                    return True
  23.141 +
  23.142 +        if get_conflicts:
  23.143 +            return conflicts
  23.144 +        else:
  23.145              return False
  23.146  
  23.147 -    return True
  23.148 +    def period_overlaps(self, period, get_periods=False):
  23.149 +
  23.150 +        """
  23.151 +        Return whether any period in the collection overlaps with the given
  23.152 +        'period', returning a collection of overlapping periods if 'get_periods'
  23.153 +        is set to a true value.
  23.154 +        """
  23.155 +
  23.156 +        overlapping = self.get_overlapping(period)
  23.157 +
  23.158 +        if get_periods:
  23.159 +            return overlapping
  23.160 +        else:
  23.161 +            return len(overlapping) != 0
  23.162 +
  23.163 +    def replace_overlapping(self, period, replacements):
  23.164 +
  23.165 +        """
  23.166 +        Replace existing periods in the collection within the given 'period',
  23.167 +        using the given 'replacements'.
  23.168 +        """
  23.169 +
  23.170 +        self._check_mutable()
  23.171 +
  23.172 +        self.remove_overlapping(period)
  23.173 +        for replacement in replacements:
  23.174 +            self.insert_period(replacement)
  23.175 +
  23.176 +    def coalesce_freebusy(self):
  23.177 +
  23.178 +        "Coalesce the periods in the collection, returning a new collection."
  23.179 +
  23.180 +        if not self:
  23.181 +            return FreeBusyCollection()
  23.182 +
  23.183 +        fb = []
  23.184 +
  23.185 +        it = iter(self)
  23.186 +        period = it.next()
  23.187 +
  23.188 +        start = period.get_start_point()
  23.189 +        end = period.get_end_point()
  23.190 +
  23.191 +        try:
  23.192 +            while True:
  23.193 +                period = it.next()
  23.194 +                if period.get_start_point() > end:
  23.195 +                    fb.append(FreeBusyPeriod(start, end))
  23.196 +                    start = period.get_start_point()
  23.197 +                    end = period.get_end_point()
  23.198 +                else:
  23.199 +                    end = max(end, period.get_end_point())
  23.200 +        except StopIteration:
  23.201 +            pass
  23.202 +
  23.203 +        fb.append(FreeBusyPeriod(start, end))
  23.204 +        return FreeBusyCollection(fb)
  23.205 +
  23.206 +    def invert_freebusy(self):
  23.207 +
  23.208 +        "Return the free periods from the collection as a new collection."
  23.209 +
  23.210 +        if not self:
  23.211 +            return FreeBusyCollection([FreeBusyPeriod(None, None)])
  23.212 +
  23.213 +        # Coalesce periods that overlap or are adjacent.
  23.214 +
  23.215 +        fb = self.coalesce_freebusy()
  23.216 +        free = []
  23.217 +
  23.218 +        # Add a start-of-time period if appropriate.
  23.219 +
  23.220 +        first = fb[0].get_start_point()
  23.221 +        if first:
  23.222 +            free.append(FreeBusyPeriod(None, first))
  23.223 +
  23.224 +        start = fb[0].get_end_point()
  23.225 +
  23.226 +        for period in fb[1:]:
  23.227 +            free.append(FreeBusyPeriod(start, period.get_start_point()))
  23.228 +            start = period.get_end_point()
  23.229 +
  23.230 +        # Add an end-of-time period if appropriate.
  23.231 +
  23.232 +        if start:
  23.233 +            free.append(FreeBusyPeriod(start, None))
  23.234 +
  23.235 +        return FreeBusyCollection(free)
  23.236 +
  23.237 +    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
  23.238 +
  23.239 +        """
  23.240 +        Update the free/busy details with the given 'periods', 'transp' setting,
  23.241 +        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
  23.242 +
  23.243 +        An optional 'expires' datetime string indicates the expiry time of any
  23.244 +        free/busy offer.
  23.245 +        """
  23.246 +
  23.247 +        self._check_mutable()
  23.248 +
  23.249 +        self.remove_event_periods(uid, recurrenceid)
  23.250 +
  23.251 +        for p in periods:
  23.252 +            self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
  23.253 +
  23.254 +class FreeBusyCollection(FreeBusyCollectionBase):
  23.255 +
  23.256 +    "An abstraction for a collection of free/busy periods."
  23.257 +
  23.258 +    def __init__(self, periods=None, mutable=True):
  23.259 +
  23.260 +        """
  23.261 +        Initialise the collection with the given list of 'periods', or start an
  23.262 +        empty collection if no list is given.
  23.263 +        """
  23.264 +
  23.265 +        FreeBusyCollectionBase.__init__(self, mutable)
  23.266 +        self.periods = periods or []
  23.267 +
  23.268 +    # List emulation methods.
  23.269 +
  23.270 +    def __nonzero__(self):
  23.271 +        return bool(self.periods)
  23.272 +
  23.273 +    def __iter__(self):
  23.274 +        return iter(self.periods)
  23.275 +
  23.276 +    def __len__(self):
  23.277 +        return len(self.periods)
  23.278 +
  23.279 +    def __getitem__(self, i):
  23.280 +        return self.periods[i]
  23.281 +
  23.282 +    # Operations.
  23.283 +
  23.284 +    def insert_period(self, period):
  23.285 +
  23.286 +        "Insert the given 'period' into the collection."
  23.287 +
  23.288 +        self._check_mutable()
  23.289 +
  23.290 +        i = bisect_left(self.periods, period)
  23.291 +        if i == len(self.periods):
  23.292 +            self.periods.append(period)
  23.293 +        elif self.periods[i] != period:
  23.294 +            self.periods.insert(i, period)
  23.295 +
  23.296 +    def remove_periods(self, periods):
  23.297 +
  23.298 +        "Remove the given 'periods' from the collection."
  23.299 +
  23.300 +        self._check_mutable()
  23.301  
  23.302 -def have_conflict(freebusy, periods, get_conflicts=False):
  23.303 +        for period in periods:
  23.304 +            i = bisect_left(self.periods, period)
  23.305 +            if i < len(self.periods) and self.periods[i] == period:
  23.306 +                del self.periods[i]
  23.307 +
  23.308 +    def remove_event_periods(self, uid, recurrenceid=None):
  23.309 +
  23.310 +        """
  23.311 +        Remove from the collection all periods associated with 'uid' and
  23.312 +        'recurrenceid' (which if omitted causes the "parent" object's periods to
  23.313 +        be referenced).
  23.314 +
  23.315 +        Return the removed periods.
  23.316 +        """
  23.317 +
  23.318 +        self._check_mutable()
  23.319 +
  23.320 +        removed = []
  23.321 +        i = 0
  23.322 +        while i < len(self.periods):
  23.323 +            fb = self.periods[i]
  23.324 +            if fb.uid == uid and fb.recurrenceid == recurrenceid:
  23.325 +                removed.append(self.periods[i])
  23.326 +                del self.periods[i]
  23.327 +            else:
  23.328 +                i += 1
  23.329 +
  23.330 +        return removed
  23.331 +
  23.332 +    def remove_additional_periods(self, uid, recurrenceids=None):
  23.333 +
  23.334 +        """
  23.335 +        Remove from the collection all periods associated with 'uid' having a
  23.336 +        recurrence identifier indicating an additional or modified period.
  23.337 +
  23.338 +        If 'recurrenceids' is specified, remove all periods associated with
  23.339 +        'uid' that do not have a recurrence identifier in the given list.
  23.340 +
  23.341 +        Return the removed periods.
  23.342 +        """
  23.343 +
  23.344 +        self._check_mutable()
  23.345 +
  23.346 +        removed = []
  23.347 +        i = 0
  23.348 +        while i < len(self.periods):
  23.349 +            fb = self.periods[i]
  23.350 +            if fb.uid == uid and fb.recurrenceid and (
  23.351 +                recurrenceids is None or
  23.352 +                recurrenceids is not None and fb.recurrenceid not in recurrenceids
  23.353 +                ):
  23.354 +                removed.append(self.periods[i])
  23.355 +                del self.periods[i]
  23.356 +            else:
  23.357 +                i += 1
  23.358 +
  23.359 +        return removed
  23.360 +
  23.361 +    def remove_affected_period(self, uid, start):
  23.362 +
  23.363 +        """
  23.364 +        Remove from the collection the period associated with 'uid' that
  23.365 +        provides an occurrence starting at the given 'start' (provided by a
  23.366 +        recurrence identifier, converted to a datetime). A recurrence identifier
  23.367 +        is used to provide an alternative time period whilst also acting as a
  23.368 +        reference to the originally-defined occurrence.
  23.369 +
  23.370 +        Return any removed period in a list.
  23.371 +        """
  23.372 +
  23.373 +        self._check_mutable()
  23.374 +
  23.375 +        removed = []
  23.376 +
  23.377 +        search = Period(start, start)
  23.378 +        found = bisect_left(self.periods, search)
  23.379 +
  23.380 +        while found < len(self.periods):
  23.381 +            fb = self.periods[found]
  23.382 +
  23.383 +            # Stop looking if the start no longer matches the recurrence identifier.
  23.384 +
  23.385 +            if fb.get_start_point() != search.get_start_point():
  23.386 +                break
  23.387 +
  23.388 +            # If the period belongs to the parent object, remove it and return.
  23.389 +
  23.390 +            if not fb.recurrenceid and uid == fb.uid:
  23.391 +                removed.append(self.periods[found])
  23.392 +                del self.periods[found]
  23.393 +                break
  23.394 +
  23.395 +            # Otherwise, keep looking for a matching period.
  23.396 +
  23.397 +            found += 1
  23.398 +
  23.399 +        return removed
  23.400 +
  23.401 +    def periods_from(self, period):
  23.402 +
  23.403 +        "Return the entries in the collection at or after 'period'."
  23.404 +
  23.405 +        first = bisect_left(self.periods, period)
  23.406 +        return self.periods[first:]
  23.407 +
  23.408 +    def periods_until(self, period):
  23.409 +
  23.410 +        "Return the entries in the collection before 'period'."
  23.411 +
  23.412 +        last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
  23.413 +        return self.periods[:last]
  23.414 +
  23.415 +    def get_overlapping(self, period):
  23.416 +
  23.417 +        """
  23.418 +        Return the entries in the collection providing periods overlapping with
  23.419 +        'period'.
  23.420 +        """
  23.421 +
  23.422 +        # Find the range of periods potentially overlapping the period in the
  23.423 +        # free/busy collection.
  23.424 +
  23.425 +        startpoints = self.periods_until(period)
  23.426 +
  23.427 +        # Find the range of periods potentially overlapping the period in a version
  23.428 +        # of the free/busy collection sorted according to end datetimes.
  23.429 +
  23.430 +        endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
  23.431 +        endpoints.sort()
  23.432 +        first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
  23.433 +        endpoints = endpoints[first:]
  23.434 +
  23.435 +        overlapping = set()
  23.436 +
  23.437 +        for p, fb in endpoints:
  23.438 +            if fb.overlaps(period):
  23.439 +                overlapping.add(fb)
  23.440 +
  23.441 +        overlapping = list(overlapping)
  23.442 +        overlapping.sort()
  23.443 +        return overlapping
  23.444 +
  23.445 +    def remove_overlapping(self, period):
  23.446 +
  23.447 +        "Remove all periods overlapping with 'period' from the collection."
  23.448 +
  23.449 +        self._check_mutable()
  23.450 +
  23.451 +        overlapping = self.get_overlapping(period)
  23.452 +
  23.453 +        if overlapping:
  23.454 +            for fb in overlapping:
  23.455 +                self.periods.remove(fb)
  23.456 +
  23.457 +class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations):
  23.458  
  23.459      """
  23.460 -    Return whether any period in 'freebusy' overlaps with the given 'periods',
  23.461 -    returning a collection of such overlapping periods if 'get_conflicts' is
  23.462 -    set to a true value.
  23.463 -    """
  23.464 -
  23.465 -    conflicts = set()
  23.466 -    for p in periods:
  23.467 -        overlapping = period_overlaps(freebusy, p, get_conflicts)
  23.468 -        if overlapping:
  23.469 -            if get_conflicts:
  23.470 -                conflicts.update(overlapping)
  23.471 -            else:
  23.472 -                return True
  23.473 -
  23.474 -    if get_conflicts:
  23.475 -        return conflicts
  23.476 -    else:
  23.477 -        return False
  23.478 -
  23.479 -def insert_period(freebusy, period):
  23.480 -
  23.481 -    "Insert into 'freebusy' the given 'period'."
  23.482 -
  23.483 -    i = bisect_left(freebusy, period)
  23.484 -    if i == len(freebusy):
  23.485 -        freebusy.append(period)
  23.486 -    elif freebusy[i] != period:
  23.487 -        freebusy.insert(i, period)
  23.488 -
  23.489 -def remove_periods(freebusy, periods):
  23.490 -
  23.491 -    "Remove from 'freebusy' the given 'periods'."
  23.492 -
  23.493 -    for period in periods:
  23.494 -        i = bisect_left(freebusy, period)
  23.495 -        if i < len(freebusy) and freebusy[i] == period:
  23.496 -            del freebusy[i]
  23.497 -
  23.498 -def remove_event_periods(freebusy, uid, recurrenceid=None):
  23.499 -
  23.500 -    """
  23.501 -    Remove from 'freebusy' all periods associated with 'uid' and 'recurrenceid'
  23.502 -    (which if omitted causes the "parent" object's periods to be referenced).
  23.503 -
  23.504 -    Return the removed periods.
  23.505 -    """
  23.506 -
  23.507 -    removed = []
  23.508 -    i = 0
  23.509 -    while i < len(freebusy):
  23.510 -        fb = freebusy[i]
  23.511 -        if fb.uid == uid and fb.recurrenceid == recurrenceid:
  23.512 -            removed.append(freebusy[i])
  23.513 -            del freebusy[i]
  23.514 -        else:
  23.515 -            i += 1
  23.516 -
  23.517 -    return removed
  23.518 -
  23.519 -def remove_additional_periods(freebusy, uid, recurrenceids=None):
  23.520 -
  23.521 -    """
  23.522 -    Remove from 'freebusy' all periods associated with 'uid' having a
  23.523 -    recurrence identifier indicating an additional or modified period.
  23.524 -
  23.525 -    If 'recurrenceids' is specified, remove all periods associated with 'uid'
  23.526 -    that do not have a recurrence identifier in the given list.
  23.527 -
  23.528 -    Return the removed periods.
  23.529 -    """
  23.530 -
  23.531 -    removed = []
  23.532 -    i = 0
  23.533 -    while i < len(freebusy):
  23.534 -        fb = freebusy[i]
  23.535 -        if fb.uid == uid and fb.recurrenceid and (
  23.536 -            recurrenceids is None or
  23.537 -            recurrenceids is not None and fb.recurrenceid not in recurrenceids
  23.538 -            ):
  23.539 -            removed.append(freebusy[i])
  23.540 -            del freebusy[i]
  23.541 -        else:
  23.542 -            i += 1
  23.543 -
  23.544 -    return removed
  23.545 -
  23.546 -def remove_affected_period(freebusy, uid, start):
  23.547 -
  23.548 -    """
  23.549 -    Remove from 'freebusy' a period associated with 'uid' that provides an
  23.550 -    occurrence starting at the given 'start' (provided by a recurrence
  23.551 -    identifier, converted to a datetime). A recurrence identifier is used to
  23.552 -    provide an alternative time period whilst also acting as a reference to the
  23.553 -    originally-defined occurrence.
  23.554 -
  23.555 -    Return any removed period in a list.
  23.556 +    An abstraction for a collection of free/busy periods stored in a database
  23.557 +    system.
  23.558      """
  23.559  
  23.560 -    removed = []
  23.561 +    period_columns = ["start", "end", "object_uid", "transp", "object_recurrenceid", "summary", "organiser", "expires"]
  23.562 +
  23.563 +    def __init__(self, cursor, table_name, column_names=None, filter_values=None, mutable=True, paramstyle=None):
  23.564  
  23.565 -    search = Period(start, start)
  23.566 -    found = bisect_left(freebusy, search)
  23.567 +        """
  23.568 +        Initialise the collection with the given 'cursor' and with the
  23.569 +        'table_name', 'column_names' and 'filter_values' configuring the
  23.570 +        selection of data.
  23.571 +        """
  23.572 +
  23.573 +        FreeBusyCollectionBase.__init__(self, mutable)
  23.574 +        DatabaseOperations.__init__(self, column_names, filter_values, paramstyle)
  23.575 +        self.cursor = cursor
  23.576 +        self.table_name = table_name
  23.577  
  23.578 -    while found < len(freebusy):
  23.579 -        fb = freebusy[found]
  23.580 +    def make_period(self, t):
  23.581 +        return FreeBusyPeriod(*from_strings(t, "utf-8"))
  23.582  
  23.583 -        # Stop looking if the start no longer matches the recurrence identifier.
  23.584 +    # List emulation methods.
  23.585 +
  23.586 +    def __nonzero__(self):
  23.587 +        return len(self) and True or False
  23.588  
  23.589 -        if fb.get_start_point() != search.get_start_point():
  23.590 -            break
  23.591 -
  23.592 -        # If the period belongs to the parent object, remove it and return.
  23.593 +    def __iter__(self):
  23.594 +        query, values = self.get_query(
  23.595 +            "select %(columns)s from %(table)s :condition" % {
  23.596 +                "columns" : self.columnlist(self.period_columns),
  23.597 +                "table" : self.table_name
  23.598 +                })
  23.599 +        self.cursor.execute(query, values)
  23.600 +        return iter(map(lambda t: self.make_period(t), self.cursor.fetchall()))
  23.601  
  23.602 -        if not fb.recurrenceid and uid == fb.uid:
  23.603 -            removed.append(freebusy[found])
  23.604 -            del freebusy[found]
  23.605 -            break
  23.606 +    def __len__(self):
  23.607 +        query, values = self.get_query(
  23.608 +            "select count(*) from %(table)s :condition" % {
  23.609 +                "table" : self.table_name
  23.610 +                })
  23.611 +        self.cursor.execute(query, values)
  23.612 +        result = self.cursor.fetchone()
  23.613 +        return result and int(result[0]) or 0
  23.614  
  23.615 -        # Otherwise, keep looking for a matching period.
  23.616 +    def __getitem__(self, i):
  23.617 +        return list(iter(self))[i]
  23.618  
  23.619 -        found += 1
  23.620 +    # Operations.
  23.621  
  23.622 -    return removed
  23.623 +    def insert_period(self, period):
  23.624 +
  23.625 +        "Insert the given 'period' into the collection."
  23.626  
  23.627 -def periods_from(freebusy, period):
  23.628 +        self._check_mutable()
  23.629 +
  23.630 +        columns, values = self.period_columns, period.as_tuple(string_datetimes=True)
  23.631  
  23.632 -    "Return the entries in 'freebusy' at or after 'period'."
  23.633 +        query, values = self.get_query(
  23.634 +            "insert into %(table)s (:columns) values (:values)" % {
  23.635 +                "table" : self.table_name
  23.636 +                },
  23.637 +            columns, [to_string(v, "utf-8") for v in values])
  23.638  
  23.639 -    first = bisect_left(freebusy, period)
  23.640 -    return freebusy[first:]
  23.641 +        self.cursor.execute(query, values)
  23.642 +
  23.643 +    def remove_periods(self, periods):
  23.644  
  23.645 -def periods_until(freebusy, period):
  23.646 +        "Remove the given 'periods' from the collection."
  23.647  
  23.648 -    "Return the entries in 'freebusy' before 'period'."
  23.649 +        self._check_mutable()
  23.650 +
  23.651 +        for period in periods:
  23.652 +            values = period.as_tuple(string_datetimes=True)
  23.653  
  23.654 -    last = bisect_right(freebusy, Period(period.get_end(), period.get_end(), period.get_tzid()))
  23.655 -    return freebusy[:last]
  23.656 +            query, values = self.get_query(
  23.657 +                "delete from %(table)s :condition" % {
  23.658 +                    "table" : self.table_name
  23.659 +                    },
  23.660 +                self.period_columns, [to_string(v, "utf-8") for v in values])
  23.661  
  23.662 -def get_overlapping(freebusy, period):
  23.663 +            self.cursor.execute(query, values)
  23.664 +
  23.665 +    def remove_event_periods(self, uid, recurrenceid=None):
  23.666 +
  23.667 +        """
  23.668 +        Remove from the collection all periods associated with 'uid' and
  23.669 +        'recurrenceid' (which if omitted causes the "parent" object's periods to
  23.670 +        be referenced).
  23.671  
  23.672 -    """
  23.673 -    Return the entries in 'freebusy' providing periods overlapping with
  23.674 -    'period'.
  23.675 -    """
  23.676 +        Return the removed periods.
  23.677 +        """
  23.678 +
  23.679 +        self._check_mutable()
  23.680 +
  23.681 +        if recurrenceid:
  23.682 +            columns, values = ["object_uid", "object_recurrenceid"], [uid, recurrenceid]
  23.683 +        else:
  23.684 +            columns, values = ["object_uid", "object_recurrenceid is null"], [uid]
  23.685  
  23.686 -    # Find the range of periods potentially overlapping the period in the
  23.687 -    # free/busy collection.
  23.688 +        query, _values = self.get_query(
  23.689 +            "select %(columns)s from %(table)s :condition" % {
  23.690 +                "columns" : self.columnlist(self.period_columns),
  23.691 +                "table" : self.table_name
  23.692 +                },
  23.693 +            columns, values)
  23.694  
  23.695 -    startpoints = periods_until(freebusy, period)
  23.696 -
  23.697 -    # Find the range of periods potentially overlapping the period in a version
  23.698 -    # of the free/busy collection sorted according to end datetimes.
  23.699 +        self.cursor.execute(query, _values)
  23.700 +        removed = self.cursor.fetchall()
  23.701  
  23.702 -    endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
  23.703 -    endpoints.sort()
  23.704 -    first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
  23.705 -    endpoints = endpoints[first:]
  23.706 +        query, values = self.get_query(
  23.707 +            "delete from %(table)s :condition" % {
  23.708 +                "table" : self.table_name
  23.709 +                },
  23.710 +            columns, values)
  23.711 +
  23.712 +        self.cursor.execute(query, values)
  23.713  
  23.714 -    overlapping = set()
  23.715 +        return map(lambda t: self.make_period(t), removed)
  23.716 +
  23.717 +    def remove_additional_periods(self, uid, recurrenceids=None):
  23.718  
  23.719 -    for p, fb in endpoints:
  23.720 -        if fb.overlaps(period):
  23.721 -            overlapping.add(fb)
  23.722 +        """
  23.723 +        Remove from the collection all periods associated with 'uid' having a
  23.724 +        recurrence identifier indicating an additional or modified period.
  23.725 +
  23.726 +        If 'recurrenceids' is specified, remove all periods associated with
  23.727 +        'uid' that do not have a recurrence identifier in the given list.
  23.728  
  23.729 -    overlapping = list(overlapping)
  23.730 -    overlapping.sort()
  23.731 -    return overlapping
  23.732 +        Return the removed periods.
  23.733 +        """
  23.734 +
  23.735 +        self._check_mutable()
  23.736  
  23.737 -def period_overlaps(freebusy, period, get_periods=False):
  23.738 +        if not recurrenceids:
  23.739 +            columns, values = ["object_uid", "object_recurrenceid is not null"], [uid]
  23.740 +        else:
  23.741 +            columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)]
  23.742  
  23.743 -    """
  23.744 -    Return whether any period in 'freebusy' overlaps with the given 'period',
  23.745 -    returning a collection of overlapping periods if 'get_periods' is set to a
  23.746 -    true value.
  23.747 -    """
  23.748 +        query, _values = self.get_query(
  23.749 +            "select %(columns)s from %(table)s :condition" % {
  23.750 +                "columns" : self.columnlist(self.period_columns),
  23.751 +                "table" : self.table_name
  23.752 +                },
  23.753 +            columns, values)
  23.754 +
  23.755 +        self.cursor.execute(query, _values)
  23.756 +        removed = self.cursor.fetchall()
  23.757  
  23.758 -    overlapping = get_overlapping(freebusy, period)
  23.759 +        query, values = self.get_query(
  23.760 +            "delete from %(table)s :condition" % {
  23.761 +                "table" : self.table_name
  23.762 +                },
  23.763 +            columns, values)
  23.764  
  23.765 -    if get_periods:
  23.766 -        return overlapping
  23.767 -    else:
  23.768 -        return len(overlapping) != 0
  23.769 +        self.cursor.execute(query, values)
  23.770  
  23.771 -def remove_overlapping(freebusy, period):
  23.772 +        return map(lambda t: self.make_period(t), removed)
  23.773 +
  23.774 +    def remove_affected_period(self, uid, start):
  23.775  
  23.776 -    "Remove from 'freebusy' all periods overlapping with 'period'."
  23.777 +        """
  23.778 +        Remove from the collection the period associated with 'uid' that
  23.779 +        provides an occurrence starting at the given 'start' (provided by a
  23.780 +        recurrence identifier, converted to a datetime). A recurrence identifier
  23.781 +        is used to provide an alternative time period whilst also acting as a
  23.782 +        reference to the originally-defined occurrence.
  23.783  
  23.784 -    overlapping = get_overlapping(freebusy, period)
  23.785 +        Return any removed period in a list.
  23.786 +        """
  23.787  
  23.788 -    if overlapping:
  23.789 -        for fb in overlapping:
  23.790 -            freebusy.remove(fb)
  23.791 +        self._check_mutable()
  23.792 +
  23.793 +        start = format_datetime(start)
  23.794 +
  23.795 +        columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start]
  23.796  
  23.797 -def replace_overlapping(freebusy, period, replacements):
  23.798 +        query, _values = self.get_query(
  23.799 +            "select %(columns)s from %(table)s :condition" % {
  23.800 +                "columns" : self.columnlist(self.period_columns),
  23.801 +                "table" : self.table_name
  23.802 +                },
  23.803 +            columns, values)
  23.804  
  23.805 -    """
  23.806 -    Replace existing periods in 'freebusy' within the given 'period', using the
  23.807 -    given 'replacements'.
  23.808 -    """
  23.809 +        self.cursor.execute(query, _values)
  23.810 +        removed = self.cursor.fetchall()
  23.811 +
  23.812 +        query, values = self.get_query(
  23.813 +            "delete from %(table)s :condition" % {
  23.814 +                "table" : self.table_name
  23.815 +                },
  23.816 +            columns, values)
  23.817  
  23.818 -    remove_overlapping(freebusy, period)
  23.819 -    for replacement in replacements:
  23.820 -        insert_period(freebusy, replacement)
  23.821 +        self.cursor.execute(query, values)
  23.822 +
  23.823 +        return map(lambda t: self.make_period(t), removed)
  23.824 +
  23.825 +    def periods_from(self, period):
  23.826 +
  23.827 +        "Return the entries in the collection at or after 'period'."
  23.828 +
  23.829 +        start = format_datetime(period.get_start_point())
  23.830  
  23.831 -def coalesce_freebusy(freebusy):
  23.832 +        columns, values = [], []
  23.833  
  23.834 -    "Coalesce the periods in 'freebusy'."
  23.835 +        if start:
  23.836 +            columns.append("start >= ?")
  23.837 +            values.append(start)
  23.838  
  23.839 -    if not freebusy:
  23.840 -        return freebusy
  23.841 +        query, values = self.get_query(
  23.842 +            "select %(columns)s from %(table)s :condition" % {
  23.843 +                "columns" : self.columnlist(self.period_columns),
  23.844 +                "table" : self.table_name
  23.845 +                },
  23.846 +            columns, values)
  23.847  
  23.848 -    fb = []
  23.849 -    start = freebusy[0].get_start_point()
  23.850 -    end = freebusy[0].get_end_point()
  23.851 +        self.cursor.execute(query, values)
  23.852 +
  23.853 +        return map(lambda t: self.make_period(t), self.cursor.fetchall())
  23.854 +
  23.855 +    def periods_until(self, period):
  23.856  
  23.857 -    for period in freebusy[1:]:
  23.858 -        if period.get_start_point() > end:
  23.859 -            fb.append(FreeBusyPeriod(start, end))
  23.860 -            start = period.get_start_point()
  23.861 -            end = period.get_end_point()
  23.862 -        else:
  23.863 -            end = max(end, period.get_end_point())
  23.864 +        "Return the entries in the collection before 'period'."
  23.865 +
  23.866 +        end = format_datetime(period.get_end_point())
  23.867 +
  23.868 +        columns, values = [], []
  23.869 +
  23.870 +        if end:
  23.871 +            columns.append("start < ?")
  23.872 +            values.append(end)
  23.873  
  23.874 -    fb.append(FreeBusyPeriod(start, end))
  23.875 -    return fb
  23.876 +        query, values = self.get_query(
  23.877 +            "select %(columns)s from %(table)s :condition" % {
  23.878 +                "columns" : self.columnlist(self.period_columns),
  23.879 +                "table" : self.table_name
  23.880 +                },
  23.881 +            columns, values)
  23.882  
  23.883 -def invert_freebusy(freebusy):
  23.884 +        self.cursor.execute(query, values)
  23.885  
  23.886 -    "Return the free periods from 'freebusy'."
  23.887 +        return map(lambda t: self.make_period(t), self.cursor.fetchall())
  23.888 +
  23.889 +    def get_overlapping(self, period):
  23.890  
  23.891 -    if not freebusy:
  23.892 -        return [FreeBusyPeriod(None, None)]
  23.893 +        """
  23.894 +        Return the entries in the collection providing periods overlapping with
  23.895 +        'period'.
  23.896 +        """
  23.897  
  23.898 -    # Coalesce periods that overlap or are adjacent.
  23.899 +        columns, values = self._get_period_values(period)
  23.900  
  23.901 -    fb = coalesce_freebusy(freebusy)
  23.902 -    free = []
  23.903 +        query, values = self.get_query(
  23.904 +            "select %(columns)s from %(table)s :condition" % {
  23.905 +                "columns" : self.columnlist(self.period_columns),
  23.906 +                "table" : self.table_name
  23.907 +                },
  23.908 +            columns, values)
  23.909  
  23.910 -    # Add a start-of-time period if appropriate.
  23.911 +        self.cursor.execute(query, values)
  23.912  
  23.913 -    first = fb[0].get_start_point()
  23.914 -    if first:
  23.915 -        free.append(FreeBusyPeriod(None, first))
  23.916 +        return map(lambda t: self.make_period(t), self.cursor.fetchall())
  23.917 +
  23.918 +    def remove_overlapping(self, period):
  23.919  
  23.920 -    start = fb[0].get_end_point()
  23.921 +        "Remove all periods overlapping with 'period' from the collection."
  23.922 +
  23.923 +        self._check_mutable()
  23.924 +
  23.925 +        columns, values = self._get_period_values(period)
  23.926  
  23.927 -    for period in fb[1:]:
  23.928 -        free.append(FreeBusyPeriod(start, period.get_start_point()))
  23.929 -        start = period.get_end_point()
  23.930 +        query, values = self.get_query(
  23.931 +            "delete from %(table)s :condition" % {
  23.932 +                "table" : self.table_name
  23.933 +                },
  23.934 +            columns, values)
  23.935 +
  23.936 +        self.cursor.execute(query, values)
  23.937 +
  23.938 +    def _get_period_values(self, period):
  23.939  
  23.940 -    # Add an end-of-time period if appropriate.
  23.941 +        start = format_datetime(period.get_start_point())
  23.942 +        end = format_datetime(period.get_end_point())
  23.943 +
  23.944 +        columns, values = [], []
  23.945  
  23.946 -    if start:
  23.947 -        free.append(FreeBusyPeriod(start, None))
  23.948 +        if end:
  23.949 +            columns.append("start < ?")
  23.950 +            values.append(end)
  23.951 +        if start:
  23.952 +            columns.append("end > ?")
  23.953 +            values.append(start)
  23.954  
  23.955 -    return free
  23.956 +        return columns, values
  23.957  
  23.958  # Period layout.
  23.959  
  23.960 @@ -1014,19 +1432,4 @@
  23.961  
  23.962      return spans
  23.963  
  23.964 -def update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
  23.965 -
  23.966 -    """
  23.967 -    Update the free/busy details with the given 'periods', 'transp' setting,
  23.968 -    'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
  23.969 -
  23.970 -    An optional 'expires' datetime string indicates the expiry time of any
  23.971 -    free/busy offer.
  23.972 -    """
  23.973 -
  23.974 -    remove_event_periods(freebusy, uid, recurrenceid)
  23.975 -
  23.976 -    for p in periods:
  23.977 -        insert_period(freebusy, FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
  23.978 -
  23.979  # vim: tabstop=4 expandtab shiftwidth=4
    24.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    24.2 +++ b/imiptools/sql.py	Fri Apr 22 16:22:58 2016 +0200
    24.3 @@ -0,0 +1,154 @@
    24.4 +#!/usr/bin/env python
    24.5 +
    24.6 +"""
    24.7 +Database utilities.
    24.8 +
    24.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
   24.10 +
   24.11 +This program is free software; you can redistribute it and/or modify it under
   24.12 +the terms of the GNU General Public License as published by the Free Software
   24.13 +Foundation; either version 3 of the License, or (at your option) any later
   24.14 +version.
   24.15 +
   24.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   24.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   24.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   24.19 +details.
   24.20 +
   24.21 +You should have received a copy of the GNU General Public License along with
   24.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   24.23 +"""
   24.24 +
   24.25 +import re
   24.26 +
   24.27 +class DatabaseOperations:
   24.28 +
   24.29 +    "Special database-related operations."
   24.30 +
   24.31 +    def __init__(self, column_names=None, filter_values=None, paramstyle=None):
   24.32 +        self.column_names = column_names
   24.33 +        self.filter_values = filter_values
   24.34 +        self.paramstyle = paramstyle
   24.35 +
   24.36 +    def get_query(self, query, columns=None, values=None, setcolumns=None,
   24.37 +                  setvalues=None):
   24.38 +
   24.39 +        """
   24.40 +        Return 'query' parameterised with condition clauses indicated by
   24.41 +        ":condition" in 'query' that are themselves populated using the given
   24.42 +        'columns' and 'values' together with any conditions provided when
   24.43 +        initialising this class.
   24.44 +
   24.45 +        If 'setcolumns' and 'setvalues' are given, such column details and
   24.46 +        values will be used to parameterise ":set" clauses in the query.
   24.47 +        """
   24.48 +
   24.49 +        columns = self.merge_default_columns(columns)
   24.50 +        values = self.merge_default_values(values)
   24.51 +
   24.52 +        condition = self.get_condition(columns)
   24.53 +        columnlist = self.columnlist(columns)
   24.54 +        placeholders = self.placeholders(values)
   24.55 +        setters = self.get_setters(setcolumns)
   24.56 +
   24.57 +        setvalues = setvalues or []
   24.58 +
   24.59 +        # Obtain the placeholder markers in order.
   24.60 +
   24.61 +        parts = re.split("(:(?:condition|set|columns|values)(?=[^a-zA-Z]|$))", query)
   24.62 +
   24.63 +        l = [parts[0]]
   24.64 +        is_placeholder = True
   24.65 +        all_values = []
   24.66 +
   24.67 +        for part in parts[1:]:
   24.68 +            if is_placeholder:
   24.69 +
   24.70 +                # Replace ":condition", replicating the given values.
   24.71 +
   24.72 +                if part == ":condition":
   24.73 +                    all_values += values
   24.74 +                    l.append(condition)
   24.75 +
   24.76 +                # Replace ":set", replicating the given values.
   24.77 +
   24.78 +                elif part == ":set":
   24.79 +                    all_values += setvalues
   24.80 +                    l.append(setters)
   24.81 +
   24.82 +                # Replace ":columns", providing a column list.
   24.83 +
   24.84 +                elif part == ":columns":
   24.85 +                    l.append(columnlist)
   24.86 +
   24.87 +                # Replace ":values", replicating the given values.
   24.88 +
   24.89 +                elif part == ":values":
   24.90 +                    all_values += values
   24.91 +                    l.append(placeholders)
   24.92 +
   24.93 +                else:
   24.94 +                    l.append(part)
   24.95 +            else:
   24.96 +                l.append(part)
   24.97 +
   24.98 +            is_placeholder = not is_placeholder
   24.99 +
  24.100 +        query = "".join(l)
  24.101 +        return query, all_values
  24.102 +
  24.103 +    def get_condition(self, columns=None):
  24.104 +
  24.105 +        "Return a condition clause featuring the given 'columns'."
  24.106 +
  24.107 +        l = self._get_columns(columns)
  24.108 +        return "where %s" % " and ".join(l)
  24.109 +
  24.110 +    def get_setters(self, columns=None):
  24.111 +
  24.112 +        "Return set operations featuring the given 'columns'."
  24.113 +
  24.114 +        l = self._get_columns(columns)
  24.115 +        return "set %s" % ", ".join(l)
  24.116 +
  24.117 +    def _get_columns(self, columns=None):
  24.118 +
  24.119 +        "Return a list of statements or tests involving 'columns'."
  24.120 +
  24.121 +        l = []
  24.122 +
  24.123 +        if columns:
  24.124 +            for column in columns:
  24.125 +                if " " in column:
  24.126 +                    column_name, remaining = column.split(" ", 1)
  24.127 +                    l.append("%s %s" % (self._quote(column_name), remaining.replace("?", self._param())))
  24.128 +                else:
  24.129 +                    l.append("%s = %s" % (self._quote(column), self._param()))
  24.130 +
  24.131 +        return l
  24.132 +
  24.133 +    def _quote(self, column):
  24.134 +        return '"%s"' % column
  24.135 +
  24.136 +    def merge_default_columns(self, columns=None):
  24.137 +        return list(self.column_names or []) + list(columns or [])
  24.138 +
  24.139 +    def merge_default_values(self, values=None):
  24.140 +        return list(self.filter_values or []) + list(values or [])
  24.141 +
  24.142 +    def columnlist(self, columns=None):
  24.143 +        return ", ".join([self._quote(column) for column in columns])
  24.144 +
  24.145 +    def placeholders(self, values=None):
  24.146 +        return ", ".join([self._param()] * len(values))
  24.147 +
  24.148 +    def _param(self):
  24.149 +
  24.150 +        # NOTE: To be expanded.
  24.151 +
  24.152 +        if self.paramstyle == "pyformat":
  24.153 +            return "%s"
  24.154 +        else:
  24.155 +            return "?"
  24.156 +
  24.157 +# vim: tabstop=4 expandtab shiftwidth=4
    25.1 --- a/imiptools/stores/__init__.py	Tue Apr 19 21:20:57 2016 +0200
    25.2 +++ b/imiptools/stores/__init__.py	Fri Apr 22 16:22:58 2016 +0200
    25.3 @@ -3,7 +3,7 @@
    25.4  """
    25.5  General support for calendar data storage.
    25.6  
    25.7 -Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    25.8 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
    25.9  
   25.10  This program is free software; you can redistribute it and/or modify it under
   25.11  the terms of the GNU General Public License as published by the Free Software
   25.12 @@ -19,568 +19,25 @@
   25.13  this program.  If not, see <http://www.gnu.org/licenses/>.
   25.14  """
   25.15  
   25.16 -from imiptools.dates import format_datetime
   25.17 -
   25.18 -class StoreBase:
   25.19 -
   25.20 -    "The core operations of a data store."
   25.21 -
   25.22 -    def acquire_lock(self, user, timeout=None):
   25.23 -        pass
   25.24 -
   25.25 -    def release_lock(self, user):
   25.26 -        pass
   25.27 -
   25.28 -    # User discovery.
   25.29 -
   25.30 -    def get_users(self):
   25.31 -
   25.32 -        "Return a list of users."
   25.33 -
   25.34 -        pass
   25.35 -
   25.36 -    # Event and event metadata access.
   25.37 -
   25.38 -    def get_events(self, user):
   25.39 -
   25.40 -        "Return a list of event identifiers."
   25.41 -
   25.42 -        pass
   25.43 -
   25.44 -    def get_all_events(self, user):
   25.45 -
   25.46 -        "Return a set of (uid, recurrenceid) tuples for all events."
   25.47 -
   25.48 -        uids = self.get_events(user)
   25.49 -        if not uids:
   25.50 -            return set()
   25.51 -
   25.52 -        all_events = set()
   25.53 -        for uid in uids:
   25.54 -            all_events.add((uid, None))
   25.55 -            all_events.update([(uid, recurrenceid) for recurrenceid in self.get_recurrences(user, uid)])
   25.56 -
   25.57 -        return all_events
   25.58 -
   25.59 -    def get_event(self, user, uid, recurrenceid=None, dirname=None):
   25.60 -
   25.61 -        """
   25.62 -        Get the event for the given 'user' with the given 'uid'. If
   25.63 -        the optional 'recurrenceid' is specified, a specific instance or
   25.64 -        occurrence of an event is returned.
   25.65 -        """
   25.66 -
   25.67 -        pass
   25.68 -
   25.69 -    def get_complete_event(self, user, uid):
   25.70 -
   25.71 -        "Get the event for the given 'user' with the given 'uid'."
   25.72 -
   25.73 -        pass
   25.74 -
   25.75 -    def set_event(self, user, uid, recurrenceid, node):
   25.76 -
   25.77 -        """
   25.78 -        Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
   25.79 -        if the latter is specified, a specific instance or occurrence of an
   25.80 -        event is referenced), using the given 'node' description.
   25.81 -        """
   25.82 +from imiptools.stores import file
   25.83 +from imiptools.stores.database import stores as database_stores
   25.84  
   25.85 -        if recurrenceid:
   25.86 -            return self.set_recurrence(user, uid, recurrenceid, node)
   25.87 -        else:
   25.88 -            return self.set_complete_event(user, uid, node)
   25.89 -
   25.90 -    def set_complete_event(self, user, uid, node):
   25.91 -
   25.92 -        "Set an event for 'user' having the given 'uid' and 'node'."
   25.93 -
   25.94 -        pass
   25.95 -
   25.96 -    def remove_event(self, user, uid, recurrenceid=None):
   25.97 -
   25.98 -        """
   25.99 -        Remove an event for 'user' having the given 'uid'. If the optional
  25.100 -        'recurrenceid' is specified, a specific instance or occurrence of an
  25.101 -        event is removed.
  25.102 -        """
  25.103 -
  25.104 -        if recurrenceid:
  25.105 -            return self.remove_recurrence(user, uid, recurrenceid)
  25.106 -        else:
  25.107 -            for recurrenceid in self.get_recurrences(user, uid) or []:
  25.108 -                self.remove_recurrence(user, uid, recurrenceid)
  25.109 -            return self.remove_complete_event(user, uid)
  25.110 -
  25.111 -    def remove_complete_event(self, user, uid):
  25.112 -
  25.113 -        "Remove an event for 'user' having the given 'uid'."
  25.114 -
  25.115 -        self.remove_recurrences(user, uid)
  25.116 -        return self.remove_parent_event(user, uid)
  25.117 -
  25.118 -    def remove_parent_event(self, user, uid):
  25.119 -
  25.120 -        "Remove the parent event for 'user' having the given 'uid'."
  25.121 -
  25.122 -        pass
  25.123 -
  25.124 -    def get_recurrences(self, user, uid):
  25.125 -
  25.126 -        """
  25.127 -        Get additional event instances for an event of the given 'user' with the
  25.128 -        indicated 'uid'. Both active and cancelled recurrences are returned.
  25.129 -        """
  25.130 -
  25.131 -        return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
  25.132 -
  25.133 -    def get_active_recurrences(self, user, uid):
  25.134 -
  25.135 -        """
  25.136 -        Get additional event instances for an event of the given 'user' with the
  25.137 -        indicated 'uid'. Cancelled recurrences are not returned.
  25.138 -        """
  25.139 -
  25.140 -        pass
  25.141 -
  25.142 -    def get_cancelled_recurrences(self, user, uid):
  25.143 -
  25.144 -        """
  25.145 -        Get additional event instances for an event of the given 'user' with the
  25.146 -        indicated 'uid'. Only cancelled recurrences are returned.
  25.147 -        """
  25.148 -
  25.149 -        pass
  25.150 -
  25.151 -    def get_recurrence(self, user, uid, recurrenceid):
  25.152 -
  25.153 -        """
  25.154 -        For the event of the given 'user' with the given 'uid', return the
  25.155 -        specific recurrence indicated by the 'recurrenceid'.
  25.156 -        """
  25.157 +# Build a catalogue of store types.
  25.158  
  25.159 -        pass
  25.160 -
  25.161 -    def set_recurrence(self, user, uid, recurrenceid, node):
  25.162 -
  25.163 -        "Set an event for 'user' having the given 'uid' and 'node'."
  25.164 -
  25.165 -        pass
  25.166 -
  25.167 -    def remove_recurrence(self, user, uid, recurrenceid):
  25.168 -
  25.169 -        """
  25.170 -        Remove a special recurrence from an event stored by 'user' having the
  25.171 -        given 'uid' and 'recurrenceid'.
  25.172 -        """
  25.173 -
  25.174 -        pass
  25.175 -
  25.176 -    def remove_recurrences(self, user, uid):
  25.177 -
  25.178 -        """
  25.179 -        Remove all recurrences for an event stored by 'user' having the given
  25.180 -        'uid'.
  25.181 -        """
  25.182 -
  25.183 -        for recurrenceid in self.get_recurrences(user, uid):
  25.184 -            self.remove_recurrence(user, uid, recurrenceid)
  25.185 -
  25.186 -        return self.remove_recurrence_collection(user, uid)
  25.187 -
  25.188 -    def remove_recurrence_collection(self, user, uid):
  25.189 -
  25.190 -        """
  25.191 -        Remove the collection of recurrences stored by 'user' having the given
  25.192 -        'uid'.
  25.193 -        """
  25.194 -
  25.195 -        pass
  25.196 -
  25.197 -    # Free/busy period providers, upon extension of the free/busy records.
  25.198 -
  25.199 -    def _get_freebusy_providers(self, user):
  25.200 -
  25.201 -        """
  25.202 -        Return the free/busy providers for the given 'user'.
  25.203 -
  25.204 -        This function returns any stored datetime and a list of providers as a
  25.205 -        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
  25.206 -        """
  25.207 -
  25.208 -        pass
  25.209 -
  25.210 -    def get_freebusy_providers(self, user, dt=None):
  25.211 -
  25.212 -        """
  25.213 -        Return a set of uncancelled events of the form (uid, recurrenceid)
  25.214 -        providing free/busy details beyond the given datetime 'dt'.
  25.215 -
  25.216 -        If 'dt' is not specified, all events previously found to provide
  25.217 -        details will be returned. Otherwise, if 'dt' is earlier than the
  25.218 -        datetime recorded for the known providers, None is returned, indicating
  25.219 -        that the list of providers must be recomputed.
  25.220 -
  25.221 -        This function returns a list of (uid, recurrenceid) tuples upon success.
  25.222 -        """
  25.223 -
  25.224 -        t = self._get_freebusy_providers(user)
  25.225 -        if not t:
  25.226 -            return None
  25.227 -
  25.228 -        dt_string, t = t
  25.229 -
  25.230 -        # If the requested datetime is earlier than the stated datetime, the
  25.231 -        # providers will need to be recomputed.
  25.232 -
  25.233 -        if dt:
  25.234 -            providers_dt = get_datetime(dt_string)
  25.235 -            if not providers_dt or providers_dt > dt:
  25.236 -                return None
  25.237 -
  25.238 -        # Otherwise, return the providers.
  25.239 -
  25.240 -        return t[1:]
  25.241 -
  25.242 -    def _set_freebusy_providers(self, user, dt_string, t):
  25.243 -
  25.244 -        "Set the given provider timestamp 'dt_string' and table 't'."
  25.245 -
  25.246 -        pass
  25.247 -
  25.248 -    def set_freebusy_providers(self, user, dt, providers):
  25.249 -
  25.250 -        """
  25.251 -        Define the uncancelled events providing free/busy details beyond the
  25.252 -        given datetime 'dt'.
  25.253 -        """
  25.254 -
  25.255 -        t = []
  25.256 -
  25.257 -        for obj in providers:
  25.258 -            t.append((obj.get_uid(), obj.get_recurrenceid()))
  25.259 -
  25.260 -        return self._set_freebusy_providers(user, format_datetime(dt), t)
  25.261 -
  25.262 -    def append_freebusy_provider(self, user, provider):
  25.263 -
  25.264 -        "For the given 'user', append the free/busy 'provider'."
  25.265 -
  25.266 -        t = self._get_freebusy_providers(user)
  25.267 -        if not t:
  25.268 -            return False
  25.269 -
  25.270 -        dt_string, t = t
  25.271 -        t.append((provider.get_uid(), provider.get_recurrenceid()))
  25.272 -
  25.273 -        return self._set_freebusy_providers(user, dt_string, t)
  25.274 -
  25.275 -    def remove_freebusy_provider(self, user, provider):
  25.276 -
  25.277 -        "For the given 'user', remove the free/busy 'provider'."
  25.278 -
  25.279 -        t = self._get_freebusy_providers(user)
  25.280 -        if not t:
  25.281 -            return False
  25.282 -
  25.283 -        dt_string, t = t
  25.284 -        try:
  25.285 -            t.remove((provider.get_uid(), provider.get_recurrenceid()))
  25.286 -        except ValueError:
  25.287 -            return False
  25.288 -
  25.289 -        return self._set_freebusy_providers(user, dt_string, t)
  25.290 -
  25.291 -    # Free/busy period access.
  25.292 -
  25.293 -    def get_freebusy(self, user, name=None):
  25.294 -
  25.295 -        "Get free/busy details for the given 'user'."
  25.296 -
  25.297 -        pass
  25.298 -
  25.299 -    def get_freebusy_for_other(self, user, other):
  25.300 +stores = {
  25.301 +    "file" : file,
  25.302 +    }
  25.303 +stores.update(database_stores)
  25.304  
  25.305 -        "For the given 'user', get free/busy details for the 'other' user."
  25.306 -
  25.307 -        pass
  25.308 -
  25.309 -    def set_freebusy(self, user, freebusy, name=None):
  25.310 -
  25.311 -        "For the given 'user', set 'freebusy' details."
  25.312 -
  25.313 -        pass
  25.314 -
  25.315 -    def set_freebusy_for_other(self, user, freebusy, other):
  25.316 -
  25.317 -        "For the given 'user', set 'freebusy' details for the 'other' user."
  25.318 -
  25.319 -        pass
  25.320 -
  25.321 -    # Tentative free/busy periods related to countering.
  25.322 -
  25.323 -    def get_freebusy_offers(self, user):
  25.324 -
  25.325 -        "Get free/busy offers for the given 'user'."
  25.326 -
  25.327 -        pass
  25.328 -
  25.329 -    def set_freebusy_offers(self, user, freebusy):
  25.330 -
  25.331 -        "For the given 'user', set 'freebusy' offers."
  25.332 -
  25.333 -        return self.set_freebusy(user, freebusy, "freebusy-offers")
  25.334 -
  25.335 -    # Requests and counter-proposals.
  25.336 -
  25.337 -    def get_requests(self, user):
  25.338 -
  25.339 -        "Get requests for the given 'user'."
  25.340 -
  25.341 -        pass
  25.342 -
  25.343 -    def set_requests(self, user, requests):
  25.344 -
  25.345 -        "For the given 'user', set the list of queued 'requests'."
  25.346 -
  25.347 -        pass
  25.348 -
  25.349 -    def set_request(self, user, uid, recurrenceid=None, type=None):
  25.350 -
  25.351 -        """
  25.352 -        For the given 'user', set the queued 'uid' and 'recurrenceid',
  25.353 -        indicating a request, along with any given 'type'.
  25.354 -        """
  25.355 -
  25.356 -        pass
  25.357 -
  25.358 -    def queue_request(self, user, uid, recurrenceid=None, type=None):
  25.359 -
  25.360 -        """
  25.361 -        Queue a request for 'user' having the given 'uid'. If the optional
  25.362 -        'recurrenceid' is specified, the entry refers to a specific instance
  25.363 -        or occurrence of an event. The 'type' parameter can be used to indicate
  25.364 -        a specific type of request.
  25.365 -        """
  25.366 -
  25.367 -        requests = self.get_requests(user) or []
  25.368 -
  25.369 -        if not self.have_request(requests, uid, recurrenceid):
  25.370 -            return self.set_request(user, uid, recurrenceid, type)
  25.371 -
  25.372 -        return False
  25.373 +# Access functions.
  25.374  
  25.375 -    def dequeue_request(self, user, uid, recurrenceid=None):
  25.376 -
  25.377 -        """
  25.378 -        Dequeue all requests for 'user' having the given 'uid'. If the optional
  25.379 -        'recurrenceid' is specified, all requests for that specific instance or
  25.380 -        occurrence of an event are dequeued.
  25.381 -        """
  25.382 -
  25.383 -        requests = self.get_requests(user) or []
  25.384 -        result = []
  25.385 -
  25.386 -        for request in requests:
  25.387 -            if request[:2] != (uid, recurrenceid):
  25.388 -                result.append(request)
  25.389 -
  25.390 -        self.set_requests(user, result)
  25.391 -        return True
  25.392 -
  25.393 -    def has_request(self, user, uid, recurrenceid=None, type=None, strict=False):
  25.394 -        return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict)
  25.395 -
  25.396 -    def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):
  25.397 -
  25.398 -        """
  25.399 -        Return whether 'requests' contains a request with the given 'uid' and
  25.400 -        any specified 'recurrenceid' and 'type'. If 'strict' is set to a true
  25.401 -        value, the precise type of the request must match; otherwise, any type
  25.402 -        of request for the identified object may be matched.
  25.403 -        """
  25.404 -
  25.405 -        for request in requests:
  25.406 -            if request[:2] == (uid, recurrenceid) and (
  25.407 -                not strict or
  25.408 -                not request[2:] and not type or
  25.409 -                request[2:] and request[2] == type):
  25.410 -
  25.411 -                return True
  25.412 -
  25.413 -        return False
  25.414 -
  25.415 -    def get_counters(self, user, uid, recurrenceid=None):
  25.416 -
  25.417 -        """
  25.418 -        For the given 'user', return a list of users from whom counter-proposals
  25.419 -        have been received for the given 'uid' and optional 'recurrenceid'.
  25.420 -        """
  25.421 -
  25.422 -        pass
  25.423 -
  25.424 -    def get_counter(self, user, other, uid, recurrenceid=None):
  25.425 -
  25.426 -        """
  25.427 -        For the given 'user', return the counter-proposal from 'other' for the
  25.428 -        given 'uid' and optional 'recurrenceid'.
  25.429 -        """
  25.430 -
  25.431 -        pass
  25.432 -
  25.433 -    def set_counter(self, user, other, node, uid, recurrenceid=None):
  25.434 -
  25.435 -        """
  25.436 -        For the given 'user', store a counter-proposal received from 'other' the
  25.437 -        given 'node' representing that proposal for the given 'uid' and
  25.438 -        'recurrenceid'.
  25.439 -        """
  25.440 -
  25.441 -        pass
  25.442 -
  25.443 -    def remove_counters(self, user, uid, recurrenceid=None):
  25.444 +def get_store(store_type, store_dir):
  25.445 +    return stores[store_type].Store(store_dir)
  25.446  
  25.447 -        """
  25.448 -        For the given 'user', remove all counter-proposals associated with the
  25.449 -        given 'uid' and 'recurrenceid'.
  25.450 -        """
  25.451 -
  25.452 -        pass
  25.453 -
  25.454 -    def remove_counter(self, user, other, uid, recurrenceid=None):
  25.455 -
  25.456 -        """
  25.457 -        For the given 'user', remove any counter-proposal from 'other'
  25.458 -        associated with the given 'uid' and 'recurrenceid'.
  25.459 -        """
  25.460 -
  25.461 -        pass
  25.462 -
  25.463 -    # Event cancellation.
  25.464 -
  25.465 -    def cancel_event(self, user, uid, recurrenceid=None):
  25.466 -
  25.467 -        """
  25.468 -        Cancel an event for 'user' having the given 'uid'. If the optional
  25.469 -        'recurrenceid' is specified, a specific instance or occurrence of an
  25.470 -        event is cancelled.
  25.471 -        """
  25.472 -
  25.473 -        pass
  25.474 -
  25.475 -    def uncancel_event(self, user, uid, recurrenceid=None):
  25.476 -
  25.477 -        """
  25.478 -        Uncancel an event for 'user' having the given 'uid'. If the optional
  25.479 -        'recurrenceid' is specified, a specific instance or occurrence of an
  25.480 -        event is uncancelled.
  25.481 -        """
  25.482 -
  25.483 -        pass
  25.484 -
  25.485 -    def remove_cancellations(self, user, uid, recurrenceid=None):
  25.486 -
  25.487 -        """
  25.488 -        Remove cancellations for 'user' for any event having the given 'uid'. If
  25.489 -        the optional 'recurrenceid' is specified, a specific instance or
  25.490 -        occurrence of an event is affected.
  25.491 -        """
  25.492 -
  25.493 -        # Remove all recurrence cancellations if a general event is indicated.
  25.494 -
  25.495 -        if not recurrenceid:
  25.496 -            for _recurrenceid in self.get_cancelled_recurrences(user, uid):
  25.497 -                self.remove_cancellation(user, uid, _recurrenceid)
  25.498 -
  25.499 -        return self.remove_cancellation(user, uid, recurrenceid)
  25.500 -
  25.501 -    def remove_cancellation(self, user, uid, recurrenceid=None):
  25.502 -
  25.503 -        """
  25.504 -        Remove a cancellation for 'user' for the event having the given 'uid'.
  25.505 -        If the optional 'recurrenceid' is specified, a specific instance or
  25.506 -        occurrence of an event is affected.
  25.507 -        """
  25.508 -
  25.509 -        pass
  25.510 -
  25.511 -class PublisherBase:
  25.512 -
  25.513 -    "The core operations of a data publisher."
  25.514 -
  25.515 -    def set_freebusy(self, user, freebusy):
  25.516 +def get_publisher(publishing_dir):
  25.517 +    return file.Publisher(publishing_dir)
  25.518  
  25.519 -        "For the given 'user', set 'freebusy' details."
  25.520 -
  25.521 -        pass
  25.522 -
  25.523 -class JournalBase:
  25.524 -
  25.525 -    "The core operations of a journal system supporting quotas."
  25.526 -
  25.527 -    # Quota and user identity/group discovery.
  25.528 -
  25.529 -    def get_quotas(self):
  25.530 -
  25.531 -        "Return a list of quotas."
  25.532 -
  25.533 -        pass
  25.534 -
  25.535 -    def get_quota_users(self, quota):
  25.536 -
  25.537 -        "Return a list of quota users."
  25.538 -
  25.539 -        pass
  25.540 -
  25.541 -    # Groups of users sharing quotas.
  25.542 -
  25.543 -    def get_groups(self, quota):
  25.544 -
  25.545 -        "Return the identity mappings for the given 'quota' as a dictionary."
  25.546 -
  25.547 -        pass
  25.548 -
  25.549 -    def get_limits(self, quota):
  25.550 -
  25.551 -        """
  25.552 -        Return the limits for the 'quota' as a dictionary mapping identities or
  25.553 -        groups to durations.
  25.554 -        """
  25.555 -
  25.556 -        pass
  25.557 -
  25.558 -    # Free/busy period access for users within quota groups.
  25.559 -
  25.560 -    def get_freebusy(self, quota, user):
  25.561 -
  25.562 -        "Get free/busy details for the given 'quota' and 'user'."
  25.563 -
  25.564 -        pass
  25.565 -
  25.566 -    def set_freebusy(self, quota, user, freebusy):
  25.567 -
  25.568 -        "For the given 'quota' and 'user', set 'freebusy' details."
  25.569 -
  25.570 -        pass
  25.571 -
  25.572 -    # Journal entry methods.
  25.573 -
  25.574 -    def get_entries(self, quota, group):
  25.575 -
  25.576 -        """
  25.577 -        Return a list of journal entries for the given 'quota' for the indicated
  25.578 -        'group'.
  25.579 -        """
  25.580 -
  25.581 -        pass
  25.582 -
  25.583 -    def set_entries(self, quota, group, entries):
  25.584 -
  25.585 -        """
  25.586 -        For the given 'quota' and indicated 'group', set the list of journal
  25.587 -        'entries'.
  25.588 -        """
  25.589 -
  25.590 -        pass
  25.591 +def get_journal(store_type, journal_dir):
  25.592 +    return stores[store_type].Journal(journal_dir)
  25.593  
  25.594  # vim: tabstop=4 expandtab shiftwidth=4
    26.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    26.2 +++ b/imiptools/stores/common.py	Fri Apr 22 16:22:58 2016 +0200
    26.3 @@ -0,0 +1,693 @@
    26.4 +#!/usr/bin/env python
    26.5 +
    26.6 +"""
    26.7 +General support for calendar data storage.
    26.8 +
    26.9 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
   26.10 +
   26.11 +This program is free software; you can redistribute it and/or modify it under
   26.12 +the terms of the GNU General Public License as published by the Free Software
   26.13 +Foundation; either version 3 of the License, or (at your option) any later
   26.14 +version.
   26.15 +
   26.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   26.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   26.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   26.19 +details.
   26.20 +
   26.21 +You should have received a copy of the GNU General Public License along with
   26.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   26.23 +"""
   26.24 +
   26.25 +from imiptools.dates import format_datetime, get_datetime
   26.26 +
   26.27 +class StoreBase:
   26.28 +
   26.29 +    "The core operations of a data store."
   26.30 +
   26.31 +    # User discovery.
   26.32 +
   26.33 +    def get_users(self):
   26.34 +
   26.35 +        "Return a list of users."
   26.36 +
   26.37 +        pass
   26.38 +
   26.39 +    # Event and event metadata access.
   26.40 +
   26.41 +    def get_all_events(self, user, dirname=None):
   26.42 +
   26.43 +        """
   26.44 +        Return a set of (uid, recurrenceid) tuples for all events. Unless
   26.45 +        'dirname' is specified, only active events are returned; otherwise,
   26.46 +        events from the given 'dirname' are returned.
   26.47 +        """
   26.48 +
   26.49 +        cancelled = self.get_cancelled_events(user)
   26.50 +        active = self.get_events(user)
   26.51 +
   26.52 +        if dirname == "cancellations":
   26.53 +            uids = cancelled
   26.54 +        else:
   26.55 +            uids = active
   26.56 +
   26.57 +        if not uids:
   26.58 +            return set()
   26.59 +
   26.60 +        all_events = set()
   26.61 +
   26.62 +        # Add all qualifying parent events to the result set.
   26.63 +
   26.64 +        for uid in uids:
   26.65 +            all_events.add((uid, None))
   26.66 +
   26.67 +        # Process all parent events regardless of status to find those in the
   26.68 +        # category/directory of interest.
   26.69 +
   26.70 +        for uid in active + cancelled:
   26.71 +
   26.72 +            if dirname == "cancellations":
   26.73 +                recurrenceids = self.get_cancelled_recurrences(user, uid)
   26.74 +            else:
   26.75 +                recurrenceids = self.get_active_recurrences(user, uid)
   26.76 +
   26.77 +            all_events.update([(uid, recurrenceid) for recurrenceid in recurrenceids])
   26.78 +
   26.79 +        return all_events
   26.80 +
   26.81 +    def get_events(self, user):
   26.82 +
   26.83 +        "Return a list of event identifiers."
   26.84 +
   26.85 +        pass
   26.86 +
   26.87 +    def get_cancelled_events(self, user):
   26.88 +
   26.89 +        "Return a list of event identifiers for cancelled events."
   26.90 +
   26.91 +        pass
   26.92 +
   26.93 +    def get_event(self, user, uid, recurrenceid=None, dirname=None):
   26.94 +
   26.95 +        """
   26.96 +        Get the event for the given 'user' with the given 'uid'. If
   26.97 +        the optional 'recurrenceid' is specified, a specific instance or
   26.98 +        occurrence of an event is returned.
   26.99 +        """
  26.100 +
  26.101 +        pass
  26.102 +
  26.103 +    def get_complete_event(self, user, uid):
  26.104 +
  26.105 +        "Get the event for the given 'user' with the given 'uid'."
  26.106 +
  26.107 +        pass
  26.108 +
  26.109 +    def set_event(self, user, uid, recurrenceid, node):
  26.110 +
  26.111 +        """
  26.112 +        Set an event for 'user' having the given 'uid' and 'recurrenceid' (which
  26.113 +        if the latter is specified, a specific instance or occurrence of an
  26.114 +        event is referenced), using the given 'node' description.
  26.115 +        """
  26.116 +
  26.117 +        if recurrenceid:
  26.118 +            return self.set_recurrence(user, uid, recurrenceid, node)
  26.119 +        else:
  26.120 +            return self.set_complete_event(user, uid, node)
  26.121 +
  26.122 +    def set_complete_event(self, user, uid, node):
  26.123 +
  26.124 +        "Set an event for 'user' having the given 'uid' and 'node'."
  26.125 +
  26.126 +        pass
  26.127 +
  26.128 +    def remove_event(self, user, uid, recurrenceid=None):
  26.129 +
  26.130 +        """
  26.131 +        Remove an event for 'user' having the given 'uid'. If the optional
  26.132 +        'recurrenceid' is specified, a specific instance or occurrence of an
  26.133 +        event is removed.
  26.134 +        """
  26.135 +
  26.136 +        if recurrenceid:
  26.137 +            return self.remove_recurrence(user, uid, recurrenceid)
  26.138 +        else:
  26.139 +            for recurrenceid in self.get_recurrences(user, uid) or []:
  26.140 +                self.remove_recurrence(user, uid, recurrenceid)
  26.141 +            return self.remove_complete_event(user, uid)
  26.142 +
  26.143 +    def remove_complete_event(self, user, uid):
  26.144 +
  26.145 +        "Remove an event for 'user' having the given 'uid'."
  26.146 +
  26.147 +        self.remove_recurrences(user, uid)
  26.148 +        return self.remove_parent_event(user, uid)
  26.149 +
  26.150 +    def remove_parent_event(self, user, uid):
  26.151 +
  26.152 +        "Remove the parent event for 'user' having the given 'uid'."
  26.153 +
  26.154 +        pass
  26.155 +
  26.156 +    def get_recurrences(self, user, uid):
  26.157 +
  26.158 +        """
  26.159 +        Get additional event instances for an event of the given 'user' with the
  26.160 +        indicated 'uid'. Both active and cancelled recurrences are returned.
  26.161 +        """
  26.162 +
  26.163 +        return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
  26.164 +
  26.165 +    def get_active_recurrences(self, user, uid):
  26.166 +
  26.167 +        """
  26.168 +        Get additional event instances for an event of the given 'user' with the
  26.169 +        indicated 'uid'. Cancelled recurrences are not returned.
  26.170 +        """
  26.171 +
  26.172 +        pass
  26.173 +
  26.174 +    def get_cancelled_recurrences(self, user, uid):
  26.175 +
  26.176 +        """
  26.177 +        Get additional event instances for an event of the given 'user' with the
  26.178 +        indicated 'uid'. Only cancelled recurrences are returned.
  26.179 +        """
  26.180 +
  26.181 +        pass
  26.182 +
  26.183 +    def get_recurrence(self, user, uid, recurrenceid):
  26.184 +
  26.185 +        """
  26.186 +        For the event of the given 'user' with the given 'uid', return the
  26.187 +        specific recurrence indicated by the 'recurrenceid'.
  26.188 +        """
  26.189 +
  26.190 +        pass
  26.191 +
  26.192 +    def set_recurrence(self, user, uid, recurrenceid, node):
  26.193 +
  26.194 +        "Set an event for 'user' having the given 'uid' and 'node'."
  26.195 +
  26.196 +        pass
  26.197 +
  26.198 +    def remove_recurrence(self, user, uid, recurrenceid):
  26.199 +
  26.200 +        """
  26.201 +        Remove a special recurrence from an event stored by 'user' having the
  26.202 +        given 'uid' and 'recurrenceid'.
  26.203 +        """
  26.204 +
  26.205 +        pass
  26.206 +
  26.207 +    def remove_recurrences(self, user, uid):
  26.208 +
  26.209 +        """
  26.210 +        Remove all recurrences for an event stored by 'user' having the given
  26.211 +        'uid'.
  26.212 +        """
  26.213 +
  26.214 +        for recurrenceid in self.get_recurrences(user, uid):
  26.215 +            self.remove_recurrence(user, uid, recurrenceid)
  26.216 +
  26.217 +        return self.remove_recurrence_collection(user, uid)
  26.218 +
  26.219 +    def remove_recurrence_collection(self, user, uid):
  26.220 +
  26.221 +        """
  26.222 +        Remove the collection of recurrences stored by 'user' having the given
  26.223 +        'uid'.
  26.224 +        """
  26.225 +
  26.226 +        pass
  26.227 +
  26.228 +    # Free/busy period providers, upon extension of the free/busy records.
  26.229 +
  26.230 +    def _get_freebusy_providers(self, user):
  26.231 +
  26.232 +        """
  26.233 +        Return the free/busy providers for the given 'user'.
  26.234 +
  26.235 +        This function returns any stored datetime and a list of providers as a
  26.236 +        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
  26.237 +        """
  26.238 +
  26.239 +        pass
  26.240 +
  26.241 +    def get_freebusy_providers(self, user, dt=None):
  26.242 +
  26.243 +        """
  26.244 +        Return a set of uncancelled events of the form (uid, recurrenceid)
  26.245 +        providing free/busy details beyond the given datetime 'dt'.
  26.246 +
  26.247 +        If 'dt' is not specified, all events previously found to provide
  26.248 +        details will be returned. Otherwise, if 'dt' is earlier than the
  26.249 +        datetime recorded for the known providers, None is returned, indicating
  26.250 +        that the list of providers must be recomputed.
  26.251 +
  26.252 +        This function returns a list of (uid, recurrenceid) tuples upon success.
  26.253 +        """
  26.254 +
  26.255 +        t = self._get_freebusy_providers(user)
  26.256 +        if not t:
  26.257 +            return None
  26.258 +
  26.259 +        dt_string, t = t
  26.260 +
  26.261 +        # If the requested datetime is earlier than the stated datetime, the
  26.262 +        # providers will need to be recomputed.
  26.263 +
  26.264 +        if dt:
  26.265 +            providers_dt = get_datetime(dt_string)
  26.266 +            if not providers_dt or providers_dt > dt:
  26.267 +                return None
  26.268 +
  26.269 +        # Otherwise, return the providers.
  26.270 +
  26.271 +        return t
  26.272 +
  26.273 +    def _set_freebusy_providers(self, user, dt_string, t):
  26.274 +
  26.275 +        "Set the given provider timestamp 'dt_string' and table 't'."
  26.276 +
  26.277 +        pass
  26.278 +
  26.279 +    def set_freebusy_providers(self, user, dt, providers):
  26.280 +
  26.281 +        """
  26.282 +        Define the uncancelled events providing free/busy details beyond the
  26.283 +        given datetime 'dt'.
  26.284 +        """
  26.285 +
  26.286 +        t = []
  26.287 +
  26.288 +        for obj in providers:
  26.289 +            t.append((obj.get_uid(), obj.get_recurrenceid()))
  26.290 +
  26.291 +        return self._set_freebusy_providers(user, format_datetime(dt), t)
  26.292 +
  26.293 +    def append_freebusy_provider(self, user, provider):
  26.294 +
  26.295 +        "For the given 'user', append the free/busy 'provider'."
  26.296 +
  26.297 +        t = self._get_freebusy_providers(user)
  26.298 +        if not t:
  26.299 +            return False
  26.300 +
  26.301 +        dt_string, t = t
  26.302 +        t.append((provider.get_uid(), provider.get_recurrenceid()))
  26.303 +
  26.304 +        return self._set_freebusy_providers(user, dt_string, t)
  26.305 +
  26.306 +    def remove_freebusy_provider(self, user, provider):
  26.307 +
  26.308 +        "For the given 'user', remove the free/busy 'provider'."
  26.309 +
  26.310 +        t = self._get_freebusy_providers(user)
  26.311 +        if not t:
  26.312 +            return False
  26.313 +
  26.314 +        dt_string, t = t
  26.315 +        try:
  26.316 +            t.remove((provider.get_uid(), provider.get_recurrenceid()))
  26.317 +        except ValueError:
  26.318 +            return False
  26.319 +
  26.320 +        return self._set_freebusy_providers(user, dt_string, t)
  26.321 +
  26.322 +    # Free/busy period access.
  26.323 +
  26.324 +    def get_freebusy(self, user, name=None, mutable=False):
  26.325 +
  26.326 +        "Get free/busy details for the given 'user'."
  26.327 +
  26.328 +        pass
  26.329 +
  26.330 +    def get_freebusy_for_other(self, user, other, mutable=False):
  26.331 +
  26.332 +        "For the given 'user', get free/busy details for the 'other' user."
  26.333 +
  26.334 +        pass
  26.335 +
  26.336 +    def get_freebusy_for_update(self, user, name=None):
  26.337 +
  26.338 +        """
  26.339 +        Get free/busy details for the given 'user' that may be updated,
  26.340 +        potentially affecting the stored information directly.
  26.341 +        """
  26.342 +
  26.343 +        return self.get_freebusy(user, name, True)
  26.344 +
  26.345 +    def get_freebusy_for_other_for_update(self, user, other):
  26.346 +
  26.347 +        """
  26.348 +        For the given 'user', get free/busy details for the 'other' user
  26.349 +        that may be updated, potentially affecting the stored information
  26.350 +        directly.
  26.351 +        """
  26.352 +
  26.353 +        return self.get_freebusy_for_other(user, other, True)
  26.354 +
  26.355 +    def set_freebusy(self, user, freebusy, name=None):
  26.356 +
  26.357 +        "For the given 'user', set 'freebusy' details."
  26.358 +
  26.359 +        pass
  26.360 +
  26.361 +    def set_freebusy_for_other(self, user, freebusy, other):
  26.362 +
  26.363 +        "For the given 'user', set 'freebusy' details for the 'other' user."
  26.364 +
  26.365 +        pass
  26.366 +
  26.367 +    def get_freebusy_others(self, user):
  26.368 +
  26.369 +        """
  26.370 +        For the given 'user', return a list of other users for whom free/busy
  26.371 +        information is retained.
  26.372 +        """
  26.373 +
  26.374 +        pass
  26.375 +
  26.376 +    # Tentative free/busy periods related to countering.
  26.377 +
  26.378 +    def get_freebusy_offers(self, user, mutable=False):
  26.379 +
  26.380 +        "Get free/busy offers for the given 'user'."
  26.381 +
  26.382 +        pass
  26.383 +
  26.384 +    def get_freebusy_offers_for_update(self, user):
  26.385 +
  26.386 +        """
  26.387 +        Get free/busy offers for the given 'user' that may be updated,
  26.388 +        potentially affecting the stored information directly.
  26.389 +        """
  26.390 +
  26.391 +        return self.get_freebusy_offers(user, True)
  26.392 +
  26.393 +    def set_freebusy_offers(self, user, freebusy):
  26.394 +
  26.395 +        "For the given 'user', set 'freebusy' offers."
  26.396 +
  26.397 +        return self.set_freebusy(user, freebusy, "freebusy-offers")
  26.398 +
  26.399 +    # Requests and counter-proposals.
  26.400 +
  26.401 +    def get_requests(self, user):
  26.402 +
  26.403 +        "Get requests for the given 'user'."
  26.404 +
  26.405 +        pass
  26.406 +
  26.407 +    def set_requests(self, user, requests):
  26.408 +
  26.409 +        "For the given 'user', set the list of queued 'requests'."
  26.410 +
  26.411 +        pass
  26.412 +
  26.413 +    def set_request(self, user, uid, recurrenceid=None, type=None):
  26.414 +
  26.415 +        """
  26.416 +        For the given 'user', set the queued 'uid' and 'recurrenceid',
  26.417 +        indicating a request, along with any given 'type'.
  26.418 +        """
  26.419 +
  26.420 +        pass
  26.421 +
  26.422 +    def queue_request(self, user, uid, recurrenceid=None, type=None):
  26.423 +
  26.424 +        """
  26.425 +        Queue a request for 'user' having the given 'uid'. If the optional
  26.426 +        'recurrenceid' is specified, the entry refers to a specific instance
  26.427 +        or occurrence of an event. The 'type' parameter can be used to indicate
  26.428 +        a specific type of request.
  26.429 +        """
  26.430 +
  26.431 +        requests = self.get_requests(user) or []
  26.432 +
  26.433 +        if not self.have_request(requests, uid, recurrenceid):
  26.434 +            return self.set_request(user, uid, recurrenceid, type)
  26.435 +
  26.436 +        return False
  26.437 +
  26.438 +    def dequeue_request(self, user, uid, recurrenceid=None):
  26.439 +
  26.440 +        """
  26.441 +        Dequeue all requests for 'user' having the given 'uid'. If the optional
  26.442 +        'recurrenceid' is specified, all requests for that specific instance or
  26.443 +        occurrence of an event are dequeued.
  26.444 +        """
  26.445 +
  26.446 +        requests = self.get_requests(user) or []
  26.447 +        result = []
  26.448 +
  26.449 +        for request in requests:
  26.450 +            if request[:2] != (uid, recurrenceid):
  26.451 +                result.append(request)
  26.452 +
  26.453 +        self.set_requests(user, result)
  26.454 +        return True
  26.455 +
  26.456 +    def has_request(self, user, uid, recurrenceid=None, type=None, strict=False):
  26.457 +        return self.have_request(self.get_requests(user) or [], uid, recurrenceid, type, strict)
  26.458 +
  26.459 +    def have_request(self, requests, uid, recurrenceid=None, type=None, strict=False):
  26.460 +
  26.461 +        """
  26.462 +        Return whether 'requests' contains a request with the given 'uid' and
  26.463 +        any specified 'recurrenceid' and 'type'. If 'strict' is set to a true
  26.464 +        value, the precise type of the request must match; otherwise, any type
  26.465 +        of request for the identified object may be matched.
  26.466 +        """
  26.467 +
  26.468 +        for request in requests:
  26.469 +            if request[:2] == (uid, recurrenceid) and (
  26.470 +                not strict or
  26.471 +                not request[2:] and not type or
  26.472 +                request[2:] and request[2] == type):
  26.473 +
  26.474 +                return True
  26.475 +
  26.476 +        return False
  26.477 +
  26.478 +    def get_counters(self, user, uid, recurrenceid=None):
  26.479 +
  26.480 +        """
  26.481 +        For the given 'user', return a list of users from whom counter-proposals
  26.482 +        have been received for the given 'uid' and optional 'recurrenceid'.
  26.483 +        """
  26.484 +
  26.485 +        pass
  26.486 +
  26.487 +    def get_counter(self, user, other, uid, recurrenceid=None):
  26.488 +
  26.489 +        """
  26.490 +        For the given 'user', return the counter-proposal from 'other' for the
  26.491 +        given 'uid' and optional 'recurrenceid'.
  26.492 +        """
  26.493 +
  26.494 +        pass
  26.495 +
  26.496 +    def set_counter(self, user, other, node, uid, recurrenceid=None):
  26.497 +
  26.498 +        """
  26.499 +        For the given 'user', store a counter-proposal received from 'other' the
  26.500 +        given 'node' representing that proposal for the given 'uid' and
  26.501 +        'recurrenceid'.
  26.502 +        """
  26.503 +
  26.504 +        pass
  26.505 +
  26.506 +    def remove_counters(self, user, uid, recurrenceid=None):
  26.507 +
  26.508 +        """
  26.509 +        For the given 'user', remove all counter-proposals associated with the
  26.510 +        given 'uid' and 'recurrenceid'.
  26.511 +        """
  26.512 +
  26.513 +        pass
  26.514 +
  26.515 +    def remove_counter(self, user, other, uid, recurrenceid=None):
  26.516 +
  26.517 +        """
  26.518 +        For the given 'user', remove any counter-proposal from 'other'
  26.519 +        associated with the given 'uid' and 'recurrenceid'.
  26.520 +        """
  26.521 +
  26.522 +        pass
  26.523 +
  26.524 +    # Event cancellation.
  26.525 +
  26.526 +    def cancel_event(self, user, uid, recurrenceid=None):
  26.527 +
  26.528 +        """
  26.529 +        Cancel an event for 'user' having the given 'uid'. If the optional
  26.530 +        'recurrenceid' is specified, a specific instance or occurrence of an
  26.531 +        event is cancelled.
  26.532 +        """
  26.533 +
  26.534 +        pass
  26.535 +
  26.536 +    def uncancel_event(self, user, uid, recurrenceid=None):
  26.537 +
  26.538 +        """
  26.539 +        Uncancel an event for 'user' having the given 'uid'. If the optional
  26.540 +        'recurrenceid' is specified, a specific instance or occurrence of an
  26.541 +        event is uncancelled.
  26.542 +        """
  26.543 +
  26.544 +        pass
  26.545 +
  26.546 +    def remove_cancellations(self, user, uid, recurrenceid=None):
  26.547 +
  26.548 +        """
  26.549 +        Remove cancellations for 'user' for any event having the given 'uid'. If
  26.550 +        the optional 'recurrenceid' is specified, a specific instance or
  26.551 +        occurrence of an event is affected.
  26.552 +        """
  26.553 +
  26.554 +        # Remove all recurrence cancellations if a general event is indicated.
  26.555 +
  26.556 +        if not recurrenceid:
  26.557 +            for _recurrenceid in self.get_cancelled_recurrences(user, uid):
  26.558 +                self.remove_cancellation(user, uid, _recurrenceid)
  26.559 +
  26.560 +        return self.remove_cancellation(user, uid, recurrenceid)
  26.561 +
  26.562 +    def remove_cancellation(self, user, uid, recurrenceid=None):
  26.563 +
  26.564 +        """
  26.565 +        Remove a cancellation for 'user' for the event having the given 'uid'.
  26.566 +        If the optional 'recurrenceid' is specified, a specific instance or
  26.567 +        occurrence of an event is affected.
  26.568 +        """
  26.569 +
  26.570 +        pass
  26.571 +
  26.572 +class PublisherBase:
  26.573 +
  26.574 +    "The core operations of a data publisher."
  26.575 +
  26.576 +    def set_freebusy(self, user, freebusy):
  26.577 +
  26.578 +        "For the given 'user', set 'freebusy' details."
  26.579 +
  26.580 +        pass
  26.581 +
  26.582 +class JournalBase:
  26.583 +
  26.584 +    "The core operations of a journal system supporting quotas."
  26.585 +
  26.586 +    # Quota and user identity/group discovery.
  26.587 +
  26.588 +    def get_quotas(self):
  26.589 +
  26.590 +        "Return a list of quotas."
  26.591 +
  26.592 +        pass
  26.593 +
  26.594 +    def get_quota_users(self, quota):
  26.595 +
  26.596 +        "Return a list of quota users."
  26.597 +
  26.598 +        pass
  26.599 +
  26.600 +    # Groups of users sharing quotas.
  26.601 +
  26.602 +    def get_groups(self, quota):
  26.603 +
  26.604 +        "Return the identity mappings for the given 'quota' as a dictionary."
  26.605 +
  26.606 +        pass
  26.607 +
  26.608 +    def set_group(self, quota, store_user, user_group):
  26.609 +
  26.610 +        """
  26.611 +        For the given 'quota', set a mapping from 'store_user' to 'user_group'.
  26.612 +        """
  26.613 +
  26.614 +        pass
  26.615 +
  26.616 +    def get_limits(self, quota):
  26.617 +
  26.618 +        """
  26.619 +        Return the limits for the 'quota' as a dictionary mapping identities or
  26.620 +        groups to durations.
  26.621 +        """
  26.622 +
  26.623 +        pass
  26.624 +
  26.625 +    def set_limit(self, quota, group, limit):
  26.626 +
  26.627 +        """
  26.628 +        For the given 'quota', set for a user 'group' the given 'limit' on
  26.629 +        resource usage.
  26.630 +        """
  26.631 +
  26.632 +        pass
  26.633 +
  26.634 +    # Free/busy period access for users within quota groups.
  26.635 +
  26.636 +    def get_freebusy_users(self, quota):
  26.637 +
  26.638 +        """
  26.639 +        Return a list of users whose free/busy details are retained for the
  26.640 +        given 'quota'.
  26.641 +        """
  26.642 +
  26.643 +        pass
  26.644 +
  26.645 +    def get_freebusy(self, quota, user, mutable=False):
  26.646 +
  26.647 +        "Get free/busy details for the given 'quota' and 'user'."
  26.648 +
  26.649 +        pass
  26.650 +
  26.651 +    def get_freebusy_for_update(self, quota, user):
  26.652 +
  26.653 +        """
  26.654 +        Get free/busy details for the given 'quota' and 'user' that may be
  26.655 +        updated, potentially affecting the stored information directly.
  26.656 +        """
  26.657 +
  26.658 +        return self.get_freebusy(quota, user, True)
  26.659 +
  26.660 +    def set_freebusy(self, quota, user, freebusy):
  26.661 +
  26.662 +        "For the given 'quota' and 'user', set 'freebusy' details."
  26.663 +
  26.664 +        pass
  26.665 +
  26.666 +    # Journal entry methods.
  26.667 +
  26.668 +    def get_entries(self, quota, group, mutable=False):
  26.669 +
  26.670 +        """
  26.671 +        Return a list of journal entries for the given 'quota' for the indicated
  26.672 +        'group'.
  26.673 +        """
  26.674 +
  26.675 +        pass
  26.676 +
  26.677 +    def get_entries_for_update(self, quota, group):
  26.678 +
  26.679 +        """
  26.680 +        Return a list of journal entries for the given 'quota' for the indicated
  26.681 +        'group' that may be updated, potentially affecting the stored
  26.682 +        information directly.
  26.683 +        """
  26.684 +
  26.685 +        return self.get_entries(quota, group, True)
  26.686 +
  26.687 +    def set_entries(self, quota, group, entries):
  26.688 +
  26.689 +        """
  26.690 +        For the given 'quota' and indicated 'group', set the list of journal
  26.691 +        'entries'.
  26.692 +        """
  26.693 +
  26.694 +        pass
  26.695 +
  26.696 +# vim: tabstop=4 expandtab shiftwidth=4
    27.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    27.2 +++ b/imiptools/stores/database/__init__.py	Fri Apr 22 16:22:58 2016 +0200
    27.3 @@ -0,0 +1,28 @@
    27.4 +#!/usr/bin/env python
    27.5 +
    27.6 +"""
    27.7 +General support for database stores.
    27.8 +
    27.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
   27.10 +
   27.11 +This program is free software; you can redistribute it and/or modify it under
   27.12 +the terms of the GNU General Public License as published by the Free Software
   27.13 +Foundation; either version 3 of the License, or (at your option) any later
   27.14 +version.
   27.15 +
   27.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   27.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   27.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   27.19 +details.
   27.20 +
   27.21 +You should have received a copy of the GNU General Public License along with
   27.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   27.23 +"""
   27.24 +
   27.25 +from imiptools.stores.database import postgresql
   27.26 +
   27.27 +stores = {
   27.28 +    "postgresql" : postgresql,
   27.29 +    }
   27.30 +
   27.31 +# vim: tabstop=4 expandtab shiftwidth=4
    28.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    28.2 +++ b/imiptools/stores/database/common.py	Fri Apr 22 16:22:58 2016 +0200
    28.3 @@ -0,0 +1,1001 @@
    28.4 +#!/usr/bin/env python
    28.5 +
    28.6 +"""
    28.7 +A database store of calendar data.
    28.8 +
    28.9 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
   28.10 +
   28.11 +This program is free software; you can redistribute it and/or modify it under
   28.12 +the terms of the GNU General Public License as published by the Free Software
   28.13 +Foundation; either version 3 of the License, or (at your option) any later
   28.14 +version.
   28.15 +
   28.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   28.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   28.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   28.19 +details.
   28.20 +
   28.21 +You should have received a copy of the GNU General Public License along with
   28.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   28.23 +"""
   28.24 +
   28.25 +from imiptools.stores.common import StoreBase, JournalBase
   28.26 +
   28.27 +from datetime import datetime
   28.28 +from imiptools.data import parse_string, to_string
   28.29 +from imiptools.dates import format_datetime, get_datetime, to_timezone
   28.30 +from imiptools.period import FreeBusyDatabaseCollection
   28.31 +from imiptools.sql import DatabaseOperations
   28.32 +
   28.33 +class DatabaseStoreBase(DatabaseOperations):
   28.34 +
   28.35 +    "A database store supporting user-specific locking."
   28.36 +
   28.37 +    def __init__(self, connection, paramstyle=None):
   28.38 +        DatabaseOperations.__init__(self, paramstyle=paramstyle)
   28.39 +        self.connection = connection
   28.40 +        self.cursor = connection.cursor()
   28.41 +
   28.42 +    def acquire_lock(self, user, timeout=None):
   28.43 +        pass
   28.44 +
   28.45 +    def release_lock(self, user):
   28.46 +        pass
   28.47 +
   28.48 +class DatabaseStore(DatabaseStoreBase, StoreBase):
   28.49 +
   28.50 +    "A database store of tabular free/busy data and objects."
   28.51 +
   28.52 +    # User discovery.
   28.53 +
   28.54 +    def get_users(self):
   28.55 +
   28.56 +        "Return a list of users."
   28.57 +
   28.58 +        query = "select distinct store_user from (" \
   28.59 +                "select store_user from freebusy " \
   28.60 +                "union all select store_user from objects " \
   28.61 +                "union all select store_user from recurrences" \
   28.62 +                ") as users"
   28.63 +        self.cursor.execute(query)
   28.64 +        return [r[0] for r in self.cursor.fetchall()]
   28.65 +
   28.66 +    # Event and event metadata access.
   28.67 +
   28.68 +    def get_all_events(self, user, dirname=None):
   28.69 +
   28.70 +        """
   28.71 +        Return a set of (uid, recurrenceid) tuples for all events. Unless
   28.72 +        'dirname' is specified, only active events are returned; otherwise,
   28.73 +        events from the given 'dirname' are returned.
   28.74 +        """
   28.75 +
   28.76 +        columns, values = self.get_event_table_filters(dirname)
   28.77 +
   28.78 +        columns += ["store_user"]
   28.79 +        values += [user]
   28.80 +
   28.81 +        query, values = self.get_query(
   28.82 +            "select object_uid, null as object_recurrenceid from objects :condition "
   28.83 +            "union all "
   28.84 +            "select object_uid, object_recurrenceid from recurrences :condition",
   28.85 +            columns, values)
   28.86 +
   28.87 +        self.cursor.execute(query, values)
   28.88 +        return self.cursor.fetchall()
   28.89 +
   28.90 +    def get_events(self, user, dirname=None):
   28.91 +
   28.92 +        "Return a list of event identifiers."
   28.93 +
   28.94 +        columns, values = self.get_event_table_filters(dirname)
   28.95 +
   28.96 +        columns += ["store_user"]
   28.97 +        values += [user]
   28.98 +
   28.99 +        query, values = self.get_query(
  28.100 +            "select object_uid from objects :condition",
  28.101 +            columns, values)
  28.102 +
  28.103 +        self.cursor.execute(query, values)
  28.104 +        return [r[0] for r in self.cursor.fetchall()]
  28.105 +
  28.106 +    def get_cancelled_events(self, user):
  28.107 +
  28.108 +        "Return a list of event identifiers for cancelled events."
  28.109 +
  28.110 +        return self.get_events(user, "cancellations")
  28.111 +
  28.112 +    def get_event(self, user, uid, recurrenceid=None, dirname=None):
  28.113 +
  28.114 +        """
  28.115 +        Get the event for the given 'user' with the given 'uid'. If
  28.116 +        the optional 'recurrenceid' is specified, a specific instance or
  28.117 +        occurrence of an event is returned.
  28.118 +        """
  28.119 +
  28.120 +        table = self.get_event_table(recurrenceid, dirname)
  28.121 +        columns, values = self.get_event_table_filters(dirname)
  28.122 +
  28.123 +        if recurrenceid:
  28.124 +            columns += ["store_user", "object_uid", "object_recurrenceid"]
  28.125 +            values += [user, uid, recurrenceid]
  28.126 +        else:
  28.127 +            columns += ["store_user", "object_uid"]
  28.128 +            values += [user, uid]
  28.129 +
  28.130 +        query, values = self.get_query(
  28.131 +            "select object_text from %(table)s :condition" % {
  28.132 +                "table" : table
  28.133 +                },
  28.134 +            columns, values)
  28.135 +
  28.136 +        self.cursor.execute(query, values)
  28.137 +        result = self.cursor.fetchone()
  28.138 +        return result and parse_string(result[0], "utf-8")
  28.139 +
  28.140 +    def get_complete_event(self, user, uid):
  28.141 +
  28.142 +        "Get the event for the given 'user' with the given 'uid'."
  28.143 +
  28.144 +        columns = ["store_user", "object_uid"]
  28.145 +        values = [user, uid]
  28.146 +
  28.147 +        query, values = self.get_query(
  28.148 +            "select object_text from objects :condition",
  28.149 +            columns, values)
  28.150 +
  28.151 +        self.cursor.execute(query, values)
  28.152 +        result = self.cursor.fetchone()
  28.153 +        return result and parse_string(result[0], "utf-8")
  28.154 +
  28.155 +    def set_complete_event(self, user, uid, node):
  28.156 +
  28.157 +        "Set an event for 'user' having the given 'uid' and 'node'."
  28.158 +
  28.159 +        columns = ["store_user", "object_uid"]
  28.160 +        values = [user, uid]
  28.161 +        setcolumns = ["object_text", "status"]
  28.162 +        setvalues = [to_string(node, "utf-8"), "active"]
  28.163 +
  28.164 +        query, values = self.get_query(
  28.165 +            "update objects :set :condition",
  28.166 +            columns, values, setcolumns, setvalues)
  28.167 +
  28.168 +        self.cursor.execute(query, values)
  28.169 +
  28.170 +        if self.cursor.rowcount > 0 or self.get_complete_event(user, uid):
  28.171 +            return True
  28.172 +
  28.173 +        columns = ["store_user", "object_uid", "object_text", "status"]
  28.174 +        values = [user, uid, to_string(node, "utf-8"), "active"]
  28.175 +
  28.176 +        query, values = self.get_query(
  28.177 +            "insert into objects (:columns) values (:values)",
  28.178 +            columns, values)
  28.179 +
  28.180 +        self.cursor.execute(query, values)
  28.181 +        return True
  28.182 +
  28.183 +    def remove_parent_event(self, user, uid):
  28.184 +
  28.185 +        "Remove the parent event for 'user' having the given 'uid'."
  28.186 +
  28.187 +        columns = ["store_user", "object_uid"]
  28.188 +        values = [user, uid]
  28.189 +
  28.190 +        query, values = self.get_query(
  28.191 +            "delete from objects :condition",
  28.192 +            columns, values)
  28.193 +
  28.194 +        self.cursor.execute(query, values)
  28.195 +        return self.cursor.rowcount > 0
  28.196 +
  28.197 +    def get_active_recurrences(self, user, uid):
  28.198 +
  28.199 +        """
  28.200 +        Get additional event instances for an event of the given 'user' with the
  28.201 +        indicated 'uid'. Cancelled recurrences are not returned.
  28.202 +        """
  28.203 +
  28.204 +        columns = ["store_user", "object_uid", "status"]
  28.205 +        values = [user, uid, "active"]
  28.206 +
  28.207 +        query, values = self.get_query(
  28.208 +            "select object_recurrenceid from recurrences :condition",
  28.209 +            columns, values)
  28.210 +
  28.211 +        self.cursor.execute(query, values)
  28.212 +        return [t[0] for t in self.cursor.fetchall() or []]
  28.213 +
  28.214 +    def get_cancelled_recurrences(self, user, uid):
  28.215 +
  28.216 +        """
  28.217 +        Get additional event instances for an event of the given 'user' with the
  28.218 +        indicated 'uid'. Only cancelled recurrences are returned.
  28.219 +        """
  28.220 +
  28.221 +        columns = ["store_user", "object_uid", "status"]
  28.222 +        values = [user, uid, "cancelled"]
  28.223 +
  28.224 +        query, values = self.get_query(
  28.225 +            "select object_recurrenceid from recurrences :condition",
  28.226 +            columns, values)
  28.227 +
  28.228 +        self.cursor.execute(query, values)
  28.229 +        return [t[0] for t in self.cursor.fetchall() or []]
  28.230 +
  28.231 +    def get_recurrence(self, user, uid, recurrenceid):
  28.232 +
  28.233 +        """
  28.234 +        For the event of the given 'user' with the given 'uid', return the
  28.235 +        specific recurrence indicated by the 'recurrenceid'.
  28.236 +        """
  28.237 +
  28.238 +        columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.239 +        values = [user, uid, recurrenceid]
  28.240 +
  28.241 +        query, values = self.get_query(
  28.242 +            "select object_text from recurrences :condition",
  28.243 +            columns, values)
  28.244 +
  28.245 +        self.cursor.execute(query, values)
  28.246 +        result = self.cursor.fetchone()
  28.247 +        return result and parse_string(result[0], "utf-8")
  28.248 +
  28.249 +    def set_recurrence(self, user, uid, recurrenceid, node):
  28.250 +
  28.251 +        "Set an event for 'user' having the given 'uid' and 'node'."
  28.252 +
  28.253 +        columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.254 +        values = [user, uid, recurrenceid]
  28.255 +        setcolumns = ["object_text", "status"]
  28.256 +        setvalues = [to_string(node, "utf-8"), "active"]
  28.257 +
  28.258 +        query, values = self.get_query(
  28.259 +            "update recurrences :set :condition",
  28.260 +            columns, values, setcolumns, setvalues)
  28.261 +
  28.262 +        self.cursor.execute(query, values)
  28.263 +
  28.264 +        if self.cursor.rowcount > 0 or self.get_recurrence(user, uid, recurrenceid):
  28.265 +            return True
  28.266 +
  28.267 +        columns = ["store_user", "object_uid", "object_recurrenceid", "object_text", "status"]
  28.268 +        values = [user, uid, recurrenceid, to_string(node, "utf-8"), "active"]
  28.269 +
  28.270 +        query, values = self.get_query(
  28.271 +            "insert into recurrences (:columns) values (:values)",
  28.272 +            columns, values)
  28.273 +
  28.274 +        self.cursor.execute(query, values)
  28.275 +        return True
  28.276 +
  28.277 +    def remove_recurrence(self, user, uid, recurrenceid):
  28.278 +
  28.279 +        """
  28.280 +        Remove a special recurrence from an event stored by 'user' having the
  28.281 +        given 'uid' and 'recurrenceid'.
  28.282 +        """
  28.283 +
  28.284 +        columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.285 +        values = [user, uid, recurrenceid]
  28.286 +
  28.287 +        query, values = self.get_query(
  28.288 +            "delete from recurrences :condition",
  28.289 +            columns, values)
  28.290 +
  28.291 +        self.cursor.execute(query, values)
  28.292 +        return True
  28.293 +
  28.294 +    def remove_recurrences(self, user, uid):
  28.295 +
  28.296 +        """
  28.297 +        Remove all recurrences for an event stored by 'user' having the given
  28.298 +        'uid'.
  28.299 +        """
  28.300 +
  28.301 +        columns = ["store_user", "object_uid"]
  28.302 +        values = [user, uid]
  28.303 +
  28.304 +        query, values = self.get_query(
  28.305 +            "delete from recurrences :condition",
  28.306 +            columns, values)
  28.307 +
  28.308 +        self.cursor.execute(query, values)
  28.309 +        return True
  28.310 +
  28.311 +    # Event table computation.
  28.312 +
  28.313 +    def get_event_table(self, recurrenceid=None, dirname=None):
  28.314 +
  28.315 +        "Get the table providing events for any specified 'dirname'."
  28.316 +
  28.317 +        if recurrenceid:
  28.318 +            return self.get_recurrence_table(dirname)
  28.319 +        else:
  28.320 +            return self.get_complete_event_table(dirname)
  28.321 +
  28.322 +    def get_event_table_filters(self, dirname=None):
  28.323 +
  28.324 +        "Get filter details for any specified 'dirname'."
  28.325 +
  28.326 +        if dirname == "cancellations":
  28.327 +            return ["status"], ["cancelled"]
  28.328 +        else:
  28.329 +            return ["status"], ["active"]
  28.330 +
  28.331 +    def get_complete_event_table(self, dirname=None):
  28.332 +
  28.333 +        "Get the table providing events for any specified 'dirname'."
  28.334 +
  28.335 +        if dirname == "counters":
  28.336 +            return "countered_objects"
  28.337 +        else:
  28.338 +            return "objects"
  28.339 +
  28.340 +    def get_recurrence_table(self, dirname=None):
  28.341 +
  28.342 +        "Get the table providing recurrences for any specified 'dirname'."
  28.343 +
  28.344 +        if dirname == "counters":
  28.345 +            return "countered_recurrences"
  28.346 +        else:
  28.347 +            return "recurrences"
  28.348 +
  28.349 +    # Free/busy period providers, upon extension of the free/busy records.
  28.350 +
  28.351 +    def _get_freebusy_providers(self, user):
  28.352 +
  28.353 +        """
  28.354 +        Return the free/busy providers for the given 'user'.
  28.355 +
  28.356 +        This function returns any stored datetime and a list of providers as a
  28.357 +        2-tuple. Each provider is itself a (uid, recurrenceid) tuple.
  28.358 +        """
  28.359 +
  28.360 +        columns = ["store_user"]
  28.361 +        values = [user]
  28.362 +
  28.363 +        query, values = self.get_query(
  28.364 +            "select object_uid, object_recurrenceid from freebusy_providers :condition",
  28.365 +            columns, values)
  28.366 +
  28.367 +        self.cursor.execute(query, values)
  28.368 +        providers = self.cursor.fetchall()
  28.369 +
  28.370 +        columns = ["store_user"]
  28.371 +        values = [user]
  28.372 +
  28.373 +        query, values = self.get_query(
  28.374 +            "select start from freebusy_provider_datetimes :condition",
  28.375 +            columns, values)
  28.376 +
  28.377 +        self.cursor.execute(query, values)
  28.378 +        result = self.cursor.fetchone()
  28.379 +        dt_string = result and result[0]
  28.380 +
  28.381 +        return dt_string, providers
  28.382 +
  28.383 +    def _set_freebusy_providers(self, user, dt_string, t):
  28.384 +
  28.385 +        "Set the given provider timestamp 'dt_string' and table 't'."
  28.386 +
  28.387 +        # NOTE: Locking?
  28.388 +
  28.389 +        columns = ["store_user"]
  28.390 +        values = [user]
  28.391 +
  28.392 +        query, values = self.get_query(
  28.393 +            "delete from freebusy_providers :condition",
  28.394 +            columns, values)
  28.395 +
  28.396 +        self.cursor.execute(query, values)
  28.397 +
  28.398 +        columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.399 +
  28.400 +        for uid, recurrenceid in t:
  28.401 +            values = [user, uid, recurrenceid]
  28.402 +
  28.403 +            query, values = self.get_query(
  28.404 +                "insert into freebusy_providers (:columns) values (:values)",
  28.405 +                columns, values)
  28.406 +
  28.407 +            self.cursor.execute(query, values)
  28.408 +
  28.409 +        columns = ["store_user"]
  28.410 +        values = [user]
  28.411 +        setcolumns = ["start"]
  28.412 +        setvalues = [dt_string]
  28.413 +
  28.414 +        query, values = self.get_query(
  28.415 +            "update freebusy_provider_datetimes :set :condition",
  28.416 +            columns, values, setcolumns, setvalues)
  28.417 +
  28.418 +        self.cursor.execute(query, values)
  28.419 +
  28.420 +        if self.cursor.rowcount > 0:
  28.421 +            return True
  28.422 +
  28.423 +        columns = ["store_user", "start"]
  28.424 +        values = [user, dt_string]
  28.425 +
  28.426 +        query, values = self.get_query(
  28.427 +            "insert into freebusy_provider_datetimes (:columns) values (:values)",
  28.428 +            columns, values)
  28.429 +
  28.430 +        self.cursor.execute(query, values)
  28.431 +        return True
  28.432 +
  28.433 +    # Free/busy period access.
  28.434 +
  28.435 +    def get_freebusy(self, user, name=None, mutable=False):
  28.436 +
  28.437 +        "Get free/busy details for the given 'user'."
  28.438 +
  28.439 +        table = name or "freebusy"
  28.440 +        return FreeBusyDatabaseCollection(self.cursor, table, ["store_user"], [user], mutable, self.paramstyle)
  28.441 +
  28.442 +    def get_freebusy_for_other(self, user, other, mutable=False):
  28.443 +
  28.444 +        "For the given 'user', get free/busy details for the 'other' user."
  28.445 +
  28.446 +        table = "freebusy_other"
  28.447 +        return FreeBusyDatabaseCollection(self.cursor, table, ["store_user", "other"], [user, other], mutable, self.paramstyle)
  28.448 +
  28.449 +    def set_freebusy(self, user, freebusy, name=None):
  28.450 +
  28.451 +        "For the given 'user', set 'freebusy' details."
  28.452 +
  28.453 +        table = name or "freebusy"
  28.454 +
  28.455 +        if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table:
  28.456 +            fbc = FreeBusyDatabaseCollection(self.cursor, table, ["store_user"], [user], True, self.paramstyle)
  28.457 +            fbc += freebusy
  28.458 +
  28.459 +        return True
  28.460 +
  28.461 +    def set_freebusy_for_other(self, user, freebusy, other):
  28.462 +
  28.463 +        "For the given 'user', set 'freebusy' details for the 'other' user."
  28.464 +
  28.465 +        table = "freebusy_other"
  28.466 +
  28.467 +        if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table:
  28.468 +            fbc = FreeBusyDatabaseCollection(self.cursor, table, ["store_user", "other"], [user, other], True, self.paramstyle)
  28.469 +            fbc += freebusy
  28.470 +
  28.471 +        return True
  28.472 +
  28.473 +    def get_freebusy_others(self, user):
  28.474 +
  28.475 +        """
  28.476 +        For the given 'user', return a list of other users for whom free/busy
  28.477 +        information is retained.
  28.478 +        """
  28.479 +
  28.480 +        columns = ["store_user"]
  28.481 +        values = [user]
  28.482 +
  28.483 +        query, values = self.get_query(
  28.484 +            "select distinct other from freebusy_other :condition",
  28.485 +            columns, values)
  28.486 +
  28.487 +        self.cursor.execute(query, values)
  28.488 +        return [r[0] for r in self.cursor.fetchall()]
  28.489 +
  28.490 +    # Tentative free/busy periods related to countering.
  28.491 +
  28.492 +    def get_freebusy_offers(self, user, mutable=False):
  28.493 +
  28.494 +        "Get free/busy offers for the given 'user'."
  28.495 +
  28.496 +        # Expire old offers and save the collection if modified.
  28.497 +
  28.498 +        now = format_datetime(to_timezone(datetime.utcnow(), "UTC"))
  28.499 +        columns = ["store_user", "expires"]
  28.500 +        values = [user, now]
  28.501 +
  28.502 +        query, values = self.get_query(
  28.503 +            "delete from freebusy_offers :condition",
  28.504 +            columns, values)
  28.505 +
  28.506 +        self.cursor.execute(query, values)
  28.507 +
  28.508 +        return self.get_freebusy(user, "freebusy_offers", mutable)
  28.509 +
  28.510 +    def set_freebusy_offers(self, user, freebusy):
  28.511 +
  28.512 +        "For the given 'user', set 'freebusy' offers."
  28.513 +
  28.514 +        return self.set_freebusy(user, freebusy, "freebusy_offers")
  28.515 +
  28.516 +    # Requests and counter-proposals.
  28.517 +
  28.518 +    def get_requests(self, user):
  28.519 +
  28.520 +        "Get requests for the given 'user'."
  28.521 +
  28.522 +        columns = ["store_user"]
  28.523 +        values = [user]
  28.524 +
  28.525 +        query, values = self.get_query(
  28.526 +            "select object_uid, object_recurrenceid, request_type from requests :condition",
  28.527 +            columns, values)
  28.528 +
  28.529 +        self.cursor.execute(query, values)
  28.530 +        return self.cursor.fetchall()
  28.531 +
  28.532 +    def set_request(self, user, uid, recurrenceid=None, type=None):
  28.533 +
  28.534 +        """
  28.535 +        For the given 'user', set the queued 'uid' and 'recurrenceid',
  28.536 +        indicating a request, along with any given 'type'.
  28.537 +        """
  28.538 +
  28.539 +        columns = ["store_user", "object_uid", "object_recurrenceid", "request_type"]
  28.540 +        values = [user, uid, recurrenceid, type]
  28.541 +
  28.542 +        query, values = self.get_query(
  28.543 +            "insert into requests (:columns) values (:values)",
  28.544 +            columns, values)
  28.545 +
  28.546 +        self.cursor.execute(query, values)
  28.547 +        return True
  28.548 +
  28.549 +    def queue_request(self, user, uid, recurrenceid=None, type=None):
  28.550 +
  28.551 +        """
  28.552 +        Queue a request for 'user' having the given 'uid'. If the optional
  28.553 +        'recurrenceid' is specified, the entry refers to a specific instance
  28.554 +        or occurrence of an event. The 'type' parameter can be used to indicate
  28.555 +        a specific type of request.
  28.556 +        """
  28.557 +
  28.558 +        if recurrenceid:
  28.559 +            columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.560 +            values = [user, uid, recurrenceid]
  28.561 +        else:
  28.562 +            columns = ["store_user", "object_uid"]
  28.563 +            values = [user, uid]
  28.564 +
  28.565 +        setcolumns = ["request_type"]
  28.566 +        setvalues = [type]
  28.567 +
  28.568 +        query, values = self.get_query(
  28.569 +            "update requests :set :condition",
  28.570 +            columns, values, setcolumns, setvalues)
  28.571 +
  28.572 +        self.cursor.execute(query, values)
  28.573 +
  28.574 +        if self.cursor.rowcount > 0:
  28.575 +            return
  28.576 +
  28.577 +        self.set_request(user, uid, recurrenceid, type)
  28.578 +
  28.579 +    def dequeue_request(self, user, uid, recurrenceid=None):
  28.580 +
  28.581 +        """
  28.582 +        Dequeue all requests for 'user' having the given 'uid'. If the optional
  28.583 +        'recurrenceid' is specified, all requests for that specific instance or
  28.584 +        occurrence of an event are dequeued.
  28.585 +        """
  28.586 +
  28.587 +        if recurrenceid:
  28.588 +            columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.589 +            values = [user, uid, recurrenceid]
  28.590 +        else:
  28.591 +            columns = ["store_user", "object_uid"]
  28.592 +            values = [user, uid]
  28.593 +
  28.594 +        query, values = self.get_query(
  28.595 +            "delete from requests :condition",
  28.596 +            columns, values)
  28.597 +
  28.598 +        self.cursor.execute(query, values)
  28.599 +        return True
  28.600 +
  28.601 +    def get_counters(self, user, uid, recurrenceid=None):
  28.602 +
  28.603 +        """
  28.604 +        For the given 'user', return a list of users from whom counter-proposals
  28.605 +        have been received for the given 'uid' and optional 'recurrenceid'.
  28.606 +        """
  28.607 +
  28.608 +        table = self.get_event_table(recurrenceid, "counters")
  28.609 +
  28.610 +        if recurrenceid:
  28.611 +            columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.612 +            values = [user, uid, recurrenceid]
  28.613 +        else:
  28.614 +            columns = ["store_user", "object_uid"]
  28.615 +            values = [user, uid]
  28.616 +
  28.617 +        query, values = self.get_query(
  28.618 +            "select other from %(table)s :condition" % {
  28.619 +                "table" : table
  28.620 +                },
  28.621 +            columns, values)
  28.622 +
  28.623 +        self.cursor.execute(query, values)
  28.624 +        return [r[0] for r in self.cursor.fetchall()]
  28.625 +
  28.626 +    def get_counter(self, user, other, uid, recurrenceid=None):
  28.627 +
  28.628 +        """
  28.629 +        For the given 'user', return the counter-proposal from 'other' for the
  28.630 +        given 'uid' and optional 'recurrenceid'.
  28.631 +        """
  28.632 +
  28.633 +        table = self.get_event_table(recurrenceid, "counters")
  28.634 +
  28.635 +        if recurrenceid:
  28.636 +            columns = ["store_user", "other", "object_uid", "object_recurrenceid"]
  28.637 +            values = [user, other, uid, recurrenceid]
  28.638 +        else:
  28.639 +            columns = ["store_user", "other", "object_uid"]
  28.640 +            values = [user, other, uid]
  28.641 +
  28.642 +        query, values = self.get_query(
  28.643 +            "select object_text from %(table)s :condition" % {
  28.644 +                "table" : table
  28.645 +                },
  28.646 +            columns, values)
  28.647 +
  28.648 +        self.cursor.execute(query, values)
  28.649 +        result = self.cursor.fetchone()
  28.650 +        return result and parse_string(result[0], "utf-8")
  28.651 +
  28.652 +    def set_counter(self, user, other, node, uid, recurrenceid=None):
  28.653 +
  28.654 +        """
  28.655 +        For the given 'user', store a counter-proposal received from 'other' the
  28.656 +        given 'node' representing that proposal for the given 'uid' and
  28.657 +        'recurrenceid'.
  28.658 +        """
  28.659 +
  28.660 +        table = self.get_event_table(recurrenceid, "counters")
  28.661 +
  28.662 +        if recurrenceid:
  28.663 +            columns = ["store_user", "other", "object_uid", "object_recurrenceid", "object_text"]
  28.664 +            values = [user, other, uid, recurrenceid, to_string(node, "utf-8")]
  28.665 +        else:
  28.666 +            columns = ["store_user", "other", "object_uid", "object_text"]
  28.667 +            values = [user, other, uid, to_string(node, "utf-8")]
  28.668 +
  28.669 +        query, values = self.get_query(
  28.670 +            "insert into %(table)s (:columns) values (:values)" % {
  28.671 +                "table" : table
  28.672 +                },
  28.673 +            columns, values)
  28.674 +
  28.675 +        self.cursor.execute(query, values)
  28.676 +        return True
  28.677 +
  28.678 +    def remove_counters(self, user, uid, recurrenceid=None):
  28.679 +
  28.680 +        """
  28.681 +        For the given 'user', remove all counter-proposals associated with the
  28.682 +        given 'uid' and 'recurrenceid'.
  28.683 +        """
  28.684 +
  28.685 +        table = self.get_event_table(recurrenceid, "counters")
  28.686 +
  28.687 +        if recurrenceid:
  28.688 +            columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.689 +            values = [user, uid, recurrenceid]
  28.690 +        else:
  28.691 +            columns = ["store_user", "object_uid"]
  28.692 +            values = [user, uid]
  28.693 +
  28.694 +        query, values = self.get_query(
  28.695 +            "delete from %(table)s :condition" % {
  28.696 +                "table" : table
  28.697 +                },
  28.698 +            columns, values)
  28.699 +
  28.700 +        self.cursor.execute(query, values)
  28.701 +        return True
  28.702 +
  28.703 +    def remove_counter(self, user, other, uid, recurrenceid=None):
  28.704 +
  28.705 +        """
  28.706 +        For the given 'user', remove any counter-proposal from 'other'
  28.707 +        associated with the given 'uid' and 'recurrenceid'.
  28.708 +        """
  28.709 +
  28.710 +        table = self.get_event_table(recurrenceid, "counters")
  28.711 +
  28.712 +        if recurrenceid:
  28.713 +            columns = ["store_user", "other", "object_uid", "object_recurrenceid"]
  28.714 +            values = [user, other, uid, recurrenceid]
  28.715 +        else:
  28.716 +            columns = ["store_user", "other", "object_uid"]
  28.717 +            values = [user, other, uid]
  28.718 +
  28.719 +        query, values = self.get_query(
  28.720 +            "delete from %(table)s :condition" % {
  28.721 +                "table" : table
  28.722 +                },
  28.723 +            columns, values)
  28.724 +
  28.725 +        self.cursor.execute(query, values)
  28.726 +        return True
  28.727 +
  28.728 +    # Event cancellation.
  28.729 +
  28.730 +    def cancel_event(self, user, uid, recurrenceid=None):
  28.731 +
  28.732 +        """
  28.733 +        Cancel an event for 'user' having the given 'uid'. If the optional
  28.734 +        'recurrenceid' is specified, a specific instance or occurrence of an
  28.735 +        event is cancelled.
  28.736 +        """
  28.737 +
  28.738 +        table = self.get_event_table(recurrenceid)
  28.739 +
  28.740 +        if recurrenceid:
  28.741 +            columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.742 +            values = [user, uid, recurrenceid]
  28.743 +        else:
  28.744 +            columns = ["store_user", "object_uid"]
  28.745 +            values = [user, uid]
  28.746 +
  28.747 +        setcolumns = ["status"]
  28.748 +        setvalues = ["cancelled"]
  28.749 +
  28.750 +        query, values = self.get_query(
  28.751 +            "update %(table)s :set :condition" % {
  28.752 +                "table" : table
  28.753 +                },
  28.754 +            columns, values, setcolumns, setvalues)
  28.755 +
  28.756 +        self.cursor.execute(query, values)
  28.757 +        return True
  28.758 +
  28.759 +    def uncancel_event(self, user, uid, recurrenceid=None):
  28.760 +
  28.761 +        """
  28.762 +        Uncancel an event for 'user' having the given 'uid'. If the optional
  28.763 +        'recurrenceid' is specified, a specific instance or occurrence of an
  28.764 +        event is uncancelled.
  28.765 +        """
  28.766 +
  28.767 +        table = self.get_event_table(recurrenceid)
  28.768 +
  28.769 +        if recurrenceid:
  28.770 +            columns = ["store_user", "object_uid", "object_recurrenceid"]
  28.771 +            values = [user, uid, recurrenceid]
  28.772 +        else:
  28.773 +            columns = ["store_user", "object_uid"]
  28.774 +            values = [user, uid]
  28.775 +
  28.776 +        setcolumns = ["status"]
  28.777 +        setvalues = ["active"]
  28.778 +
  28.779 +        query, values = self.get_query(
  28.780 +            "update %(table)s :set :condition" % {
  28.781 +                "table" : table
  28.782 +                },
  28.783 +            columns, values, setcolumns, setvalues)
  28.784 +
  28.785 +        self.cursor.execute(query, values)
  28.786 +        return True
  28.787 +
  28.788 +    def remove_cancellation(self, user, uid, recurrenceid=None):
  28.789 +
  28.790 +        """
  28.791 +        Remove a cancellation for 'user' for the event having the given 'uid'.
  28.792 +        If the optional 'recurrenceid' is specified, a specific instance or
  28.793 +        occurrence of an event is affected.
  28.794 +        """
  28.795 +
  28.796 +        table = self.get_event_table(recurrenceid)
  28.797 +
  28.798 +        if recurrenceid:
  28.799 +            columns = ["store_user", "object_uid", "object_recurrenceid", "status"]
  28.800 +            values = [user, uid, recurrenceid, "cancelled"]
  28.801 +        else:
  28.802 +            columns = ["store_user", "object_uid", "status"]
  28.803 +            values = [user, uid, "cancelled"]
  28.804 +
  28.805 +        query, values = self.get_query(
  28.806 +            "delete from %(table)s :condition" % {
  28.807 +                "table" : table
  28.808 +                },
  28.809 +            columns, values)
  28.810 +
  28.811 +        self.cursor.execute(query, values)
  28.812 +        return True
  28.813 +
  28.814 +class DatabaseJournal(DatabaseStoreBase, JournalBase):
  28.815 +
  28.816 +    "A journal system to support quotas."
  28.817 +
  28.818 +    # Quota and user identity/group discovery.
  28.819 +
  28.820 +    def get_quotas(self):
  28.821 +
  28.822 +        "Return a list of quotas."
  28.823 +
  28.824 +        query = "select distinct quota from (" \
  28.825 +                "select quota from quota_freebusy " \
  28.826 +                "union all select quota from quota_limits" \
  28.827 +                ") as quotas"
  28.828 +        self.cursor.execute(query)
  28.829 +        return [r[0] for r in self.cursor.fetchall()]
  28.830 +
  28.831 +    def get_quota_users(self, quota):
  28.832 +
  28.833 +        "Return a list of quota users."
  28.834 +
  28.835 +        columns = ["quota"]
  28.836 +        values = [quota]
  28.837 +
  28.838 +        query, values = self.get_query(
  28.839 +            "select distinct user_group from quota_freebusy :condition",
  28.840 +            columns, values)
  28.841 +
  28.842 +        self.cursor.execute(query, values)
  28.843 +        return [r[0] for r in self.cursor.fetchall()]
  28.844 +
  28.845 +    # Groups of users sharing quotas.
  28.846 +
  28.847 +    def get_groups(self, quota):
  28.848 +
  28.849 +        "Return the identity mappings for the given 'quota' as a dictionary."
  28.850 +
  28.851 +        columns = ["quota"]
  28.852 +        values = [quota]
  28.853 +
  28.854 +        query, values = self.get_query(
  28.855 +            "select store_user, user_group from user_groups :condition",
  28.856 +            columns, values)
  28.857 +
  28.858 +        self.cursor.execute(query, values)
  28.859 +        return dict(self.cursor.fetchall())
  28.860 +
  28.861 +    def set_group(self, quota, store_user, user_group):
  28.862 +
  28.863 +        """
  28.864 +        For the given 'quota', set a mapping from 'store_user' to 'user_group'.
  28.865 +        """
  28.866 +
  28.867 +        columns = ["quota", "store_user"]
  28.868 +        values = [quota, store_user]
  28.869 +        setcolumns = ["user_group"]
  28.870 +        setvalues = [user_group]
  28.871 +
  28.872 +        query, values = self.get_query(
  28.873 +            "update user_groups :set :condition",
  28.874 +            columns, values, setcolumns, setvalues)
  28.875 +
  28.876 +        self.cursor.execute(query, values)
  28.877 +
  28.878 +        if self.cursor.rowcount > 0:
  28.879 +            return True
  28.880 +
  28.881 +        columns = ["quota", "store_user", "user_group"]
  28.882 +        values = [quota, store_user, user_group]
  28.883 +
  28.884 +        query, values = self.get_query(
  28.885 +            "insert into user_groups (:columns) values (:values)",
  28.886 +            columns, values)
  28.887 +
  28.888 +        self.cursor.execute(query, values)
  28.889 +        return True
  28.890 +
  28.891 +    def get_limits(self, quota):
  28.892 +
  28.893 +        """
  28.894 +        Return the limits for the 'quota' as a dictionary mapping identities or
  28.895 +        groups to durations.
  28.896 +        """
  28.897 +
  28.898 +        columns = ["quota"]
  28.899 +        values = [quota]
  28.900 +
  28.901 +        query, values = self.get_query(
  28.902 +            "select user_group, quota_limit from quota_limits :condition",
  28.903 +            columns, values)
  28.904 +
  28.905 +        self.cursor.execute(query, values)
  28.906 +        return dict(self.cursor.fetchall())
  28.907 +
  28.908 +    def set_limit(self, quota, group, limit):
  28.909 +
  28.910 +        """
  28.911 +        For the given 'quota', set for a user 'group' the given 'limit' on
  28.912 +        resource usage.
  28.913 +        """
  28.914 +
  28.915 +        columns = ["quota", "user_group"]
  28.916 +        values = [quota, group]
  28.917 +        setcolumns = ["quota_limit"]
  28.918 +        setvalues = [limit]
  28.919 +
  28.920 +        query, values = self.get_query(
  28.921 +            "update quota_limits :set :condition",
  28.922 +            columns, values, setcolumns, setvalues)
  28.923 +
  28.924 +        self.cursor.execute(query, values)
  28.925 +
  28.926 +        if self.cursor.rowcount > 0:
  28.927 +            return True
  28.928 +
  28.929 +        columns = ["quota", "user_group", "quota_limit"]
  28.930 +        values = [quota, group, limit]
  28.931 +
  28.932 +        query, values = self.get_query(
  28.933 +            "insert into quota_limits (:columns) values (:values)",
  28.934 +            columns, values)
  28.935 +
  28.936 +        self.cursor.execute(query, values)
  28.937 +        return True
  28.938 +
  28.939 +    # Free/busy period access for users within quota groups.
  28.940 +
  28.941 +    def get_freebusy_users(self, quota):
  28.942 +
  28.943 +        """
  28.944 +        Return a list of users whose free/busy details are retained for the
  28.945 +        given 'quota'.
  28.946 +        """
  28.947 +
  28.948 +        columns = ["quota"]
  28.949 +        values = [quota]
  28.950 +
  28.951 +        query, values = self.get_query(
  28.952 +            "select distinct store_user from user_freebusy :condition",
  28.953 +            columns, values)
  28.954 +
  28.955 +        self.cursor.execute(query, values)
  28.956 +        return [r[0] for r in self.cursor.fetchall()]
  28.957 +
  28.958 +    def get_freebusy(self, quota, user, mutable=False):
  28.959 +
  28.960 +        "Get free/busy details for the given 'quota' and 'user'."
  28.961 +
  28.962 +        table = "user_freebusy"
  28.963 +        return FreeBusyDatabaseCollection(self.cursor, table, ["quota", "store_user"], [quota, user], mutable, self.paramstyle)
  28.964 +
  28.965 +    def set_freebusy(self, quota, user, freebusy):
  28.966 +
  28.967 +        "For the given 'quota' and 'user', set 'freebusy' details."
  28.968 +
  28.969 +        table = "user_freebusy"
  28.970 +
  28.971 +        if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table:
  28.972 +            fbc = FreeBusyDatabaseCollection(self.cursor, table, ["quota", "store_user"], [quota, user], True, self.paramstyle)
  28.973 +            fbc += freebusy
  28.974 +
  28.975 +        return True
  28.976 +
  28.977 +    # Journal entry methods.
  28.978 +
  28.979 +    def get_entries(self, quota, group, mutable=False):
  28.980 +
  28.981 +        """
  28.982 +        Return a list of journal entries for the given 'quota' for the indicated
  28.983 +        'group'.
  28.984 +        """
  28.985 +
  28.986 +        table = "quota_freebusy"
  28.987 +        return FreeBusyDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], mutable, self.paramstyle)
  28.988 +
  28.989 +    def set_entries(self, quota, group, entries):
  28.990 +
  28.991 +        """
  28.992 +        For the given 'quota' and indicated 'group', set the list of journal
  28.993 +        'entries'.
  28.994 +        """
  28.995 +
  28.996 +        table = "quota_freebusy"
  28.997 +
  28.998 +        if not isinstance(entries, FreeBusyDatabaseCollection) or entries.table_name != table:
  28.999 +            fbc = FreeBusyDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], True, self.paramstyle)
 28.1000 +            fbc += entries
 28.1001 +
 28.1002 +        return True
 28.1003 +
 28.1004 +# vim: tabstop=4 expandtab shiftwidth=4
    29.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    29.2 +++ b/imiptools/stores/database/postgresql.py	Fri Apr 22 16:22:58 2016 +0200
    29.3 @@ -0,0 +1,66 @@
    29.4 +#!/usr/bin/env python
    29.5 +
    29.6 +"""
    29.7 +A PostgreSQL database store of calendar data.
    29.8 +
    29.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
   29.10 +
   29.11 +This program is free software; you can redistribute it and/or modify it under
   29.12 +the terms of the GNU General Public License as published by the Free Software
   29.13 +Foundation; either version 3 of the License, or (at your option) any later
   29.14 +version.
   29.15 +
   29.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   29.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   29.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   29.19 +details.
   29.20 +
   29.21 +You should have received a copy of the GNU General Public License along with
   29.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   29.23 +"""
   29.24 +
   29.25 +from imiptools.config import STORE_DIR, JOURNAL_DIR
   29.26 +from imiptools.stores.database.common import DatabaseStore, DatabaseJournal
   29.27 +import psycopg2
   29.28 +
   29.29 +class Store(DatabaseStore):
   29.30 +
   29.31 +    "A PostgreSQL database store of calendar objects and free/busy data."
   29.32 +
   29.33 +    def __init__(self, store_dir=None):
   29.34 +
   29.35 +        "Interpret 'store_dir' as a connection string."
   29.36 +
   29.37 +        connection = psycopg2.connect(store_dir or STORE_DIR)
   29.38 +        connection.autocommit = True
   29.39 +        DatabaseStore.__init__(self, connection, psycopg2.paramstyle)
   29.40 +
   29.41 +    def acquire_lock(self, user, timeout=None):
   29.42 +        query = "select pg_advisory_lock(20160311)"
   29.43 +        self.cursor.execute(query)
   29.44 +
   29.45 +    def release_lock(self, user):
   29.46 +        query = "select pg_advisory_unlock(20160311)"
   29.47 +        self.cursor.execute(query)
   29.48 +
   29.49 +class Journal(DatabaseJournal):
   29.50 +
   29.51 +    "A PostgreSQL journal system supporting quotas."
   29.52 +
   29.53 +    def __init__(self, store_dir=None):
   29.54 +
   29.55 +        "Interpret 'store_dir' as a connection string."
   29.56 +
   29.57 +        connection = psycopg2.connect(store_dir or JOURNAL_DIR)
   29.58 +        connection.autocommit = True
   29.59 +        DatabaseJournal.__init__(self, connection, psycopg2.paramstyle)
   29.60 +
   29.61 +    def acquire_lock(self, user, timeout=None):
   29.62 +        query = "select pg_advisory_lock(20160312)"
   29.63 +        self.cursor.execute(query)
   29.64 +
   29.65 +    def release_lock(self, user):
   29.66 +        query = "select pg_advisory_unlock(20160312)"
   29.67 +        self.cursor.execute(query)
   29.68 +
   29.69 +# vim: tabstop=4 expandtab shiftwidth=4
    30.1 --- a/imiptools/stores/file.py	Tue Apr 19 21:20:57 2016 +0200
    30.2 +++ b/imiptools/stores/file.py	Fri Apr 22 16:22:58 2016 +0200
    30.3 @@ -19,14 +19,14 @@
    30.4  this program.  If not, see <http://www.gnu.org/licenses/>.
    30.5  """
    30.6  
    30.7 -from imiptools.stores import StoreBase, PublisherBase, JournalBase
    30.8 +from imiptools.stores.common import StoreBase, PublisherBase, JournalBase
    30.9  
   30.10  from datetime import datetime
   30.11  from imiptools.config import STORE_DIR, PUBLISH_DIR, JOURNAL_DIR
   30.12  from imiptools.data import make_calendar, parse_object, to_stream
   30.13  from imiptools.dates import format_datetime, get_datetime, to_timezone
   30.14  from imiptools.filesys import fix_permissions, FileBase
   30.15 -from imiptools.period import FreeBusyPeriod
   30.16 +from imiptools.period import FreeBusyPeriod, FreeBusyCollection
   30.17  from imiptools.text import parse_line
   30.18  from os.path import isdir, isfile, join
   30.19  from os import listdir, remove, rmdir
   30.20 @@ -148,7 +148,7 @@
   30.21          finally:
   30.22              self.release_lock(user)
   30.23  
   30.24 -class FileStore(FileStoreBase, StoreBase):
   30.25 +class Store(FileStoreBase, StoreBase):
   30.26  
   30.27      "A file store of tabular free/busy data and objects."
   30.28  
   30.29 @@ -232,25 +232,19 @@
   30.30  
   30.31          filename = self.get_object_in_store(user, "objects")
   30.32          if not filename or not isdir(filename):
   30.33 -            return None
   30.34 +            return []
   30.35  
   30.36          return [name for name in listdir(filename) if isfile(join(filename, name))]
   30.37  
   30.38 -    def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
   30.39 +    def get_cancelled_events(self, user):
   30.40  
   30.41 -        """
   30.42 -        Get the filename providing the event for the given 'user' with the given
   30.43 -        'uid'. If the optional 'recurrenceid' is specified, a specific instance
   30.44 -        or occurrence of an event is returned.
   30.45 +        "Return a list of event identifiers for cancelled events."
   30.46  
   30.47 -        Where 'dirname' is specified, the given directory name is used as the
   30.48 -        base of the location within which any filename will reside.
   30.49 -        """
   30.50 +        filename = self.get_object_in_store(user, "cancellations", "objects")
   30.51 +        if not filename or not isdir(filename):
   30.52 +            return []
   30.53  
   30.54 -        if recurrenceid:
   30.55 -            return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
   30.56 -        else:
   30.57 -            return self.get_complete_event_filename(user, uid, dirname, username)
   30.58 +        return [name for name in listdir(filename) if isfile(join(filename, name))]
   30.59  
   30.60      def get_event(self, user, uid, recurrenceid=None, dirname=None):
   30.61  
   30.62 @@ -266,21 +260,6 @@
   30.63  
   30.64          return filename and self._get_object(user, filename)
   30.65  
   30.66 -    def get_complete_event_filename(self, user, uid, dirname=None, username=None):
   30.67 -
   30.68 -        """
   30.69 -        Get the filename providing the event for the given 'user' with the given
   30.70 -        'uid'. 
   30.71 -
   30.72 -        Where 'dirname' is specified, the given directory name is used as the
   30.73 -        base of the location within which any filename will reside.
   30.74 -
   30.75 -        Where 'username' is specified, the event details will reside in a file
   30.76 -        bearing that name within a directory having 'uid' as its name.
   30.77 -        """
   30.78 -
   30.79 -        return self.get_object_in_store(user, dirname, "objects", uid, username)
   30.80 -
   30.81      def get_complete_event(self, user, uid):
   30.82  
   30.83          "Get the event for the given 'user' with the given 'uid'."
   30.84 @@ -346,21 +325,6 @@
   30.85  
   30.86          return [name for name in listdir(filename) if isfile(join(filename, name))]
   30.87  
   30.88 -    def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
   30.89 -
   30.90 -        """
   30.91 -        For the event of the given 'user' with the given 'uid', return the
   30.92 -        filename providing the recurrence with the given 'recurrenceid'.
   30.93 -
   30.94 -        Where 'dirname' is specified, the given directory name is used as the
   30.95 -        base of the location within which any filename will reside.
   30.96 -
   30.97 -        Where 'username' is specified, the event details will reside in a file
   30.98 -        bearing that name within a directory having 'uid' as its name.
   30.99 -        """
  30.100 -
  30.101 -        return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
  30.102 -
  30.103      def get_recurrence(self, user, uid, recurrenceid):
  30.104  
  30.105          """
  30.106 @@ -410,6 +374,54 @@
  30.107  
  30.108          return True
  30.109  
  30.110 +    # Event filename computation.
  30.111 +
  30.112 +    def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None):
  30.113 +
  30.114 +        """
  30.115 +        Get the filename providing the event for the given 'user' with the given
  30.116 +        'uid'. If the optional 'recurrenceid' is specified, a specific instance
  30.117 +        or occurrence of an event is returned.
  30.118 +
  30.119 +        Where 'dirname' is specified, the given directory name is used as the
  30.120 +        base of the location within which any filename will reside.
  30.121 +        """
  30.122 +
  30.123 +        if recurrenceid:
  30.124 +            return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username)
  30.125 +        else:
  30.126 +            return self.get_complete_event_filename(user, uid, dirname, username)
  30.127 +
  30.128 +    def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None):
  30.129 +
  30.130 +        """
  30.131 +        For the event of the given 'user' with the given 'uid', return the
  30.132 +        filename providing the recurrence with the given 'recurrenceid'.
  30.133 +
  30.134 +        Where 'dirname' is specified, the given directory name is used as the
  30.135 +        base of the location within which any filename will reside.
  30.136 +
  30.137 +        Where 'username' is specified, the event details will reside in a file
  30.138 +        bearing that name within a directory having 'uid' as its name.
  30.139 +        """
  30.140 +
  30.141 +        return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username)
  30.142 +
  30.143 +    def get_complete_event_filename(self, user, uid, dirname=None, username=None):
  30.144 +
  30.145 +        """
  30.146 +        Get the filename providing the event for the given 'user' with the given
  30.147 +        'uid'. 
  30.148 +
  30.149 +        Where 'dirname' is specified, the given directory name is used as the
  30.150 +        base of the location within which any filename will reside.
  30.151 +
  30.152 +        Where 'username' is specified, the event details will reside in a file
  30.153 +        bearing that name within a directory having 'uid' as its name.
  30.154 +        """
  30.155 +
  30.156 +        return self.get_object_in_store(user, dirname, "objects", uid, username)
  30.157 +
  30.158      # Free/busy period providers, upon extension of the free/busy records.
  30.159  
  30.160      def _get_freebusy_providers(self, user):
  30.161 @@ -450,28 +462,34 @@
  30.162  
  30.163      # Free/busy period access.
  30.164  
  30.165 -    def get_freebusy(self, user, name=None):
  30.166 +    def get_freebusy(self, user, name=None, mutable=False):
  30.167  
  30.168          "Get free/busy details for the given 'user'."
  30.169  
  30.170          filename = self.get_object_in_store(user, name or "freebusy")
  30.171 +
  30.172          if not filename or not isfile(filename):
  30.173 -            return []
  30.174 +            periods = []
  30.175          else:
  30.176 -            return map(lambda t: FreeBusyPeriod(*t),
  30.177 +            periods = map(lambda t: FreeBusyPeriod(*t),
  30.178                  self._get_table_atomic(user, filename))
  30.179  
  30.180 -    def get_freebusy_for_other(self, user, other):
  30.181 +        return FreeBusyCollection(periods, mutable)
  30.182 +
  30.183 +    def get_freebusy_for_other(self, user, other, mutable=False):
  30.184  
  30.185          "For the given 'user', get free/busy details for the 'other' user."
  30.186  
  30.187          filename = self.get_object_in_store(user, "freebusy-other", other)
  30.188 +
  30.189          if not filename or not isfile(filename):
  30.190 -            return []
  30.191 +            periods = []
  30.192          else:
  30.193 -            return map(lambda t: FreeBusyPeriod(*t),
  30.194 +            periods = map(lambda t: FreeBusyPeriod(*t),
  30.195                  self._get_table_atomic(user, filename))
  30.196  
  30.197 +        return FreeBusyCollection(periods, mutable)
  30.198 +
  30.199      def set_freebusy(self, user, freebusy, name=None):
  30.200  
  30.201          "For the given 'user', set 'freebusy' details."
  30.202 @@ -481,7 +499,7 @@
  30.203              return False
  30.204  
  30.205          self._set_table_atomic(user, filename,
  30.206 -            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
  30.207 +            map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy)))
  30.208          return True
  30.209  
  30.210      def set_freebusy_for_other(self, user, freebusy, other):
  30.211 @@ -493,12 +511,26 @@
  30.212              return False
  30.213  
  30.214          self._set_table_atomic(user, filename,
  30.215 -            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
  30.216 +            map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy)))
  30.217          return True
  30.218  
  30.219 +    def get_freebusy_others(self, user):
  30.220 +
  30.221 +        """
  30.222 +        For the given 'user', return a list of other users for whom free/busy
  30.223 +        information is retained.
  30.224 +        """
  30.225 +
  30.226 +        filename = self.get_object_in_store(user, "freebusy-other")
  30.227 +
  30.228 +        if not filename or not isdir(filename):
  30.229 +            return []
  30.230 +
  30.231 +        return listdir(filename)
  30.232 +
  30.233      # Tentative free/busy periods related to countering.
  30.234  
  30.235 -    def get_freebusy_offers(self, user):
  30.236 +    def get_freebusy_offers(self, user, mutable=False):
  30.237  
  30.238          "Get free/busy offers for the given 'user'."
  30.239  
  30.240 @@ -522,7 +554,7 @@
  30.241          finally:
  30.242              self.release_lock(user)
  30.243  
  30.244 -        return offers
  30.245 +        return FreeBusyCollection(offers, mutable)
  30.246  
  30.247      # Requests and counter-proposals.
  30.248  
  30.249 @@ -532,7 +564,7 @@
  30.250  
  30.251          filename = self.get_object_in_store(user, queue)
  30.252          if not filename or not isfile(filename):
  30.253 -            return None
  30.254 +            return []
  30.255  
  30.256          return self._get_table_atomic(user, filename, [(1, None), (2, None)])
  30.257  
  30.258 @@ -603,7 +635,7 @@
  30.259  
  30.260          filename = self.get_event_filename(user, uid, recurrenceid, "counters")
  30.261          if not filename or not isdir(filename):
  30.262 -            return False
  30.263 +            return []
  30.264  
  30.265          return [name for name in listdir(filename) if isfile(join(filename, name))]
  30.266  
  30.267 @@ -615,8 +647,8 @@
  30.268          """
  30.269  
  30.270          filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
  30.271 -        if not filename:
  30.272 -            return False
  30.273 +        if not filename or not isfile(filename):
  30.274 +            return None
  30.275  
  30.276          return self._get_object(user, filename)
  30.277  
  30.278 @@ -718,7 +750,7 @@
  30.279  
  30.280          return False
  30.281  
  30.282 -class FilePublisher(FileBase, PublisherBase):
  30.283 +class Publisher(FileBase, PublisherBase):
  30.284  
  30.285      "A publisher of objects."
  30.286  
  30.287 @@ -754,7 +786,7 @@
  30.288  
  30.289          return True
  30.290  
  30.291 -class FileJournal(FileStoreBase, JournalBase):
  30.292 +class Journal(FileStoreBase, JournalBase):
  30.293  
  30.294      "A journal system to support quotas."
  30.295  
  30.296 @@ -791,6 +823,22 @@
  30.297  
  30.298          return dict(self._get_table_atomic(quota, filename, tab_separated=False))
  30.299  
  30.300 +    def set_group(self, quota, store_user, user_group):
  30.301 +
  30.302 +        """
  30.303 +        For the given 'quota', set a mapping from 'store_user' to 'user_group'.
  30.304 +        """
  30.305 +
  30.306 +        filename = self.get_object_in_store(quota, "groups")
  30.307 +        if not filename:
  30.308 +            return False
  30.309 +
  30.310 +        groups = self.get_groups(quota) or {}
  30.311 +        groups[store_user] = user_group
  30.312 +
  30.313 +        self._set_table_atomic(quota, filename, groups.items())
  30.314 +        return True
  30.315 +
  30.316      def get_limits(self, quota):
  30.317  
  30.318          """
  30.319 @@ -800,22 +848,55 @@
  30.320  
  30.321          filename = self.get_object_in_store(quota, "limits")
  30.322          if not filename or not isfile(filename):
  30.323 -            return None
  30.324 +            return {}
  30.325  
  30.326          return dict(self._get_table_atomic(quota, filename, tab_separated=False))
  30.327  
  30.328 +    def set_limit(self, quota, group, limit):
  30.329 +
  30.330 +        """
  30.331 +        For the given 'quota', set for a user 'group' the given 'limit' on
  30.332 +        resource usage.
  30.333 +        """
  30.334 +
  30.335 +        filename = self.get_object_in_store(quota, "limits")
  30.336 +        if not filename:
  30.337 +            return False
  30.338 +
  30.339 +        limits = self.get_limits(quota) or {}
  30.340 +        limits[group] = limit
  30.341 +
  30.342 +        self._set_table_atomic(quota, filename, limits.items())
  30.343 +        return True
  30.344 +
  30.345      # Free/busy period access for users within quota groups.
  30.346  
  30.347 -    def get_freebusy(self, quota, user):
  30.348 +    def get_freebusy_users(self, quota):
  30.349 +
  30.350 +        """
  30.351 +        Return a list of users whose free/busy details are retained for the
  30.352 +        given 'quota'.
  30.353 +        """
  30.354 +
  30.355 +        filename = self.get_object_in_store(quota, "freebusy")
  30.356 +        if not filename or not isdir(filename):
  30.357 +            return []
  30.358 +
  30.359 +        return listdir(filename)
  30.360 +
  30.361 +    def get_freebusy(self, quota, user, mutable=False):
  30.362  
  30.363          "Get free/busy details for the given 'quota' and 'user'."
  30.364  
  30.365          filename = self.get_object_in_store(quota, "freebusy", user)
  30.366 -        if not filename or not isfile(filename):
  30.367 -            return []
  30.368  
  30.369 -        return map(lambda t: FreeBusyPeriod(*t),
  30.370 -            self._get_table_atomic(quota, filename))
  30.371 +        if not filename or not isfile(filename):
  30.372 +            periods = []
  30.373 +        else:
  30.374 +            periods = map(lambda t: FreeBusyPeriod(*t),
  30.375 +                self._get_table_atomic(quota, filename))
  30.376 +
  30.377 +        return FreeBusyCollection(periods, mutable)
  30.378  
  30.379      def set_freebusy(self, quota, user, freebusy):
  30.380  
  30.381 @@ -826,12 +907,12 @@
  30.382              return False
  30.383  
  30.384          self._set_table_atomic(quota, filename,
  30.385 -            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
  30.386 +            map(lambda fb: fb.as_tuple(strings_only=True), list(freebusy)))
  30.387          return True
  30.388  
  30.389      # Journal entry methods.
  30.390  
  30.391 -    def get_entries(self, quota, group):
  30.392 +    def get_entries(self, quota, group, mutable=False):
  30.393  
  30.394          """
  30.395          Return a list of journal entries for the given 'quota' for the indicated
  30.396 @@ -839,11 +920,14 @@
  30.397          """
  30.398  
  30.399          filename = self.get_object_in_store(quota, "journal", group)
  30.400 -        if not filename or not isfile(filename):
  30.401 -            return []
  30.402  
  30.403 -        return map(lambda t: FreeBusyPeriod(*t),
  30.404 -            self._get_table_atomic(quota, filename))
  30.405 +        if not filename or not isfile(filename):
  30.406 +            periods = []
  30.407 +        else:
  30.408 +            periods = map(lambda t: FreeBusyPeriod(*t),
  30.409 +                self._get_table_atomic(quota, filename))
  30.410 +
  30.411 +        return FreeBusyCollection(periods, mutable)
  30.412  
  30.413      def set_entries(self, quota, group, entries):
  30.414  
  30.415 @@ -857,7 +941,7 @@
  30.416              return False
  30.417  
  30.418          self._set_table_atomic(quota, filename,
  30.419 -            map(lambda fb: fb.as_tuple(strings_only=True), entries))
  30.420 +            map(lambda fb: fb.as_tuple(strings_only=True), list(entries)))
  30.421          return True
  30.422  
  30.423  # vim: tabstop=4 expandtab shiftwidth=4
    31.1 --- a/imipweb/calendar.py	Tue Apr 19 21:20:57 2016 +0200
    31.2 +++ b/imipweb/calendar.py	Fri Apr 22 16:22:58 2016 +0200
    31.3 @@ -3,7 +3,7 @@
    31.4  """
    31.5  A Web interface to an event calendar.
    31.6  
    31.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
    31.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    31.9  
   31.10  This program is free software; you can redistribute it and/or modify it under
   31.11  the terms of the GNU General Public License as published by the Free Software
   31.12 @@ -26,7 +26,6 @@
   31.13                              get_start_of_next_day, get_timestamp, ends_on_same_day, \
   31.14                              to_date, to_timezone
   31.15  from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
   31.16 -                             get_overlapping, \
   31.17                               get_scale, get_slots, get_spans, partition_by_day, \
   31.18                               remove_end_slot, Period, Point
   31.19  from imipweb.resource import FormUtilities, ResourceClient
   31.20 @@ -323,8 +322,8 @@
   31.21          view_end = view_period.get_end()
   31.22          duration = view_period.get_duration()
   31.23  
   31.24 -        preceding_events = view_start and get_overlapping(freebusy, Period(None, view_start, self.get_tzid())) or []
   31.25 -        following_events = view_end and get_overlapping(freebusy, Period(view_end, None, self.get_tzid())) or []
   31.26 +        preceding_events = view_start and freebusy.get_overlapping(Period(None, view_start, self.get_tzid())) or []
   31.27 +        following_events = view_end and freebusy.get_overlapping(Period(view_end, None, self.get_tzid())) or []
   31.28  
   31.29          last_preceding = preceding_events and to_date(preceding_events[-1].get_end()) + timedelta(1) or None
   31.30          first_following = following_events and to_date(following_events[0].get_start()) or None
   31.31 @@ -487,7 +486,7 @@
   31.32              # Filter periods outside the given view.
   31.33  
   31.34              if view_period:
   31.35 -                periods = get_overlapping(periods, view_period)
   31.36 +                periods = periods.get_overlapping(view_period)
   31.37  
   31.38              # Get the time scale with start and end points.
   31.39  
    32.1 --- a/imipweb/event.py	Tue Apr 19 21:20:57 2016 +0200
    32.2 +++ b/imipweb/event.py	Fri Apr 22 16:22:58 2016 +0200
    32.3 @@ -3,7 +3,7 @@
    32.4  """
    32.5  A Web interface to a calendar event.
    32.6  
    32.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
    32.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
    32.9  
   32.10  This program is free software; you can redistribute it and/or modify it under
   32.11  the terms of the GNU General Public License as published by the Free Software
   32.12 @@ -23,7 +23,6 @@
   32.13                             uri_parts, uri_values
   32.14  from imiptools.dates import format_datetime, to_timezone
   32.15  from imiptools.mail import Messenger
   32.16 -from imiptools.period import have_conflict
   32.17  from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError
   32.18  from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject
   32.19  
   32.20 @@ -713,7 +712,7 @@
   32.21              partstat = participant_attr and participant_attr.get("PARTSTAT")
   32.22              recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid)
   32.23  
   32.24 -            for p in have_conflict(freebusy, periods, True):
   32.25 +            for p in freebusy.have_conflict(periods, True):
   32.26                  if not self.recurrenceid and p.is_replaced(recurrences):
   32.27                      continue
   32.28  
    33.1 --- a/imipweb/resource.py	Tue Apr 19 21:20:57 2016 +0200
    33.2 +++ b/imipweb/resource.py	Fri Apr 22 16:22:58 2016 +0200
    33.3 @@ -23,6 +23,7 @@
    33.4  from imiptools.client import Client, ClientForObject
    33.5  from imiptools.data import get_uri
    33.6  from imiptools.dates import format_datetime, to_date
    33.7 +from imiptools.period import FreeBusyCollection
    33.8  from imipweb.data import event_period_from_period, form_period_from_period, \
    33.9                           FormDate, PeriodError
   33.10  from imipweb.env import CGIEnvironment
   33.11 @@ -151,7 +152,7 @@
   33.12  
   33.13          "Return a list of periods comprising the request summary."
   33.14  
   33.15 -        summary = []
   33.16 +        summary = FreeBusyCollection()
   33.17  
   33.18          for uid, recurrenceid, request_type in self._get_requests():
   33.19  
    34.1 --- a/messages/da_DK.imip-agent.po	Tue Apr 19 21:20:57 2016 +0200
    34.2 +++ b/messages/da_DK.imip-agent.po	Fri Apr 22 16:22:58 2016 +0200
    34.3 @@ -6,8 +6,8 @@
    34.4  msgid ""
    34.5  msgstr ""
    34.6  "Project-Id-Version: 71b35ab1f0bd+\n"
    34.7 -"Report-Msgid-Bugs-To: Jonas Smedegaard <dr@jones.dk>\n"
    34.8 -"POT-Creation-Date: 2015-11-05 23:53+0100\n"
    34.9 +"Report-Msgid-Bugs-To: \n"
   34.10 +"POT-Creation-Date: 2016-04-18 21:06+0200\n"
   34.11  "PO-Revision-Date: 2016-04-08 09:19+0200\n"
   34.12  "Last-Translator: Jonas Smedegaard <dr@jones.dk>\n"
   34.13  "Language-Team: Danish\n"
   34.14 @@ -16,7 +16,7 @@
   34.15  "Content-Type: text/plain; charset=UTF-8\n"
   34.16  "Content-Transfer-Encoding: 8bit\n"
   34.17  
   34.18 -#: imiptools/handlers/__init__.py:77
   34.19 +#: imiptools/handlers/__init__.py:78
   34.20  #, python-format
   34.21  msgid ""
   34.22  "If your mail program cannot handle this message, you may view the details "
   34.23 @@ -173,272 +173,272 @@
   34.24  msgid "Do not participate"
   34.25  msgstr "Deltag ikke"
   34.26  
   34.27 -#: imipweb/calendar.py:133
   34.28 +#: imipweb/calendar.py:132
   34.29  #, python-format
   34.30  msgid "New event at %s"
   34.31  msgstr "Ny begivenhed ved %s"
   34.32  
   34.33 -#: imipweb/calendar.py:231
   34.34 +#: imipweb/calendar.py:230
   34.35  msgid "Pending requests:"
   34.36  msgstr "Afventende forespørgsler:"
   34.37  
   34.38 -#: imipweb/calendar.py:253
   34.39 +#: imipweb/calendar.py:252
   34.40  msgid "There are no pending requests."
   34.41  msgstr "Der er ingen afventende forespørgsler."
   34.42  
   34.43 -#: imipweb/calendar.py:270
   34.44 +#: imipweb/calendar.py:269
   34.45  msgid "Participants for scheduling:"
   34.46  msgstr "Deltagere til gruppeplanlægning:"
   34.47  
   34.48 -#: imipweb/calendar.py:275 imipweb/event.py:426 imipweb/event.py:525
   34.49 +#: imipweb/calendar.py:274 imipweb/event.py:425 imipweb/event.py:524
   34.50  msgid "Remove"
   34.51  msgstr "Fjern"
   34.52  
   34.53 -#: imipweb/calendar.py:280
   34.54 +#: imipweb/calendar.py:279
   34.55  msgid "Add"
   34.56  msgstr "Tilføj"
   34.57  
   34.58 -#: imipweb/calendar.py:304
   34.59 +#: imipweb/calendar.py:303
   34.60  msgid "Select days or periods for a new event."
   34.61  msgstr "Vælg dage eller perioder til en ny begivenhed."
   34.62  
   34.63 -#: imipweb/calendar.py:305
   34.64 +#: imipweb/calendar.py:304
   34.65  msgid "Hide busy time periods"
   34.66  msgstr "Skjul optagede perioder"
   34.67  
   34.68 -#: imipweb/calendar.py:306
   34.69 +#: imipweb/calendar.py:305
   34.70  msgid "Show busy time periods"
   34.71  msgstr "Vis optagede perioder"
   34.72  
   34.73 -#: imipweb/calendar.py:307
   34.74 +#: imipweb/calendar.py:306
   34.75  msgid "Show empty days"
   34.76  msgstr "Vis ledige dage"
   34.77  
   34.78 -#: imipweb/calendar.py:308
   34.79 +#: imipweb/calendar.py:307
   34.80  msgid "Hide empty days"
   34.81  msgstr "Skjul ledige dage"
   34.82  
   34.83 -#: imipweb/calendar.py:309 imipweb/calendar.py:846
   34.84 +#: imipweb/calendar.py:308 imipweb/calendar.py:845
   34.85  msgid "Clear selections"
   34.86  msgstr "Nulstil valgte perioder"
   34.87  
   34.88 -#: imipweb/calendar.py:339
   34.89 +#: imipweb/calendar.py:338
   34.90  msgid "Show earlier events"
   34.91  msgstr "Vis tidligere begivenheder"
   34.92  
   34.93 -#: imipweb/calendar.py:345
   34.94 +#: imipweb/calendar.py:344
   34.95  msgid "Show earlier"
   34.96  msgstr "Vis tidligere"
   34.97  
   34.98 -#: imipweb/calendar.py:354
   34.99 +#: imipweb/calendar.py:353
  34.100  msgid "Show later"
  34.101  msgstr "Vis senere"
  34.102  
  34.103 -#: imipweb/calendar.py:361
  34.104 +#: imipweb/calendar.py:360
  34.105  msgid "Show later events"
  34.106  msgstr "Vis senere begivenhder"
  34.107  
  34.108 -#: imipweb/calendar.py:440
  34.109 +#: imipweb/calendar.py:439
  34.110  #, python-format
  34.111  msgid "Showing events from %(start)s until %(end)s"
  34.112  msgstr "Viser begivenheder fra %(start)s frem til %(end)s"
  34.113  
  34.114 -#: imipweb/calendar.py:444
  34.115 +#: imipweb/calendar.py:443
  34.116  #, python-format
  34.117  msgid "Showing events from %s"
  34.118  msgstr "Viser begivenheder fra %s"
  34.119  
  34.120 -#: imipweb/calendar.py:446
  34.121 +#: imipweb/calendar.py:445
  34.122  #, python-format
  34.123  msgid "Showing events until %s"
  34.124  msgstr "Viser begivenheder frem til %s"
  34.125  
  34.126 -#: imipweb/calendar.py:470
  34.127 +#: imipweb/calendar.py:469
  34.128  msgid "Pending requests"
  34.129  msgstr "Forespørgsler som venter"
  34.130  
  34.131 -#: imipweb/calendar.py:470
  34.132 +#: imipweb/calendar.py:469
  34.133  msgid "Your schedule"
  34.134  msgstr "Din tidsplan"
  34.135  
  34.136 -#: imipweb/calendar.py:589
  34.137 +#: imipweb/calendar.py:588
  34.138  msgid "Calendar"
  34.139  msgstr "Kalender"
  34.140  
  34.141 -#: imipweb/calendar.py:840
  34.142 +#: imipweb/calendar.py:839
  34.143  msgid "Summary:"
  34.144  msgstr "Opsummering:"
  34.145  
  34.146 -#: imipweb/calendar.py:842
  34.147 +#: imipweb/calendar.py:841
  34.148  msgid "New event"
  34.149  msgstr "Ny begivenhed"
  34.150  
  34.151 -#: imipweb/calendar.py:993
  34.152 +#: imipweb/calendar.py:992
  34.153  msgid "(Participant is busy)"
  34.154  msgstr "(Deltager er optaget)"
  34.155  
  34.156 -#: imipweb/calendar.py:1076
  34.157 +#: imipweb/calendar.py:1075
  34.158  msgid "Select/deselect period"
  34.159  msgstr "Vælg/nulstil periode"
  34.160  
  34.161 -#: imipweb/event.py:44
  34.162 +#: imipweb/event.py:43
  34.163  msgid "Summary"
  34.164  msgstr "Opsummering"
  34.165  
  34.166 -#: imipweb/event.py:45 imipweb/event.py:629 imipweb/event.py:742
  34.167 +#: imipweb/event.py:44 imipweb/event.py:628 imipweb/event.py:741
  34.168  msgid "Start"
  34.169  msgstr "Start"
  34.170  
  34.171 -#: imipweb/event.py:46 imipweb/event.py:630 imipweb/event.py:743
  34.172 +#: imipweb/event.py:45 imipweb/event.py:629 imipweb/event.py:742
  34.173  msgid "End"
  34.174  msgstr "Slut"
  34.175  
  34.176 -#: imipweb/event.py:47
  34.177 +#: imipweb/event.py:46
  34.178  msgid "Organiser"
  34.179  msgstr "Arrangør"
  34.180  
  34.181 -#: imipweb/event.py:48 imipweb/event.py:595
  34.182 +#: imipweb/event.py:47 imipweb/event.py:594
  34.183  msgid "Attendee"
  34.184  msgstr "Deltager"
  34.185  
  34.186 -#: imipweb/event.py:52
  34.187 +#: imipweb/event.py:51
  34.188  msgid "Not confirmed"
  34.189  msgstr "Ikke bekræftet"
  34.190  
  34.191 -#: imipweb/event.py:53
  34.192 +#: imipweb/event.py:52
  34.193  msgid "Attending"
  34.194  msgstr "Deltager"
  34.195  
  34.196 -#: imipweb/event.py:54
  34.197 +#: imipweb/event.py:53
  34.198  msgid "Tentatively attending"
  34.199  msgstr "Deltager måske"
  34.200  
  34.201 -#: imipweb/event.py:55
  34.202 +#: imipweb/event.py:54
  34.203  msgid "Not attending"
  34.204  msgstr "Deltager ikke"
  34.205  
  34.206 -#: imipweb/event.py:56
  34.207 +#: imipweb/event.py:55
  34.208  msgid "Delegated"
  34.209  msgstr "Er delegeret til en anden"
  34.210  
  34.211 -#: imipweb/event.py:57
  34.212 +#: imipweb/event.py:56
  34.213  msgid "Not indicated"
  34.214  msgstr "Ikke angivet"
  34.215  
  34.216 -#: imipweb/event.py:154
  34.217 +#: imipweb/event.py:153
  34.218  msgid "This event has not been shared."
  34.219  msgstr "Denne begivenhed er ikke blevet delt med andre."
  34.220  
  34.221 -#: imipweb/event.py:159
  34.222 +#: imipweb/event.py:158
  34.223  msgid "An action is required for this request:"
  34.224  msgstr "Der skal tages et valg for denne forespørgsel:"
  34.225  
  34.226 -#: imipweb/event.py:162
  34.227 +#: imipweb/event.py:161
  34.228  msgid "Send reply"
  34.229  msgstr "Send svar"
  34.230  
  34.231 -#: imipweb/event.py:164 imipweb/event.py:183
  34.232 +#: imipweb/event.py:163 imipweb/event.py:182
  34.233  msgid "Discard event"
  34.234  msgstr "Drop begivenheden"
  34.235  
  34.236 -#: imipweb/event.py:166 imipweb/event.py:186
  34.237 +#: imipweb/event.py:165 imipweb/event.py:185
  34.238  msgid "Return to the calendar"
  34.239  msgstr "Tilbage til kalenderen"
  34.240  
  34.241 -#: imipweb/event.py:170
  34.242 +#: imipweb/event.py:169
  34.243  msgid "As organiser, you can perform the following:"
  34.244  msgstr "Som arrangør kan du gøre følgende:"
  34.245  
  34.246 -#: imipweb/event.py:173
  34.247 +#: imipweb/event.py:172
  34.248  msgid "Update event"
  34.249  msgstr "Opdatér begivenheden"
  34.250  
  34.251 -#: imipweb/event.py:177
  34.252 +#: imipweb/event.py:176
  34.253  msgid "Ignore counter-proposals"
  34.254  msgstr "Ignorér modforslag"
  34.255  
  34.256 -#: imipweb/event.py:181
  34.257 +#: imipweb/event.py:180
  34.258  msgid "Cancel event"
  34.259  msgstr "Aflys begivenheden"
  34.260  
  34.261 -#: imipweb/event.py:188
  34.262 +#: imipweb/event.py:187
  34.263  msgid "Save without sending"
  34.264  msgstr "Gem uden at sende"
  34.265  
  34.266 -#: imipweb/event.py:227 imipweb/event.py:741 imipweb/event.py:1290
  34.267 +#: imipweb/event.py:226 imipweb/event.py:740 imipweb/event.py:1289
  34.268  msgid "Event"
  34.269  msgstr "Begivenhed"
  34.270  
  34.271 -#: imipweb/event.py:274
  34.272 +#: imipweb/event.py:273
  34.273  msgid "First occurrence replaced by a separate event"
  34.274  msgstr "Den første periode er erstattet af en anden begivenhed"
  34.275  
  34.276 -#: imipweb/event.py:283
  34.277 +#: imipweb/event.py:282
  34.278  msgid "First occurrence excluded"
  34.279  msgstr "Den første periode blev fjernet"
  34.280  
  34.281 -#: imipweb/event.py:294
  34.282 +#: imipweb/event.py:293
  34.283  msgid "Add a recurrence"
  34.284  msgstr "Tilføj en ny periode"
  34.285  
  34.286 -#: imipweb/event.py:335
  34.287 +#: imipweb/event.py:334
  34.288  msgid "Add attendee"
  34.289  msgstr "Tilføj en deltager"
  34.290  
  34.291 -#: imipweb/event.py:428
  34.292 +#: imipweb/event.py:427
  34.293  msgid "(Uninvited)"
  34.294  msgstr "(Ikke inviteret)"
  34.295  
  34.296 -#: imipweb/event.py:429
  34.297 +#: imipweb/event.py:428
  34.298  msgid "Re-invite"
  34.299  msgstr "Invitér påny"
  34.300  
  34.301 -#: imipweb/event.py:453
  34.302 +#: imipweb/event.py:452
  34.303  msgid "This event modifies a recurring event."
  34.304  msgstr "Denne begivenhed ændrer en periode i en begivenhed som gentager sig."
  34.305  
  34.306 -#: imipweb/event.py:463
  34.307 +#: imipweb/event.py:462
  34.308  #, python-format
  34.309  msgid "This event occurs on the following occasions within the next %d days:"
  34.310  msgstr "Denne begivenhed forekommer på følgende anledninger de næste %d dage:"
  34.311  
  34.312 -#: imipweb/event.py:494
  34.313 +#: imipweb/event.py:493
  34.314  msgid "Occurrence"
  34.315  msgstr "Forekomst"
  34.316  
  34.317 -#: imipweb/event.py:494
  34.318 +#: imipweb/event.py:493
  34.319  msgid "Occurrence from rule"
  34.320  msgstr "Forekomst fra regel"
  34.321  
  34.322 -#: imipweb/event.py:527
  34.323 +#: imipweb/event.py:526
  34.324  msgid "(Removed)"
  34.325  msgstr "(Fjernet)"
  34.326  
  34.327 -#: imipweb/event.py:528
  34.328 +#: imipweb/event.py:527
  34.329  msgid "Re-add"
  34.330  msgstr "Tilføj påny"
  34.331  
  34.332 -#: imipweb/event.py:590
  34.333 +#: imipweb/event.py:589
  34.334  msgid "The following attendees have been suggested for this event:"
  34.335  msgstr "Følgende deltagere er blevet foreslået for denne begivenhed:"
  34.336  
  34.337 -#: imipweb/event.py:596 imipweb/event.py:626
  34.338 +#: imipweb/event.py:595 imipweb/event.py:625
  34.339  msgid "Suggested by..."
  34.340  msgstr "Foreslået af..."
  34.341  
  34.342 -#: imipweb/event.py:620
  34.343 +#: imipweb/event.py:619
  34.344  msgid "The following periods have been suggested for this event:"
  34.345  msgstr "Følgende perioder er blevet foreslået for denne begivenhed:"
  34.346  
  34.347 -#: imipweb/event.py:625
  34.348 +#: imipweb/event.py:624
  34.349  msgid "Periods"
  34.350  msgstr "Perioder"
  34.351  
  34.352 -#: imipweb/event.py:736
  34.353 +#: imipweb/event.py:735
  34.354  msgid "This event conflicts with others:"
  34.355  msgstr "Begivenheden er i konflikt med andre i tidsplanen:"
  34.356  
  34.357 -#: imipweb/event.py:763
  34.358 +#: imipweb/event.py:762
  34.359  msgid "(Unspecified event)"
  34.360  msgstr "(Ikke angivet begivenhed)"
  34.361  
    35.1 --- a/messages/en_GB.imip-agent.po	Tue Apr 19 21:20:57 2016 +0200
    35.2 +++ b/messages/en_GB.imip-agent.po	Fri Apr 22 16:22:58 2016 +0200
    35.3 @@ -167,272 +167,272 @@
    35.4  msgid "Do not participate"
    35.5  msgstr ""
    35.6  
    35.7 -#: imipweb/calendar.py:133
    35.8 +#: imipweb/calendar.py:132
    35.9  #, python-format
   35.10  msgid "New event at %s"
   35.11  msgstr ""
   35.12  
   35.13 -#: imipweb/calendar.py:231
   35.14 +#: imipweb/calendar.py:230
   35.15  msgid "Pending requests:"
   35.16  msgstr ""
   35.17  
   35.18 -#: imipweb/calendar.py:253
   35.19 +#: imipweb/calendar.py:252
   35.20  msgid "There are no pending requests."
   35.21  msgstr ""
   35.22  
   35.23 -#: imipweb/calendar.py:270
   35.24 +#: imipweb/calendar.py:269
   35.25  msgid "Participants for scheduling:"
   35.26  msgstr ""
   35.27  
   35.28 -#: imipweb/calendar.py:275 imipweb/event.py:426 imipweb/event.py:525
   35.29 +#: imipweb/calendar.py:274 imipweb/event.py:425 imipweb/event.py:524
   35.30  msgid "Remove"
   35.31  msgstr ""
   35.32  
   35.33 -#: imipweb/calendar.py:280
   35.34 +#: imipweb/calendar.py:279
   35.35  msgid "Add"
   35.36  msgstr ""
   35.37  
   35.38 +#: imipweb/calendar.py:303
   35.39 +msgid "Select days or periods for a new event."
   35.40 +msgstr ""
   35.41 +
   35.42  #: imipweb/calendar.py:304
   35.43 -msgid "Select days or periods for a new event."
   35.44 +msgid "Hide busy time periods"
   35.45  msgstr ""
   35.46  
   35.47  #: imipweb/calendar.py:305
   35.48 -msgid "Hide busy time periods"
   35.49 +msgid "Show busy time periods"
   35.50  msgstr ""
   35.51  
   35.52  #: imipweb/calendar.py:306
   35.53 -msgid "Show busy time periods"
   35.54 +msgid "Show empty days"
   35.55  msgstr ""
   35.56  
   35.57  #: imipweb/calendar.py:307
   35.58 -msgid "Show empty days"
   35.59 -msgstr ""
   35.60 -
   35.61 -#: imipweb/calendar.py:308
   35.62  msgid "Hide empty days"
   35.63  msgstr ""
   35.64  
   35.65 -#: imipweb/calendar.py:309 imipweb/calendar.py:846
   35.66 +#: imipweb/calendar.py:308 imipweb/calendar.py:845
   35.67  msgid "Clear selections"
   35.68  msgstr ""
   35.69  
   35.70 -#: imipweb/calendar.py:339
   35.71 +#: imipweb/calendar.py:338
   35.72  msgid "Show earlier events"
   35.73  msgstr ""
   35.74  
   35.75 -#: imipweb/calendar.py:345
   35.76 +#: imipweb/calendar.py:344
   35.77  msgid "Show earlier"
   35.78  msgstr ""
   35.79  
   35.80 -#: imipweb/calendar.py:354
   35.81 +#: imipweb/calendar.py:353
   35.82  msgid "Show later"
   35.83  msgstr ""
   35.84  
   35.85 -#: imipweb/calendar.py:361
   35.86 +#: imipweb/calendar.py:360
   35.87  msgid "Show later events"
   35.88  msgstr ""
   35.89  
   35.90 -#: imipweb/calendar.py:440
   35.91 +#: imipweb/calendar.py:439
   35.92  #, python-format
   35.93  msgid "Showing events from %(start)s until %(end)s"
   35.94  msgstr ""
   35.95  
   35.96 -#: imipweb/calendar.py:444
   35.97 +#: imipweb/calendar.py:443
   35.98  #, python-format
   35.99  msgid "Showing events from %s"
  35.100  msgstr ""
  35.101  
  35.102 -#: imipweb/calendar.py:446
  35.103 +#: imipweb/calendar.py:445
  35.104  #, python-format
  35.105  msgid "Showing events until %s"
  35.106  msgstr ""
  35.107  
  35.108 -#: imipweb/calendar.py:470
  35.109 +#: imipweb/calendar.py:469
  35.110  msgid "Pending requests"
  35.111  msgstr ""
  35.112  
  35.113 -#: imipweb/calendar.py:470
  35.114 +#: imipweb/calendar.py:469
  35.115  msgid "Your schedule"
  35.116  msgstr ""
  35.117  
  35.118 -#: imipweb/calendar.py:589
  35.119 +#: imipweb/calendar.py:588
  35.120  msgid "Calendar"
  35.121  msgstr ""
  35.122  
  35.123 -#: imipweb/calendar.py:840
  35.124 +#: imipweb/calendar.py:839
  35.125  msgid "Summary:"
  35.126  msgstr ""
  35.127  
  35.128 -#: imipweb/calendar.py:842
  35.129 +#: imipweb/calendar.py:841
  35.130  msgid "New event"
  35.131  msgstr ""
  35.132  
  35.133 -#: imipweb/calendar.py:993
  35.134 +#: imipweb/calendar.py:992
  35.135  msgid "(Participant is busy)"
  35.136  msgstr ""
  35.137  
  35.138 -#: imipweb/calendar.py:1076
  35.139 +#: imipweb/calendar.py:1075
  35.140  msgid "Select/deselect period"
  35.141  msgstr ""
  35.142  
  35.143 -#: imipweb/event.py:44
  35.144 +#: imipweb/event.py:43
  35.145  msgid "Summary"
  35.146  msgstr ""
  35.147  
  35.148 +#: imipweb/event.py:44 imipweb/event.py:628 imipweb/event.py:741
  35.149 +msgid "Start"
  35.150 +msgstr ""
  35.151 +
  35.152  #: imipweb/event.py:45 imipweb/event.py:629 imipweb/event.py:742
  35.153 -msgid "Start"
  35.154 -msgstr ""
  35.155 -
  35.156 -#: imipweb/event.py:46 imipweb/event.py:630 imipweb/event.py:743
  35.157  msgid "End"
  35.158  msgstr ""
  35.159  
  35.160 -#: imipweb/event.py:47
  35.161 +#: imipweb/event.py:46
  35.162  msgid "Organiser"
  35.163  msgstr ""
  35.164  
  35.165 -#: imipweb/event.py:48 imipweb/event.py:595
  35.166 +#: imipweb/event.py:47 imipweb/event.py:594
  35.167  msgid "Attendee"
  35.168  msgstr ""
  35.169  
  35.170 +#: imipweb/event.py:51
  35.171 +msgid "Not confirmed"
  35.172 +msgstr ""
  35.173 +
  35.174  #: imipweb/event.py:52
  35.175 -msgid "Not confirmed"
  35.176 +msgid "Attending"
  35.177  msgstr ""
  35.178  
  35.179  #: imipweb/event.py:53
  35.180 -msgid "Attending"
  35.181 +msgid "Tentatively attending"
  35.182  msgstr ""
  35.183  
  35.184  #: imipweb/event.py:54
  35.185 -msgid "Tentatively attending"
  35.186 +msgid "Not attending"
  35.187  msgstr ""
  35.188  
  35.189  #: imipweb/event.py:55
  35.190 -msgid "Not attending"
  35.191 +msgid "Delegated"
  35.192  msgstr ""
  35.193  
  35.194  #: imipweb/event.py:56
  35.195 -msgid "Delegated"
  35.196 -msgstr ""
  35.197 -
  35.198 -#: imipweb/event.py:57
  35.199  msgid "Not indicated"
  35.200  msgstr ""
  35.201  
  35.202 -#: imipweb/event.py:154
  35.203 +#: imipweb/event.py:153
  35.204  msgid "This event has not been shared."
  35.205  msgstr ""
  35.206  
  35.207 -#: imipweb/event.py:159
  35.208 +#: imipweb/event.py:158
  35.209  msgid "An action is required for this request:"
  35.210  msgstr ""
  35.211  
  35.212 -#: imipweb/event.py:162
  35.213 +#: imipweb/event.py:161
  35.214  msgid "Send reply"
  35.215  msgstr ""
  35.216  
  35.217 -#: imipweb/event.py:164 imipweb/event.py:183
  35.218 +#: imipweb/event.py:163 imipweb/event.py:182
  35.219  msgid "Discard event"
  35.220  msgstr ""
  35.221  
  35.222 -#: imipweb/event.py:166 imipweb/event.py:186
  35.223 +#: imipweb/event.py:165 imipweb/event.py:185
  35.224  msgid "Return to the calendar"
  35.225  msgstr ""
  35.226  
  35.227 -#: imipweb/event.py:170
  35.228 +#: imipweb/event.py:169
  35.229  msgid "As organiser, you can perform the following:"
  35.230  msgstr ""
  35.231  
  35.232 -#: imipweb/event.py:173
  35.233 +#: imipweb/event.py:172
  35.234  msgid "Update event"
  35.235  msgstr ""
  35.236  
  35.237 -#: imipweb/event.py:177
  35.238 +#: imipweb/event.py:176
  35.239  msgid "Ignore counter-proposals"
  35.240  msgstr ""
  35.241  
  35.242 -#: imipweb/event.py:181
  35.243 +#: imipweb/event.py:180
  35.244  msgid "Cancel event"
  35.245  msgstr ""
  35.246  
  35.247 -#: imipweb/event.py:188
  35.248 +#: imipweb/event.py:187
  35.249  msgid "Save without sending"
  35.250  msgstr ""
  35.251  
  35.252 -#: imipweb/event.py:227 imipweb/event.py:741 imipweb/event.py:1290
  35.253 +#: imipweb/event.py:226 imipweb/event.py:740 imipweb/event.py:1289
  35.254  msgid "Event"
  35.255  msgstr ""
  35.256  
  35.257 -#: imipweb/event.py:274
  35.258 +#: imipweb/event.py:273
  35.259  msgid "First occurrence replaced by a separate event"
  35.260  msgstr ""
  35.261  
  35.262 -#: imipweb/event.py:283
  35.263 +#: imipweb/event.py:282
  35.264  msgid "First occurrence excluded"
  35.265  msgstr ""
  35.266  
  35.267 -#: imipweb/event.py:294
  35.268 +#: imipweb/event.py:293
  35.269  msgid "Add a recurrence"
  35.270  msgstr ""
  35.271  
  35.272 -#: imipweb/event.py:335
  35.273 +#: imipweb/event.py:334
  35.274  msgid "Add attendee"
  35.275  msgstr ""
  35.276  
  35.277 +#: imipweb/event.py:427
  35.278 +msgid "(Uninvited)"
  35.279 +msgstr ""
  35.280 +
  35.281  #: imipweb/event.py:428
  35.282 -msgid "(Uninvited)"
  35.283 -msgstr ""
  35.284 -
  35.285 -#: imipweb/event.py:429
  35.286  msgid "Re-invite"
  35.287  msgstr ""
  35.288  
  35.289 -#: imipweb/event.py:453
  35.290 +#: imipweb/event.py:452
  35.291  msgid "This event modifies a recurring event."
  35.292  msgstr ""
  35.293  
  35.294 -#: imipweb/event.py:463
  35.295 +#: imipweb/event.py:462
  35.296  #, python-format
  35.297  msgid "This event occurs on the following occasions within the next %d days:"
  35.298  msgstr ""
  35.299  
  35.300 -#: imipweb/event.py:494
  35.301 +#: imipweb/event.py:493
  35.302  msgid "Occurrence"
  35.303  msgstr ""
  35.304  
  35.305 -#: imipweb/event.py:494
  35.306 +#: imipweb/event.py:493
  35.307  msgid "Occurrence from rule"
  35.308  msgstr ""
  35.309  
  35.310 +#: imipweb/event.py:526
  35.311 +msgid "(Removed)"
  35.312 +msgstr ""
  35.313 +
  35.314  #: imipweb/event.py:527
  35.315 -msgid "(Removed)"
  35.316 -msgstr ""
  35.317 -
  35.318 -#: imipweb/event.py:528
  35.319  msgid "Re-add"
  35.320  msgstr ""
  35.321  
  35.322 -#: imipweb/event.py:590
  35.323 +#: imipweb/event.py:589
  35.324  msgid "The following attendees have been suggested for this event:"
  35.325  msgstr ""
  35.326  
  35.327 -#: imipweb/event.py:596 imipweb/event.py:626
  35.328 +#: imipweb/event.py:595 imipweb/event.py:625
  35.329  msgid "Suggested by..."
  35.330  msgstr ""
  35.331  
  35.332 -#: imipweb/event.py:620
  35.333 +#: imipweb/event.py:619
  35.334  msgid "The following periods have been suggested for this event:"
  35.335  msgstr ""
  35.336  
  35.337 -#: imipweb/event.py:625
  35.338 +#: imipweb/event.py:624
  35.339  msgid "Periods"
  35.340  msgstr ""
  35.341  
  35.342 -#: imipweb/event.py:736
  35.343 +#: imipweb/event.py:735
  35.344  msgid "This event conflicts with others:"
  35.345  msgstr ""
  35.346  
  35.347 -#: imipweb/event.py:763
  35.348 +#: imipweb/event.py:762
  35.349  msgid "(Unspecified event)"
  35.350  msgstr ""
  35.351  
    36.1 --- a/messages/nb_NO.imip-agent.po	Tue Apr 19 21:20:57 2016 +0200
    36.2 +++ b/messages/nb_NO.imip-agent.po	Fri Apr 22 16:22:58 2016 +0200
    36.3 @@ -174,272 +174,272 @@
    36.4  msgid "Do not participate"
    36.5  msgstr "Ikke delta"
    36.6  
    36.7 -#: imipweb/calendar.py:133
    36.8 +#: imipweb/calendar.py:132
    36.9  #, python-format
   36.10  msgid "New event at %s"
   36.11  msgstr "Ny hendelse på %s"
   36.12  
   36.13 -#: imipweb/calendar.py:231
   36.14 +#: imipweb/calendar.py:230
   36.15  msgid "Pending requests:"
   36.16  msgstr "Forespørsler som venter"
   36.17  
   36.18 -#: imipweb/calendar.py:253
   36.19 +#: imipweb/calendar.py:252
   36.20  msgid "There are no pending requests."
   36.21  msgstr "Ingen forespørsler"
   36.22  
   36.23 -#: imipweb/calendar.py:270
   36.24 +#: imipweb/calendar.py:269
   36.25  msgid "Participants for scheduling:"
   36.26  msgstr "Deltakere for gruppeplanlegging:"
   36.27  
   36.28 -#: imipweb/calendar.py:275 imipweb/event.py:426 imipweb/event.py:525
   36.29 +#: imipweb/calendar.py:274 imipweb/event.py:425 imipweb/event.py:524
   36.30  msgid "Remove"
   36.31  msgstr "Fjerne"
   36.32  
   36.33 -#: imipweb/calendar.py:280
   36.34 +#: imipweb/calendar.py:279
   36.35  msgid "Add"
   36.36  msgstr "Legge til"
   36.37  
   36.38 -#: imipweb/calendar.py:304
   36.39 +#: imipweb/calendar.py:303
   36.40  msgid "Select days or periods for a new event."
   36.41  msgstr "Velg dager eller perioder for en ny hendelse."
   36.42  
   36.43 -#: imipweb/calendar.py:305
   36.44 +#: imipweb/calendar.py:304
   36.45  msgid "Hide busy time periods"
   36.46  msgstr "Skjule opptatte perioder"
   36.47  
   36.48 -#: imipweb/calendar.py:306
   36.49 +#: imipweb/calendar.py:305
   36.50  msgid "Show busy time periods"
   36.51  msgstr "Vise opptatte perioder"
   36.52  
   36.53 -#: imipweb/calendar.py:307
   36.54 +#: imipweb/calendar.py:306
   36.55  msgid "Show empty days"
   36.56  msgstr "Vise ledige dager"
   36.57  
   36.58 -#: imipweb/calendar.py:308
   36.59 +#: imipweb/calendar.py:307
   36.60  msgid "Hide empty days"
   36.61  msgstr "Skjule ledige dager"
   36.62  
   36.63 -#: imipweb/calendar.py:309 imipweb/calendar.py:846
   36.64 +#: imipweb/calendar.py:308 imipweb/calendar.py:845
   36.65  msgid "Clear selections"
   36.66  msgstr "Nullstille valgte perioder"
   36.67  
   36.68 -#: imipweb/calendar.py:339
   36.69 +#: imipweb/calendar.py:338
   36.70  msgid "Show earlier events"
   36.71  msgstr "Tidligere hendelser"
   36.72  
   36.73 -#: imipweb/calendar.py:345
   36.74 +#: imipweb/calendar.py:344
   36.75  msgid "Show earlier"
   36.76  msgstr "Tidligere"
   36.77  
   36.78 -#: imipweb/calendar.py:354
   36.79 +#: imipweb/calendar.py:353
   36.80  msgid "Show later"
   36.81  msgstr "Senere"
   36.82  
   36.83 -#: imipweb/calendar.py:361
   36.84 +#: imipweb/calendar.py:360
   36.85  msgid "Show later events"
   36.86  msgstr "Senere hendelser"
   36.87  
   36.88 -#: imipweb/calendar.py:440
   36.89 +#: imipweb/calendar.py:439
   36.90  #, python-format
   36.91  msgid "Showing events from %(start)s until %(end)s"
   36.92  msgstr "Viser hendelser fra %(start)s frem til %(end)s"
   36.93  
   36.94 -#: imipweb/calendar.py:444
   36.95 +#: imipweb/calendar.py:443
   36.96  #, python-format
   36.97  msgid "Showing events from %s"
   36.98  msgstr "Viser hendelser fra %s"
   36.99  
  36.100 -#: imipweb/calendar.py:446
  36.101 +#: imipweb/calendar.py:445
  36.102  #, python-format
  36.103  msgid "Showing events until %s"
  36.104  msgstr "Viser hendelser frem til %s"
  36.105  
  36.106 -#: imipweb/calendar.py:470
  36.107 +#: imipweb/calendar.py:469
  36.108  msgid "Pending requests"
  36.109  msgstr "Forespørsler som venter"
  36.110  
  36.111 -#: imipweb/calendar.py:470
  36.112 +#: imipweb/calendar.py:469
  36.113  msgid "Your schedule"
  36.114  msgstr "Din tidsplan"
  36.115  
  36.116 -#: imipweb/calendar.py:589
  36.117 +#: imipweb/calendar.py:588
  36.118  msgid "Calendar"
  36.119  msgstr "Kalender"
  36.120  
  36.121 -#: imipweb/calendar.py:840
  36.122 +#: imipweb/calendar.py:839
  36.123  msgid "Summary:"
  36.124  msgstr "Sammendrag:"
  36.125  
  36.126 -#: imipweb/calendar.py:842
  36.127 +#: imipweb/calendar.py:841
  36.128  msgid "New event"
  36.129  msgstr "Ny hendelse"
  36.130  
  36.131 -#: imipweb/calendar.py:993
  36.132 +#: imipweb/calendar.py:992
  36.133  msgid "(Participant is busy)"
  36.134  msgstr "(Deltaker er opptatt)"
  36.135  
  36.136 -#: imipweb/calendar.py:1076
  36.137 +#: imipweb/calendar.py:1075
  36.138  msgid "Select/deselect period"
  36.139  msgstr "Velge/nullstille periode"
  36.140  
  36.141 -#: imipweb/event.py:44
  36.142 +#: imipweb/event.py:43
  36.143  msgid "Summary"
  36.144  msgstr "Sammendrag"
  36.145  
  36.146 -#: imipweb/event.py:45 imipweb/event.py:629 imipweb/event.py:742
  36.147 +#: imipweb/event.py:44 imipweb/event.py:628 imipweb/event.py:741
  36.148  msgid "Start"
  36.149  msgstr "Start"
  36.150  
  36.151 -#: imipweb/event.py:46 imipweb/event.py:630 imipweb/event.py:743
  36.152 +#: imipweb/event.py:45 imipweb/event.py:629 imipweb/event.py:742
  36.153  msgid "End"
  36.154  msgstr "Slutt"
  36.155  
  36.156 -#: imipweb/event.py:47
  36.157 +#: imipweb/event.py:46
  36.158  msgid "Organiser"
  36.159  msgstr "Arrangør"
  36.160  
  36.161 -#: imipweb/event.py:48 imipweb/event.py:595
  36.162 +#: imipweb/event.py:47 imipweb/event.py:594
  36.163  msgid "Attendee"
  36.164  msgstr "Deltaker"
  36.165  
  36.166 -#: imipweb/event.py:52
  36.167 +#: imipweb/event.py:51
  36.168  msgid "Not confirmed"
  36.169  msgstr "Ikke bekreftet"
  36.170  
  36.171 -#: imipweb/event.py:53
  36.172 +#: imipweb/event.py:52
  36.173  msgid "Attending"
  36.174  msgstr "Deltar"
  36.175  
  36.176 -#: imipweb/event.py:54
  36.177 +#: imipweb/event.py:53
  36.178  msgid "Tentatively attending"
  36.179  msgstr "Delta kanskje"
  36.180  
  36.181 -#: imipweb/event.py:55
  36.182 +#: imipweb/event.py:54
  36.183  msgid "Not attending"
  36.184  msgstr "Deltar ikke"
  36.185  
  36.186 -#: imipweb/event.py:56
  36.187 +#: imipweb/event.py:55
  36.188  msgid "Delegated"
  36.189  msgstr "Har delegert til en annen"
  36.190  
  36.191 -#: imipweb/event.py:57
  36.192 +#: imipweb/event.py:56
  36.193  msgid "Not indicated"
  36.194  msgstr "Ikke angitt"
  36.195  
  36.196 -#: imipweb/event.py:154
  36.197 +#: imipweb/event.py:153
  36.198  msgid "This event has not been shared."
  36.199  msgstr "Denne hendelsen er ikke blitt delt med andre."
  36.200  
  36.201 -#: imipweb/event.py:159
  36.202 +#: imipweb/event.py:158
  36.203  msgid "An action is required for this request:"
  36.204  msgstr "Et valg må tas med denne forespørselen:"
  36.205  
  36.206 -#: imipweb/event.py:162
  36.207 +#: imipweb/event.py:161
  36.208  msgid "Send reply"
  36.209  msgstr "Send svar"
  36.210  
  36.211 -#: imipweb/event.py:164 imipweb/event.py:183
  36.212 +#: imipweb/event.py:163 imipweb/event.py:182
  36.213  msgid "Discard event"
  36.214  msgstr "Kaste hendelsen"
  36.215  
  36.216 -#: imipweb/event.py:166 imipweb/event.py:186
  36.217 +#: imipweb/event.py:165 imipweb/event.py:185
  36.218  msgid "Return to the calendar"
  36.219  msgstr "Tilbake til kalenderen"
  36.220  
  36.221 -#: imipweb/event.py:170
  36.222 +#: imipweb/event.py:169
  36.223  msgid "As organiser, you can perform the following:"
  36.224  msgstr "Som arrangør kan du utføre følgende:"
  36.225  
  36.226 -#: imipweb/event.py:173
  36.227 +#: imipweb/event.py:172
  36.228  msgid "Update event"
  36.229  msgstr "Oppdatere hendelsen"
  36.230  
  36.231 -#: imipweb/event.py:177
  36.232 +#: imipweb/event.py:176
  36.233  msgid "Ignore counter-proposals"
  36.234  msgstr "Overse motforslag"
  36.235  
  36.236 -#: imipweb/event.py:181
  36.237 +#: imipweb/event.py:180
  36.238  msgid "Cancel event"
  36.239  msgstr "Send avbud for hendelsen"
  36.240  
  36.241 -#: imipweb/event.py:188
  36.242 +#: imipweb/event.py:187
  36.243  msgid "Save without sending"
  36.244  msgstr "Lagre uten å sende"
  36.245  
  36.246 -#: imipweb/event.py:227 imipweb/event.py:741 imipweb/event.py:1290
  36.247 +#: imipweb/event.py:226 imipweb/event.py:740 imipweb/event.py:1289
  36.248  msgid "Event"
  36.249  msgstr "Hendelse"
  36.250  
  36.251 -#: imipweb/event.py:274
  36.252 +#: imipweb/event.py:273
  36.253  msgid "First occurrence replaced by a separate event"
  36.254  msgstr "Den første perioden er erstattet med en annen hendelse"
  36.255  
  36.256 -#: imipweb/event.py:283
  36.257 +#: imipweb/event.py:282
  36.258  msgid "First occurrence excluded"
  36.259  msgstr "Den første perioden ble fjernet"
  36.260  
  36.261 -#: imipweb/event.py:294
  36.262 +#: imipweb/event.py:293
  36.263  msgid "Add a recurrence"
  36.264  msgstr "Legge til en ny periode"
  36.265  
  36.266 -#: imipweb/event.py:335
  36.267 +#: imipweb/event.py:334
  36.268  msgid "Add attendee"
  36.269  msgstr "Legge til en deltaker"
  36.270  
  36.271 -#: imipweb/event.py:428
  36.272 +#: imipweb/event.py:427
  36.273  msgid "(Uninvited)"
  36.274  msgstr "(Ikke invitert)"
  36.275  
  36.276 -#: imipweb/event.py:429
  36.277 +#: imipweb/event.py:428
  36.278  msgid "Re-invite"
  36.279  msgstr "Invitere på nytt"
  36.280  
  36.281 -#: imipweb/event.py:453
  36.282 +#: imipweb/event.py:452
  36.283  msgid "This event modifies a recurring event."
  36.284  msgstr "Denne hendelsen endrer en period i en hendelse som gjentar seg."
  36.285  
  36.286 -#: imipweb/event.py:463
  36.287 +#: imipweb/event.py:462
  36.288  #, python-format
  36.289  msgid "This event occurs on the following occasions within the next %d days:"
  36.290  msgstr "Denne hendelsen forekommer på følgende anledninger innen %d dager:"
  36.291  
  36.292 -#: imipweb/event.py:494
  36.293 +#: imipweb/event.py:493
  36.294  msgid "Occurrence"
  36.295  msgstr "Forekomst"
  36.296  
  36.297 -#: imipweb/event.py:494
  36.298 +#: imipweb/event.py:493
  36.299  msgid "Occurrence from rule"
  36.300  msgstr "Forekomst fra regel"
  36.301  
  36.302 -#: imipweb/event.py:527
  36.303 +#: imipweb/event.py:526
  36.304  msgid "(Removed)"
  36.305  msgstr "(Fjernet)"
  36.306  
  36.307 -#: imipweb/event.py:528
  36.308 +#: imipweb/event.py:527
  36.309  msgid "Re-add"
  36.310  msgstr "Legge til på nytt"
  36.311  
  36.312 -#: imipweb/event.py:590
  36.313 +#: imipweb/event.py:589
  36.314  msgid "The following attendees have been suggested for this event:"
  36.315  msgstr "Følgende deltakere er blitt foreslått for denne hendelsen:"
  36.316  
  36.317 -#: imipweb/event.py:596 imipweb/event.py:626
  36.318 +#: imipweb/event.py:595 imipweb/event.py:625
  36.319  msgid "Suggested by..."
  36.320  msgstr "Foreslått av..."
  36.321  
  36.322 -#: imipweb/event.py:620
  36.323 +#: imipweb/event.py:619
  36.324  msgid "The following periods have been suggested for this event:"
  36.325  msgstr "Følgende perioder er blitt foreslått for denne hendelsen:"
  36.326  
  36.327 -#: imipweb/event.py:625
  36.328 +#: imipweb/event.py:624
  36.329  msgid "Periods"
  36.330  msgstr "Perioder"
  36.331  
  36.332 -#: imipweb/event.py:736
  36.333 +#: imipweb/event.py:735
  36.334  msgid "This event conflicts with others:"
  36.335  msgstr "Hendelsen er i konflikt med andre i tidsplanen:"
  36.336  
  36.337 -#: imipweb/event.py:763
  36.338 +#: imipweb/event.py:762
  36.339  msgid "(Unspecified event)"
  36.340  msgstr "(Ikke angitt hendelse)"
  36.341  
    37.1 --- a/test_all.sh	Tue Apr 19 21:20:57 2016 +0200
    37.2 +++ b/test_all.sh	Fri Apr 22 16:22:58 2016 +0200
    37.3 @@ -1,3 +1,6 @@
    37.4  #!/bin/sh
    37.5  
    37.6 -for FILENAME in tests/test_*.sh ; do echo $FILENAME ; $FILENAME ; done
    37.7 +for FILENAME in tests/test_*.sh ; do
    37.8 +    echo "$FILENAME"
    37.9 +    "$FILENAME"
   37.10 +done
    38.1 --- a/tests/common.sh	Tue Apr 19 21:20:57 2016 +0200
    38.2 +++ b/tests/common.sh	Fri Apr 22 16:22:58 2016 +0200
    38.3 @@ -1,47 +1,44 @@
    38.4  #!/bin/sh
    38.5  
    38.6 -THIS_DIR=`dirname "$0"`
    38.7 -BASE_DIR="$THIS_DIR/.."
    38.8 -
    38.9 -STORE=/tmp/store
   38.10 -STATIC=/tmp/static
   38.11 -PREFS=/tmp/prefs
   38.12 -JOURNAL=/tmp/journal
   38.13 -
   38.14 -ARGS="-S $STORE -P $STATIC -p $PREFS -j $JOURNAL -d"
   38.15 +. "`dirname \"$0\"`/common_minimal.sh"
   38.16  
   38.17  ACCEPT_SCRIPT="$THIS_DIR/test_handle.py"
   38.18 -ACCEPT_ARGS="accept $STORE $JOURNAL $PREFS"
   38.19 +ACCEPT_ARGS="accept $STORE_TYPE $STORE $JOURNAL $PREFS"
   38.20  
   38.21  COUNTER_SCRIPT="$THIS_DIR/test_handle.py"
   38.22 -COUNTER_ARGS="counter $STORE $JOURNAL $PREFS"
   38.23 +COUNTER_ARGS="counter $STORE_TYPE $STORE $JOURNAL $PREFS"
   38.24  
   38.25  DECLINE_SCRIPT="$THIS_DIR/test_handle.py"
   38.26 -DECLINE_ARGS="decline $STORE $JOURNAL $PREFS"
   38.27 +DECLINE_ARGS="decline $STORE_TYPE $STORE $JOURNAL $PREFS"
   38.28  
   38.29  FREEBUSY_SCRIPT="$BASE_DIR/tools/make_freebusy.py"
   38.30  FREEBUSY_ARGS="-s -n"
   38.31  
   38.32 +LIST_SCRIPT="$THIS_DIR/list_table.py"
   38.33 +LIST_ARGS="$STORE_TYPE $STORE $JOURNAL"
   38.34 +
   38.35  OUTGOING_SCRIPT="$BASE_DIR/imip_person_outgoing.py"
   38.36  
   38.37  PERSON_SCRIPT="$BASE_DIR/imip_person.py"
   38.38  
   38.39 -RESOURCE_SCRIPT="$BASE_DIR/imip_resource.py"
   38.40 -
   38.41 -SHOWMAIL="$BASE_DIR/tools/showmail.py"
   38.42 +SET_QUOTA_LIMIT="$BASE_DIR/tools/set_quota_limit.py"
   38.43 +SET_QUOTA_LIMIT_ARGS="-T $STORE_TYPE -j $JOURNAL" 
   38.44  
   38.45  TAB=`printf '\t'`
   38.46  
   38.47 -TEMPLATES="$THIS_DIR/templates"
   38.48 -
   38.49 -ERROR=err.tmp
   38.50 -
   38.51  PYTHONPATH="$BASE_DIR"
   38.52  export PYTHONPATH
   38.53  
   38.54 -rm -rf "$STORE"
   38.55 +if [ "$STORE_TYPE" = "file" ]; then
   38.56 +    rm -rf "$STORE"
   38.57 +    rm -rf "$JOURNAL"
   38.58 +elif [ "$STORE_TYPE" = "postgresql" ]; then
   38.59 +    dropdb "$DBNAME"
   38.60 +    createdb "$DBNAME"
   38.61 +    psql -q -f "$BASE_DIR/conf/postgresql/schema.sql" "$DBNAME"
   38.62 +fi
   38.63 +
   38.64  rm -rf "$STATIC"
   38.65  rm -rf "$PREFS"
   38.66 -rm -rf "$JOURNAL"
   38.67  rm -f "$ERROR"
   38.68  rm -f out*.tmp
    39.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    39.2 +++ b/tests/common_minimal.sh	Fri Apr 22 16:22:58 2016 +0200
    39.3 @@ -0,0 +1,28 @@
    39.4 +#!/bin/sh
    39.5 +
    39.6 +THIS_DIR=`dirname "$0"`
    39.7 +BASE_DIR="$THIS_DIR/.."
    39.8 +
    39.9 +STORE_TYPE=${STORE_TYPE:-file}
   39.10 +
   39.11 +if [ "$STORE_TYPE" = "file" ]; then
   39.12 +    STORE=/tmp/store
   39.13 +    JOURNAL=/tmp/journal
   39.14 +elif [ "$STORE_TYPE" = "postgresql" ]; then
   39.15 +    DBNAME='imip_agent_test'
   39.16 +    STORE="dbname=$DBNAME"
   39.17 +    JOURNAL="$STORE"
   39.18 +fi
   39.19 +
   39.20 +STATIC=/tmp/static
   39.21 +PREFS=/tmp/prefs
   39.22 +
   39.23 +ARGS="-T $STORE_TYPE -S $STORE -P $STATIC -p $PREFS -j $JOURNAL -d"
   39.24 +
   39.25 +RESOURCE_SCRIPT="$BASE_DIR/imip_resource.py"
   39.26 +
   39.27 +SHOWMAIL="$BASE_DIR/tools/showmail.py"
   39.28 +
   39.29 +TEMPLATES="$THIS_DIR/templates"
   39.30 +
   39.31 +ERROR=err.tmp
    40.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    40.2 +++ b/tests/list_table.py	Fri Apr 22 16:22:58 2016 +0200
    40.3 @@ -0,0 +1,110 @@
    40.4 +#!/usr/bin/env python
    40.5 +
    40.6 +"""
    40.7 +Show the contents of a table.
    40.8 +
    40.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
   40.10 +
   40.11 +This program is free software; you can redistribute it and/or modify it under
   40.12 +the terms of the GNU General Public License as published by the Free Software
   40.13 +Foundation; either version 3 of the License, or (at your option) any later
   40.14 +version.
   40.15 +
   40.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   40.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   40.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   40.19 +details.
   40.20 +
   40.21 +You should have received a copy of the GNU General Public License along with
   40.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   40.23 +"""
   40.24 +
   40.25 +from imiptools.data import Object
   40.26 +from imiptools.stores import get_store, get_journal
   40.27 +import sys
   40.28 +
   40.29 +def show_list(data):
   40.30 +    for row in data:
   40.31 +        print row or ""
   40.32 +
   40.33 +def show_object(data):
   40.34 +    if data:
   40.35 +        print Object(data).to_string()
   40.36 +
   40.37 +def show_periods(data):
   40.38 +    for row in data:
   40.39 +        print "\t".join(row.as_tuple(strings_only=True))
   40.40 +
   40.41 +def show_tuples(data):
   40.42 +    for row in data:
   40.43 +        print "\t".join([(column or "") for column in row])
   40.44 +
   40.45 +if __name__ == "__main__":
   40.46 +    try:
   40.47 +        store_type, store_dir, journal_dir, user, table = sys.argv[1:6]
   40.48 +        args = sys.argv[6:]
   40.49 +    except ValueError:
   40.50 +        print >>sys.stderr, """\
   40.51 +Need a store type, a store directory, a journal directory, a user URI, and a
   40.52 +table to show. Other arguments may be needed for certain tables.
   40.53 +"""
   40.54 +        sys.exit(1)
   40.55 +
   40.56 +    store = get_store(store_type, store_dir)
   40.57 +    journal = get_journal(store_type, journal_dir)
   40.58 +
   40.59 +    # Periods.
   40.60 +
   40.61 +    if table == "entries":
   40.62 +        group = args[0]
   40.63 +        data = journal.get_entries(user, group)
   40.64 +        show_periods(data)
   40.65 +
   40.66 +    elif table == "freebusy":
   40.67 +        data = store.get_freebusy(user)
   40.68 +        show_periods(data)
   40.69 +
   40.70 +    elif table == "freebusy_offers":
   40.71 +        data = store.get_freebusy_offers(user)
   40.72 +        show_periods(data)
   40.73 +
   40.74 +    elif table == "freebusy_other":
   40.75 +        other = args[0]
   40.76 +        data = store.get_freebusy_for_other(user, other)
   40.77 +        show_periods(data)
   40.78 +
   40.79 +    # Tuples.
   40.80 +
   40.81 +    elif table == "requests":
   40.82 +        data = store.get_requests(user)
   40.83 +        show_tuples(data)
   40.84 +
   40.85 +    elif table == "freebusy_providers":
   40.86 +        data = store.get_freebusy_providers(user)
   40.87 +        show_tuples(data)
   40.88 +
   40.89 +    # Objects.
   40.90 +
   40.91 +    elif table == "countered_object":
   40.92 +        uid = args[0]
   40.93 +        other = args[1]
   40.94 +        data = store.get_counter(user, other, uid)
   40.95 +        show_object(data)
   40.96 +
   40.97 +    elif table == "object":
   40.98 +        uid = args[0]
   40.99 +        data = store.get_event(user, uid)
  40.100 +        show_object(data)
  40.101 +
  40.102 +    elif table == "recurrence":
  40.103 +        uid = args[0]
  40.104 +        recurrenceid = args[1]
  40.105 +        data = store.get_event(user, uid, recurrenceid)
  40.106 +        show_object(data)
  40.107 +
  40.108 +    elif table == "cancelled_recurrences":
  40.109 +        uid = args[0]
  40.110 +        data = store.get_cancelled_recurrences(user, uid)
  40.111 +        show_list(data)
  40.112 +
  40.113 +# vim: tabstop=4 expandtab shiftwidth=4
    41.1 --- a/tests/resource_request.sh	Tue Apr 19 21:20:57 2016 +0200
    41.2 +++ b/tests/resource_request.sh	Fri Apr 22 16:22:58 2016 +0200
    41.3 @@ -1,17 +1,6 @@
    41.4  #!/bin/sh
    41.5  
    41.6 -THIS_DIR=`dirname $0`
    41.7 -
    41.8 -TEMPLATES="$THIS_DIR/templates"
    41.9 -RESOURCE_SCRIPT="$THIS_DIR/../imip_resource.py"
   41.10 -SHOWMAIL="$THIS_DIR/../tools/showmail.py"
   41.11 -STORE=/tmp/store
   41.12 -STATIC=/tmp/static
   41.13 -PREFS=/tmp/prefs
   41.14 -JOURNAL=/tmp/journal
   41.15 -ARGS="-S $STORE -P $STATIC -p $PREFS -j $JOURNAL -d"
   41.16 -
   41.17 -ERROR=err.tmp
   41.18 +. "`dirname \"$0\"`/common_minimal.sh"
   41.19  
   41.20  export N=$1
   41.21  export START=20141126T090000
    42.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    42.2 +++ b/tests/templates/event-request-person-recurring-rdate.txt	Fri Apr 22 16:22:58 2016 +0200
    42.3 @@ -0,0 +1,37 @@
    42.4 +Content-Type: multipart/alternative; boundary="===============0047278175=="
    42.5 +MIME-Version: 1.0
    42.6 +From: paul.boddie@example.com
    42.7 +To: vincent.vole@example.com, harvey.horse@example.com
    42.8 +Subject: Invitation!
    42.9 +
   42.10 +--===============0047278175==
   42.11 +Content-Type: text/plain; charset="us-ascii"
   42.12 +MIME-Version: 1.0
   42.13 +Content-Transfer-Encoding: 7bit
   42.14 +
   42.15 +This message contains an event.
   42.16 +
   42.17 +--===============0047278175==
   42.18 +MIME-Version: 1.0
   42.19 +Content-Transfer-Encoding: 7bit
   42.20 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST"
   42.21 +
   42.22 +BEGIN:VCALENDAR
   42.23 +PRODID:-//imip-agent/test//EN
   42.24 +METHOD:REQUEST
   42.25 +VERSION:2.0
   42.26 +BEGIN:VEVENT
   42.27 +ORGANIZER:mailto:paul.boddie@example.com
   42.28 +ATTENDEE;RSVP=TRUE:mailto:vincent.vole@example.com
   42.29 +ATTENDEE;RSVP=TRUE:mailto:harvey.horse@example.com
   42.30 +ATTENDEE;RSVP=TRUE:mailto:paul.boddie@example.com
   42.31 +DTSTAMP:20141009T182400Z
   42.32 +DTSTART;TZID=Europe/Oslo:20141010T100000
   42.33 +DTEND;TZID=Europe/Oslo:20141010T110000
   42.34 +RDATE;TZID=Europe/Oslo;VALUE=PERIOD:20141011T100000/20141011T110000
   42.35 +SUMMARY:Recurring event
   42.36 +UID:event26@example.com
   42.37 +END:VEVENT
   42.38 +END:VCALENDAR
   42.39 +
   42.40 +--===============0047278175==--
    43.1 --- a/tests/test_freebusy_publishing.sh	Tue Apr 19 21:20:57 2016 +0200
    43.2 +++ b/tests/test_freebusy_publishing.sh	Fri Apr 22 16:22:58 2016 +0200
    43.3 @@ -4,8 +4,6 @@
    43.4  
    43.5  USER="mailto:paul.boddie@example.com"
    43.6  SENDER="mailto:resource-room-confroom@example.com"
    43.7 -FBFILE="$STORE/$USER/freebusy"
    43.8 -FBOTHERFILE="$STORE/$USER/freebusy-other/$SENDER"
    43.9  
   43.10  mkdir -p "$PREFS/$USER"
   43.11  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   43.12 @@ -15,17 +13,23 @@
   43.13  | "$SHOWMAIL" \
   43.14  > out0.tmp
   43.15  
   43.16 -   grep -q "^20140401T070000Z${TAB}20140401T080000Z" "$FBOTHERFILE" \
   43.17 -&& grep -q "^20140401T080000Z${TAB}20140401T100000Z" "$FBOTHERFILE" \
   43.18 +  "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   43.19 +> out1.tmp
   43.20 +
   43.21 +   grep -q "^20140401T070000Z${TAB}20140401T080000Z" "out1.tmp" \
   43.22 +&& grep -q "^20140401T080000Z${TAB}20140401T100000Z" "out1.tmp" \
   43.23  && echo "Success" \
   43.24  || echo "Failed"
   43.25  
   43.26    "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/fb-publish-again.txt" 2>> $ERROR \
   43.27  | "$SHOWMAIL" \
   43.28 -> out0.tmp
   43.29 +> out2.tmp
   43.30  
   43.31 -   grep -q "^20140401T070000Z${TAB}20140401T080000Z" "$FBOTHERFILE" \
   43.32 -&& ! grep -q "^20140401T080000Z${TAB}20140401T100000Z" "$FBOTHERFILE" \
   43.33 -&& grep -q "^20140401T083000Z${TAB}20140401T100000Z" "$FBOTHERFILE" \
   43.34 +  "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   43.35 +> out3.tmp
   43.36 +
   43.37 +   grep -q "^20140401T070000Z${TAB}20140401T080000Z" "out3.tmp" \
   43.38 +&& ! grep -q "^20140401T080000Z${TAB}20140401T100000Z" "out3.tmp" \
   43.39 +&& grep -q "^20140401T083000Z${TAB}20140401T100000Z" "out3.tmp" \
   43.40  && echo "Success" \
   43.41  || echo "Failed"
    44.1 --- a/tests/test_handle.py	Tue Apr 19 21:20:57 2016 +0200
    44.2 +++ b/tests/test_handle.py	Fri Apr 22 16:22:58 2016 +0200
    44.3 @@ -24,7 +24,8 @@
    44.4  from imiptools.dates import get_datetime, to_timezone
    44.5  from imiptools.mail import Messenger
    44.6  from imiptools.period import RecurringPeriod
    44.7 -import imiptools.stores.file
    44.8 +from imiptools.stores import get_store, get_journal
    44.9 +from os.path import split
   44.10  import sys
   44.11  
   44.12  class TestClient(ClientForObject):
   44.13 @@ -36,7 +37,7 @@
   44.14  
   44.15      # Action methods.
   44.16  
   44.17 -    def handle_request(self, action, start=None, end=None):
   44.18 +    def handle_request(self, action, start=None, end=None, recurrenceid=None):
   44.19  
   44.20          """
   44.21          Process the current request for the current user. Return whether the
   44.22 @@ -44,18 +45,36 @@
   44.23  
   44.24          If 'start' and 'end' are specified, they will be used in any
   44.25          counter-proposal.
   44.26 +
   44.27 +        Where 'recurrenceid' is specified and refers to a new recurrence, the
   44.28 +        action will apply only to this new recurrence.
   44.29          """
   44.30  
   44.31 +        have_new_recurrence = self.obj.get_recurrenceid() != recurrenceid
   44.32 +
   44.33 +        if have_new_recurrence:
   44.34 +            self.obj["RECURRENCE-ID"] = [(recurrenceid, {})]
   44.35 +            self.obj.remove_all(["RDATE", "RRULE"])
   44.36 +
   44.37          # Reply only on behalf of this user.
   44.38  
   44.39          if action in ("accept", "decline"):
   44.40              attendee_attr = self.update_participation(action == "accept" and "ACCEPTED" or "DECLINED")
   44.41              method = "REPLY"
   44.42  
   44.43 -        # For counter-proposals, set a new main period for the event.
   44.44 -
   44.45          elif action == "counter":
   44.46              attendee_attr = self.obj.get_value_map("ATTENDEE").get(self.user)
   44.47 +            method = "COUNTER"
   44.48 +
   44.49 +        # Nothing else is supported.
   44.50 +
   44.51 +        else:
   44.52 +            return None
   44.53 +
   44.54 +        # For counter-proposals or new recurrences, set a new main period for
   44.55 +        # the event.
   44.56 +
   44.57 +        if action == "counter" or have_new_recurrence:
   44.58              period = self.obj.get_main_period(self.get_tzid())
   44.59  
   44.60              # Use the existing or configured time zone for the specified
   44.61 @@ -65,9 +84,8 @@
   44.62              end = to_timezone(get_datetime(end), period.tzid)
   44.63              period = RecurringPeriod(start, end, period.tzid, period.origin, period.get_start_attr(), period.get_end_attr())
   44.64              self.obj.set_period(period)
   44.65 -            method = "COUNTER"
   44.66 -        else:
   44.67 -            return None
   44.68 +
   44.69 +        # Where no attendees remain, no message is generated.
   44.70  
   44.71          if not attendee_attr:
   44.72              return None
   44.73 @@ -93,39 +111,53 @@
   44.74  # response message to standard output.
   44.75  
   44.76  if __name__ == "__main__":
   44.77 +    progname = split(sys.argv[0])[-1]
   44.78 +
   44.79      try:
   44.80 -        action, store_dir, journal_dir, preferences_dir, user = sys.argv[1:6]
   44.81 -        if action == "counter":
   44.82 -            start, end = sys.argv[6:8]
   44.83 -            i = 8
   44.84 +        action, store_type, store_dir, journal_dir, preferences_dir, user = sys.argv[1:7]
   44.85 +        if len(sys.argv) >= 10:
   44.86 +            start, end = sys.argv[7:9]
   44.87 +            i = 9
   44.88          else:
   44.89              start, end = None, None
   44.90 -            i = 6
   44.91 +            i = 7
   44.92          uid, recurrenceid = (sys.argv[i:i+2] + [None] * 2)[:2]
   44.93      except ValueError:
   44.94          print >>sys.stderr, """\
   44.95 -Need 'accept', 'counter' or 'decline', a store directory, a preferences
   44.96 -directory, user URI, any counter-proposal datetimes (see below), plus the
   44.97 -appropriate event UID and RECURRENCE-ID (if a recurrence is involved).
   44.98 +Usage: %s <action> <store type> <store directory> <journal directory>
   44.99 +       <preferences directory> <user URI> [ <start> <end> ]
  44.100 +       <uid> <recurrence-id>
  44.101 +
  44.102 +Need 'accept', 'counter' or 'decline', a store type, a store directory, a
  44.103 +journal directory, a preferences directory, user URI, any counter-proposal or
  44.104 +new recurrence datetimes (see below), plus the appropriate event UID and
  44.105 +RECURRENCE-ID (if a recurrence is involved).
  44.106  
  44.107  The RECURRENCE-ID must be in exactly the form employed by the store, not a
  44.108 -different but equivalent representation.
  44.109 +different but equivalent representation, if the identifier is to refer to an
  44.110 +existing recurrence.
  44.111  
  44.112  Alternatively, omit the UID and RECURRENCE-ID and provide event-only details on
  44.113  standard input to force the script to handle an event not already present in the
  44.114  store.
  44.115  
  44.116  If 'counter' has been indicated, alternative start and end datetimes are also
  44.117 -required.
  44.118 +required. If a specific recurrence is being separated from an event, such
  44.119 +datetimes are also required in order to set the main period of the recurrence.
  44.120  """
  44.121          sys.exit(1)
  44.122  
  44.123 -    store = imiptools.stores.file.FileStore(store_dir)
  44.124 -    journal = imiptools.stores.file.FileJournal(journal_dir)
  44.125 +    store = get_store(store_type, store_dir)
  44.126 +    journal = get_journal(store_type, journal_dir)
  44.127  
  44.128      if uid is not None:
  44.129          fragment = store.get_event(user, uid, recurrenceid)
  44.130  
  44.131 +        # Permit new recurrences by getting the parent object.
  44.132 +
  44.133 +        if not fragment:
  44.134 +            fragment = store.get_event(user, uid)
  44.135 +
  44.136          if not fragment:
  44.137              print >>sys.stderr, "No such event:", uid, recurrenceid
  44.138              sys.exit(1)
  44.139 @@ -134,7 +166,7 @@
  44.140  
  44.141      obj = Object(fragment)
  44.142      handler = TestClient(obj, user, Messenger(), store, None, journal, preferences_dir)
  44.143 -    response = handler.handle_request(action, start, end)
  44.144 +    response = handler.handle_request(action, start, end, recurrenceid)
  44.145  
  44.146      if response:
  44.147          if uid is not None:
    45.1 --- a/tests/test_outgoing_invitation.sh	Tue Apr 19 21:20:57 2016 +0200
    45.2 +++ b/tests/test_outgoing_invitation.sh	Fri Apr 22 16:22:58 2016 +0200
    45.3 @@ -3,70 +3,84 @@
    45.4  . "`dirname \"$0\"`/common.sh"
    45.5  
    45.6  USER="mailto:paul.boddie@example.com"
    45.7 -FBFILE="$STORE/$USER/freebusy"
    45.8  
    45.9  mkdir -p "$PREFS/$USER"
   45.10  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   45.11  echo 'share' > "$PREFS/$USER/freebusy_sharing"
   45.12  
   45.13  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request.txt" 2>> $ERROR
   45.14 -cp "$FBFILE" out1.tmp
   45.15  
   45.16 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   45.17 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   45.18 +|  tee out1.tmp \
   45.19 +|  grep -q "^20141126T150000Z${TAB}20141126T160000Z" \
   45.20  && echo "Success" \
   45.21  || echo "Failed"
   45.22  
   45.23  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel.txt" 2>> $ERROR
   45.24  echo "Cancel..."
   45.25 -cp "$FBFILE" out2.tmp
   45.26  
   45.27 -   ! grep -q '^2' "$FBFILE" \
   45.28 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   45.29 +>  out2.tmp
   45.30 +
   45.31 +   ! grep -q '^2' "out2.tmp" \
   45.32  && echo "Success" \
   45.33  || echo "Failed"
   45.34  
   45.35  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring.txt" 2>> $ERROR
   45.36 -cp "$FBFILE" out3.tmp
   45.37  
   45.38 -   [ `cat "$FBFILE" | wc -l` = '3' ] \
   45.39 -&& grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   45.40 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   45.41 +>  out3.tmp
   45.42 +
   45.43 +   [ `cat "out3.tmp" | wc -l` = '3' ] \
   45.44 +&& grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out3.tmp" \
   45.45  && echo "Success" \
   45.46  || echo "Failed"
   45.47  
   45.48  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-recurring-instance.txt" 2>> $ERROR
   45.49 -cp "$FBFILE" out4.tmp
   45.50  
   45.51 -   [ `cat "$FBFILE" | wc -l` = '2' ] \
   45.52 -&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "$FBFILE" \
   45.53 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   45.54 +>  out4.tmp
   45.55 +
   45.56 +   [ `cat "out4.tmp" | wc -l` = '2' ] \
   45.57 +&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "out4.tmp" \
   45.58  && echo "Success" \
   45.59  || echo "Failed"
   45.60  
   45.61  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring-reschedule-instance.txt" 2>> $ERROR
   45.62 -cp "$FBFILE" out5.tmp
   45.63  
   45.64 -   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBFILE" \
   45.65 -&& ! grep -q "^20141010T090000Z${TAB}20141010T100000Z" "$FBFILE" \
   45.66 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   45.67 +>  out5.tmp
   45.68 +
   45.69 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out5.tmp" \
   45.70 +&& ! grep -q "^20141010T090000Z${TAB}20141010T100000Z" "out5.tmp" \
   45.71  && echo "Success" \
   45.72  || echo "Failed"
   45.73  
   45.74  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-recurring.txt" 2>> $ERROR
   45.75 -cp "$FBFILE" out6.tmp
   45.76  
   45.77 -   ! grep -q '^2' "$FBFILE" \
   45.78 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   45.79 +>  out6.tmp
   45.80 +
   45.81 +   ! grep -q '^2' "out6.tmp" \
   45.82  && echo "Success" \
   45.83  || echo "Failed"
   45.84  
   45.85  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring-day.txt" 2>> $ERROR
   45.86 -cp "$FBFILE" out7.tmp
   45.87  
   45.88 -   [ `cat "$FBFILE" | wc -l` = '3' ] \
   45.89 -&& grep -q "^20141211T230000Z${TAB}20141212T230000Z" "$FBFILE" \
   45.90 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   45.91 +>  out7.tmp
   45.92 +
   45.93 +   [ `cat "out7.tmp" | wc -l` = '3' ] \
   45.94 +&& grep -q "^20141211T230000Z${TAB}20141212T230000Z" "out7.tmp" \
   45.95  && echo "Success" \
   45.96  || echo "Failed"
   45.97  
   45.98  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-recurring-day.txt" 2>> $ERROR
   45.99 -cp "$FBFILE" out8.tmp
  45.100  
  45.101 -   ! grep -q '^2' "$FBFILE" \
  45.102 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  45.103 +>  out8.tmp
  45.104 +
  45.105 +   ! grep -q '^2' "out8.tmp" \
  45.106  && echo "Success" \
  45.107  || echo "Failed"
  45.108  
  45.109 @@ -75,31 +89,39 @@
  45.110  echo 'Europe/Mariehamn' > "$PREFS/$USER/TZID"
  45.111  
  45.112  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring.txt" 2>> $ERROR
  45.113 -cp "$FBFILE" out9.tmp
  45.114  
  45.115 -   [ `cat "$FBFILE" | wc -l` = '3' ] \
  45.116 -&& grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
  45.117 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  45.118 +>  out9.tmp
  45.119 +
  45.120 +   [ `cat "out9.tmp" | wc -l` = '3' ] \
  45.121 +&& grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out9.tmp" \
  45.122  && echo "Success" \
  45.123  || echo "Failed"
  45.124  
  45.125  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-recurring.txt" 2>> $ERROR
  45.126 -cp "$FBFILE" out10.tmp
  45.127  
  45.128 -   ! grep -q '^2' "$FBFILE" \
  45.129 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  45.130 +>  out10.tmp
  45.131 +
  45.132 +   ! grep -q '^2' "out10.tmp" \
  45.133  && echo "Success" \
  45.134  || echo "Failed"
  45.135  
  45.136  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring-day.txt" 2>> $ERROR
  45.137 -cp "$FBFILE" out11.tmp
  45.138  
  45.139 -   [ `cat "$FBFILE" | wc -l` = '3' ] \
  45.140 -&& grep -q "^20141211T230000Z${TAB}20141212T230000Z" "$FBFILE" \
  45.141 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  45.142 +>  out11.tmp
  45.143 +
  45.144 +   [ `cat "out11.tmp" | wc -l` = '3' ] \
  45.145 +&& grep -q "^20141211T230000Z${TAB}20141212T230000Z" "out11.tmp" \
  45.146  && echo "Success" \
  45.147  || echo "Failed"
  45.148  
  45.149  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-recurring-day.txt" 2>> $ERROR
  45.150 -cp "$FBFILE" out12.tmp
  45.151  
  45.152 -   ! grep -q '^2' "$FBFILE" \
  45.153 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  45.154 +>  out12.tmp
  45.155 +
  45.156 +   ! grep -q '^2' "out12.tmp" \
  45.157  && echo "Success" \
  45.158  || echo "Failed"
    46.1 --- a/tests/test_person_invitation.sh	Tue Apr 19 21:20:57 2016 +0200
    46.2 +++ b/tests/test_person_invitation.sh	Fri Apr 22 16:22:58 2016 +0200
    46.3 @@ -4,8 +4,6 @@
    46.4  
    46.5  USER="mailto:vincent.vole@example.com"
    46.6  SENDER="mailto:paul.boddie@example.com"
    46.7 -FBFILE="$STORE/$USER/freebusy"
    46.8 -FBOTHERFILE="$STORE/$USER/freebusy-other/$SENDER"
    46.9  
   46.10  mkdir -p "$PREFS/$USER"
   46.11  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   46.12 @@ -37,12 +35,17 @@
   46.13  && echo "Success" \
   46.14  || echo "Failed"
   46.15  
   46.16 -   ! [ -e "$FBFILE" ] \
   46.17 -|| ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   46.18 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   46.19 +>  out2f.tmp
   46.20 +
   46.21 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \
   46.22  && echo "Success" \
   46.23  || echo "Failed"
   46.24  
   46.25 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBOTHERFILE" \
   46.26 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   46.27 +>  out2fo.tmp
   46.28 +
   46.29 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2fo.tmp" \
   46.30  && echo "Success" \
   46.31  || echo "Failed"
   46.32  
   46.33 @@ -50,7 +53,10 @@
   46.34  | tee out3.tmp \
   46.35  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
   46.36  
   46.37 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   46.38 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   46.39 +>  out3f.tmp
   46.40 +
   46.41 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out3f.tmp" \
   46.42  && echo "Success" \
   46.43  || echo "Failed"
   46.44  
   46.45 @@ -62,11 +68,17 @@
   46.46  && echo "Success" \
   46.47  || echo "Failed"
   46.48  
   46.49 -   ! grep -q "event7@example.com" "$FBFILE" \
   46.50 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   46.51 +>  out4f.tmp
   46.52 +
   46.53 +   ! grep -q "event7@example.com" "out4f.tmp" \
   46.54  && echo "Success" \
   46.55  || echo "Failed"
   46.56  
   46.57 -   grep -q "event7@example.com" "$FBOTHERFILE" \
   46.58 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   46.59 +>  out4fo.tmp
   46.60 +
   46.61 +   grep -q "event7@example.com" "out4fo.tmp" \
   46.62  && echo "Success" \
   46.63  || echo "Failed"
   46.64  
   46.65 @@ -74,8 +86,11 @@
   46.66  | tee out5.tmp \
   46.67  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
   46.68  
   46.69 -   grep -q "event6@example.com" "$FBFILE" \
   46.70 -&& ! grep -q "event7@example.com" "$FBFILE" \
   46.71 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   46.72 +>  out5f.tmp
   46.73 +
   46.74 +   grep -q "event6@example.com" "out5f.tmp" \
   46.75 +&& ! grep -q "event7@example.com" "out5f.tmp" \
   46.76  && echo "Success" \
   46.77  || echo "Failed"
   46.78  
   46.79 @@ -87,11 +102,17 @@
   46.80  && echo "Success" \
   46.81  || echo "Failed"
   46.82  
   46.83 -   ! grep -q "event6@example.com" "$FBFILE" \
   46.84 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   46.85 +>  out6f.tmp
   46.86 +
   46.87 +   ! grep -q "event6@example.com" "out6f.tmp" \
   46.88  && echo "Success" \
   46.89  || echo "Failed"
   46.90  
   46.91 -   grep -q "event6@example.com" "$FBOTHERFILE" \
   46.92 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   46.93 +>  out6fo.tmp
   46.94 +
   46.95 +   grep -q "event6@example.com" "out6fo.tmp" \
   46.96  && echo "Success" \
   46.97  || echo "Failed"
   46.98  
   46.99 @@ -103,11 +124,17 @@
  46.100  && echo "Success" \
  46.101  || echo "Failed"
  46.102  
  46.103 -   ! grep -q "event6@example.com" "$FBFILE" \
  46.104 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  46.105 +>  out7f.tmp
  46.106 +
  46.107 +   ! grep -q "event6@example.com" "out7f.tmp" \
  46.108  && echo "Success" \
  46.109  || echo "Failed"
  46.110  
  46.111 -   ! grep -q "event6@example.com" "$FBOTHERFILE" \
  46.112 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  46.113 +>  out7fo.tmp
  46.114 +
  46.115 +   ! grep -q "event6@example.com" "out7fo.tmp" \
  46.116  && echo "Success" \
  46.117  || echo "Failed"
  46.118  
  46.119 @@ -119,10 +146,16 @@
  46.120  && echo "Success" \
  46.121  || echo "Failed"
  46.122  
  46.123 -   ! grep -q "spoof2@example.com" "$FBFILE" \
  46.124 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  46.125 +>  out8f.tmp
  46.126 +
  46.127 +   ! grep -q "spoof2@example.com" "out8f.tmp" \
  46.128  && echo "Success" \
  46.129  || echo "Failed"
  46.130  
  46.131 -   ! grep -q "spoof2@example.com" "$FBOTHERFILE" \
  46.132 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  46.133 +>  out8fo.tmp
  46.134 +
  46.135 +   ! grep -q "spoof2@example.com" "out8fo.tmp" \
  46.136  && echo "Success" \
  46.137  || echo "Failed"
    47.1 --- a/tests/test_person_invitation_add.sh	Tue Apr 19 21:20:57 2016 +0200
    47.2 +++ b/tests/test_person_invitation_add.sh	Fri Apr 22 16:22:58 2016 +0200
    47.3 @@ -4,10 +4,6 @@
    47.4  
    47.5  USER="mailto:vincent.vole@example.com"
    47.6  SENDER="mailto:paul.boddie@example.com"
    47.7 -FBFILE="$STORE/$USER/freebusy"
    47.8 -FBOTHERFILE="$STORE/$USER/freebusy-other/$SENDER"
    47.9 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   47.10 -FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
   47.11  
   47.12  mkdir -p "$PREFS/$USER"
   47.13  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   47.14 @@ -21,7 +17,10 @@
   47.15  
   47.16  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring.txt" 2>> $ERROR
   47.17  
   47.18 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDERFILE" \
   47.19 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   47.20 +>  out1f.tmp
   47.21 +
   47.22 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out1f.tmp" \
   47.23  && echo "Success" \
   47.24  || echo "Failed"
   47.25  
   47.26 @@ -35,12 +34,17 @@
   47.27  && echo "Success" \
   47.28  || echo "Failed"
   47.29  
   47.30 -   ! [ -e "$FBFILE" ] \
   47.31 -|| ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   47.32 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   47.33 +>  out2f.tmp
   47.34 +
   47.35 +   ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out2f.tmp" \
   47.36  && echo "Success" \
   47.37  || echo "Failed"
   47.38  
   47.39 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBOTHERFILE" \
   47.40 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   47.41 +>  out2fo.tmp
   47.42 +
   47.43 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out2fo.tmp" \
   47.44  && echo "Success" \
   47.45  || echo "Failed"
   47.46  
   47.47 @@ -54,7 +58,10 @@
   47.48  && echo "Success" \
   47.49  || echo "Failed"
   47.50  
   47.51 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   47.52 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   47.53 +>  out3f.tmp
   47.54 +
   47.55 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out3f.tmp" \
   47.56  && echo "Success" \
   47.57  || echo "Failed"
   47.58  
   47.59 @@ -64,7 +71,10 @@
   47.60  | "$SHOWMAIL" \
   47.61  > out4.tmp
   47.62  
   47.63 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDEROTHERFILE" \
   47.64 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \
   47.65 +>  out4fo.tmp
   47.66 +
   47.67 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out4fo.tmp" \
   47.68  && echo "Success" \
   47.69  || echo "Failed"
   47.70  
   47.71 @@ -72,8 +82,11 @@
   47.72  
   47.73  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-add-person-recurring.txt" 2>> $ERROR
   47.74  
   47.75 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDERFILE" \
   47.76 -&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBSENDERFILE" \
   47.77 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   47.78 +>  out4f.tmp
   47.79 +
   47.80 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out4f.tmp" \
   47.81 +&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out4f.tmp" \
   47.82  && echo "Success" \
   47.83  || echo "Failed"
   47.84  
   47.85 @@ -87,13 +100,19 @@
   47.86  && echo "Success" \
   47.87  || echo "Failed"
   47.88  
   47.89 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   47.90 -&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBFILE" \
   47.91 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   47.92 +>  out5f.tmp
   47.93 +
   47.94 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out5f.tmp" \
   47.95 +&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out5f.tmp" \
   47.96  && echo "Success" \
   47.97  || echo "Failed"
   47.98  
   47.99 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBOTHERFILE" \
  47.100 -&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBOTHERFILE" \
  47.101 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  47.102 +>  out5fo.tmp
  47.103 +
  47.104 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out5fo.tmp" \
  47.105 +&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out5fo.tmp" \
  47.106  && echo "Success" \
  47.107  || echo "Failed"
  47.108  
  47.109 @@ -118,13 +137,19 @@
  47.110  && echo "Success" \
  47.111  || echo "Failed"
  47.112  
  47.113 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
  47.114 -&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBFILE" \
  47.115 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  47.116 +>  out7f.tmp
  47.117 +
  47.118 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out7f.tmp" \
  47.119 +&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out7f.tmp" \
  47.120  && echo "Success" \
  47.121  || echo "Failed"
  47.122  
  47.123 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBOTHERFILE" \
  47.124 -&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBOTHERFILE" \
  47.125 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  47.126 +>  out7fo.tmp
  47.127 +
  47.128 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out7fo.tmp" \
  47.129 +&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out7fo.tmp" \
  47.130  && echo "Success" \
  47.131  || echo "Failed"
  47.132  
  47.133 @@ -146,7 +171,10 @@
  47.134  && echo "Success" \
  47.135  || echo "Failed"
  47.136  
  47.137 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
  47.138 -&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBFILE" \
  47.139 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  47.140 +>  out9f.tmp
  47.141 +
  47.142 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out9f.tmp" \
  47.143 +&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out9f.tmp" \
  47.144  && echo "Success" \
  47.145  || echo "Failed"
    48.1 --- a/tests/test_person_invitation_counter.sh	Tue Apr 19 21:20:57 2016 +0200
    48.2 +++ b/tests/test_person_invitation_counter.sh	Fri Apr 22 16:22:58 2016 +0200
    48.3 @@ -4,11 +4,6 @@
    48.4  
    48.5  USER="mailto:vincent.vole@example.com"
    48.6  SENDER="mailto:paul.boddie@example.com"
    48.7 -FBFILE="$STORE/$USER/freebusy"
    48.8 -FBOFFERFILE="$STORE/$USER/freebusy-offers"
    48.9 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   48.10 -FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
   48.11 -FBSENDERREQUESTS="$STORE/$SENDER/requests"
   48.12  
   48.13  mkdir -p "$PREFS/$USER"
   48.14  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   48.15 @@ -28,7 +23,10 @@
   48.16  
   48.17  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person.txt" 2>> $ERROR
   48.18  
   48.19 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
   48.20 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   48.21 +>  out0f.tmp
   48.22 +
   48.23 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out0f.tmp" \
   48.24  && echo "Success" \
   48.25  || echo "Failed"
   48.26  
   48.27 @@ -42,8 +40,10 @@
   48.28  && echo "Success" \
   48.29  || echo "Failed"
   48.30  
   48.31 -   ! [ -e "$FBFILE" ] \
   48.32 -|| ! grep -q "event6@example.com" "$FBFILE" \
   48.33 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   48.34 +>  out1f.tmp
   48.35 +
   48.36 +   ! grep -q "event6@example.com" "out1f.tmp" \
   48.37  && echo "Success" \
   48.38  || echo "Failed"
   48.39  
   48.40 @@ -61,9 +61,11 @@
   48.41  
   48.42  # Note that the invitation has only been prepared, not processed.
   48.43  
   48.44 -   ! [ -e "$FBFILE" ] \
   48.45 -|| ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   48.46 -  && ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBFILE" ) \
   48.47 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   48.48 +>  out2f.tmp
   48.49 +
   48.50 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \
   48.51 +&& ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out2f.tmp" \
   48.52  && echo "Success" \
   48.53  || echo "Failed"
   48.54  
   48.55 @@ -72,8 +74,10 @@
   48.56  && echo "Success" \
   48.57  || echo "Failed"
   48.58  
   48.59 -   ! [ -e "$FBOFFERFILE" ] \
   48.60 -|| ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBOFFERFILE" \
   48.61 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
   48.62 +>  out2o.tmp
   48.63 +
   48.64 +   ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out2o.tmp" \
   48.65  && echo "Success" \
   48.66  || echo "Failed"
   48.67  
   48.68 @@ -81,13 +85,18 @@
   48.69  
   48.70  "$OUTGOING_SCRIPT" $ARGS < out2r.tmp 2>> $ERROR
   48.71  
   48.72 -   ! [ -e "$FBFILE" ] \
   48.73 -|| ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   48.74 -  && ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBFILE" ) \
   48.75 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   48.76 +>  out2f2.tmp
   48.77 +
   48.78 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f2.tmp" \
   48.79 +&& ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out2f2.tmp" \
   48.80  && echo "Success" \
   48.81  || echo "Failed"
   48.82  
   48.83 -   grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBOFFERFILE" \
   48.84 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
   48.85 +>  out2o2.tmp
   48.86 +
   48.87 +   grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out2o2.tmp" \
   48.88  && echo "Success" \
   48.89  || echo "Failed"
   48.90  
   48.91 @@ -98,21 +107,32 @@
   48.92  | "$SHOWMAIL" \
   48.93  > out3.tmp
   48.94  
   48.95 -   ! [ -e "$FBSENDEROTHERFILE" ] \
   48.96 -|| ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDEROTHERFILE" \
   48.97 -  && ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBSENDEROTHERFILE" ) \
   48.98 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \
   48.99 +>  out3f.tmp
  48.100 +
  48.101 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out3f.tmp" \
  48.102 +&& ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out3f.tmp" \
  48.103  && echo "Success" \
  48.104  || echo "Failed"
  48.105  
  48.106 -   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T160000' "$STORE/$SENDER/objects/event6@example.com" \
  48.107 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event6@example.com" \
  48.108 +>  out3O.tmp
  48.109 +
  48.110 +   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T160000' "out3O.tmp" \
  48.111  && echo "Success" \
  48.112  || echo "Failed"
  48.113  
  48.114 -   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T170000' "$STORE/$SENDER/counters/objects/event6@example.com/$USER" \
  48.115 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "countered_object" "event6@example.com" "$USER" \
  48.116 +>  out3C.tmp
  48.117 +
  48.118 +   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T170000' "out3C.tmp" \
  48.119  && echo "Success" \
  48.120  || echo "Failed"
  48.121  
  48.122 -   grep -q 'event6@example.com' "$FBSENDERREQUESTS" \
  48.123 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "requests" \
  48.124 +>  out3R.tmp
  48.125 +
  48.126 +   grep -q 'event6@example.com' "out3R.tmp" \
  48.127  && echo "Success" \
  48.128  || echo "Failed"
  48.129  
  48.130 @@ -137,11 +157,17 @@
  48.131  
  48.132  "$OUTGOING_SCRIPT" $ARGS < out5.tmp 2>> $ERROR
  48.133  
  48.134 -   ! [ -e "$STORE/$SENDER/counters/objects/event6@example.com/$USER" ] \
  48.135 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "countered_object" "event6@example.com" "$USER" \
  48.136 +>  out5C.tmp
  48.137 +
  48.138 +   ! grep -q 'event6@example.com' "out5C.tmp" \
  48.139  && echo "Success" \
  48.140  || echo "Failed"
  48.141  
  48.142 -   ! grep -q 'event6@example.com' "$FBSENDERREQUESTS" \
  48.143 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "requests" \
  48.144 +>  out5R.tmp
  48.145 +
  48.146 +   ! grep -q 'event6@example.com' "out5R.tmp" \
  48.147  && echo "Success" \
  48.148  || echo "Failed"
  48.149  
  48.150 @@ -149,13 +175,18 @@
  48.151  | "$SHOWMAIL" \
  48.152  > out6.tmp
  48.153  
  48.154 -   ! [ -e "$FBFILE" ] \
  48.155 -|| ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
  48.156 -  && ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBFILE" ) \
  48.157 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  48.158 +>  out6f.tmp
  48.159 +
  48.160 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out6f.tmp" \
  48.161 +&& ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out6f.tmp" \
  48.162  && echo "Success" \
  48.163  || echo "Failed"
  48.164  
  48.165 -   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBOFFERFILE" \
  48.166 -&& ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBOFFERFILE" \
  48.167 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
  48.168 +>  out6o.tmp
  48.169 +
  48.170 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out6o.tmp" \
  48.171 +&& ! grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out6o.tmp" \
  48.172  && echo "Success" \
  48.173  || echo "Failed"
    49.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    49.2 +++ b/tests/test_person_invitation_decline_instance.sh	Fri Apr 22 16:22:58 2016 +0200
    49.3 @@ -0,0 +1,392 @@
    49.4 +#!/bin/sh
    49.5 +
    49.6 +. "`dirname \"$0\"`/common.sh"
    49.7 +
    49.8 +USER1="mailto:vincent.vole@example.com"
    49.9 +USER2="mailto:harvey.horse@example.com"
   49.10 +SENDER="mailto:paul.boddie@example.com"
   49.11 +
   49.12 +mkdir -p "$PREFS/$USER1"
   49.13 +echo 'Europe/Oslo' > "$PREFS/$USER1/TZID"
   49.14 +echo 'share' > "$PREFS/$USER1/freebusy_sharing"
   49.15 +
   49.16 +mkdir -p "$PREFS/$USER2"
   49.17 +echo 'Europe/Oslo' > "$PREFS/$USER2/TZID"
   49.18 +echo 'share' > "$PREFS/$USER2/freebusy_sharing"
   49.19 +
   49.20 +mkdir -p "$PREFS/$SENDER"
   49.21 +echo 'Europe/Oslo' > "$PREFS/$SENDER/TZID"
   49.22 +
   49.23 +# Test free/busy responses.
   49.24 +
   49.25 +  "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/fb-request-person-all.txt" 2>> $ERROR \
   49.26 +| "$SHOWMAIL" \
   49.27 +> out0.tmp
   49.28 +
   49.29 +   grep -q 'METHOD:REPLY' out0.tmp \
   49.30 +&& ! grep -q '^FREEBUSY' out0.tmp \
   49.31 +&& echo "Success" \
   49.32 +|| echo "Failed"
   49.33 +
   49.34 +  "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/fb-request-person.txt" 2>> $ERROR \
   49.35 +| "$SHOWMAIL" \
   49.36 +> out1.tmp
   49.37 +
   49.38 +   grep -q 'METHOD:REPLY' out1.tmp \
   49.39 +&& ! grep -q '^FREEBUSY' out1.tmp \
   49.40 +&& echo "Success" \
   49.41 +|| echo "Failed"
   49.42 +
   49.43 +# Publish an event, testing registration in the outgoing handler.
   49.44 +
   49.45 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-rdate.txt" 2>> $ERROR
   49.46 +
   49.47 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   49.48 +>  out1f.tmp
   49.49 +
   49.50 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out1f.tmp" \
   49.51 +&& echo "Success" \
   49.52 +|| echo "Failed"
   49.53 +
   49.54 +# There should be an event created by the sender.
   49.55 +
   49.56 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event26@example.com" \
   49.57 +>  out1O.tmp
   49.58 +
   49.59 +   grep -q 'event26@example.com' "out1O.tmp" \
   49.60 +&& echo "Success" \
   49.61 +|| echo "Failed"
   49.62 +
   49.63 +# Test registration in the incoming handler for the recipients.
   49.64 +
   49.65 +  "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-rdate.txt" 2>> $ERROR \
   49.66 +| "$SHOWMAIL" \
   49.67 +> out2.tmp
   49.68 +
   49.69 +   ! grep -q 'METHOD:REPLY' out2.tmp \
   49.70 +&& echo "Success" \
   49.71 +|| echo "Failed"
   49.72 +
   49.73 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
   49.74 +>  out2f.tmp
   49.75 +
   49.76 +   ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2f.tmp" \
   49.77 +&& echo "Success" \
   49.78 +|| echo "Failed"
   49.79 +
   49.80 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy_other" "$SENDER" \
   49.81 +>  out2o.tmp
   49.82 +
   49.83 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2o.tmp" \
   49.84 +&& echo "Success" \
   49.85 +|| echo "Failed"
   49.86 +
   49.87 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
   49.88 +>  out2f2.tmp
   49.89 +
   49.90 +   ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2f2.tmp" \
   49.91 +&& echo "Success" \
   49.92 +|| echo "Failed"
   49.93 +
   49.94 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy_other" "$SENDER" \
   49.95 +>  out2o2.tmp
   49.96 +
   49.97 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2o2.tmp" \
   49.98 +&& echo "Success" \
   49.99 +|| echo "Failed"
  49.100 +
  49.101 +# There should be an event created by the sender.
  49.102 +
  49.103 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "object" "event26@example.com" \
  49.104 +>  out2O.tmp
  49.105 +
  49.106 +   grep -q 'event26@example.com' "out2O.tmp" \
  49.107 +&& echo "Success" \
  49.108 +|| echo "Failed"
  49.109 +
  49.110 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "object" "event26@example.com" \
  49.111 +>  out2O2.tmp
  49.112 +
  49.113 +   grep -q 'event26@example.com' "out2O2.tmp" \
  49.114 +&& echo "Success" \
  49.115 +|| echo "Failed"
  49.116 +
  49.117 +# Test acceptance and registration in the outgoing handler.
  49.118 +
  49.119 +  "$ACCEPT_SCRIPT" $ACCEPT_ARGS "$USER1" "event26@example.com" 2>> $ERROR \
  49.120 +| tee out3.tmp \
  49.121 +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  49.122 +
  49.123 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  49.124 +>  out3f.tmp
  49.125 +
  49.126 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out3f.tmp" \
  49.127 +&& echo "Success" \
  49.128 +|| echo "Failed"
  49.129 +
  49.130 +  "$ACCEPT_SCRIPT" $ACCEPT_ARGS "$USER2" "event26@example.com" 2>> $ERROR \
  49.131 +| tee out32.tmp \
  49.132 +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  49.133 +
  49.134 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  49.135 +>  out3f2.tmp
  49.136 +
  49.137 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out3f2.tmp" \
  49.138 +&& echo "Success" \
  49.139 +|| echo "Failed"
  49.140 +
  49.141 +# Test registration in the incoming handler.
  49.142 +
  49.143 +  "$PERSON_SCRIPT" $ARGS < out3.tmp 2>> $ERROR \
  49.144 +| "$SHOWMAIL" \
  49.145 +> out4.tmp
  49.146 +
  49.147 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  49.148 +>  out4f.tmp
  49.149 +
  49.150 +   [ `grep "event26@example.com" "out4f.tmp" | wc -l` = '2' ] \
  49.151 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4f.tmp" \
  49.152 +&& echo "Success" \
  49.153 +|| echo "Failed"
  49.154 +
  49.155 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy_other" "$SENDER" \
  49.156 +>  out4o.tmp
  49.157 +
  49.158 +   [ `grep "event26@example.com" "out4o.tmp" | wc -l` = '2' ] \
  49.159 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4o.tmp" \
  49.160 +&& echo "Success" \
  49.161 +|| echo "Failed"
  49.162 +
  49.163 +  "$PERSON_SCRIPT" $ARGS < out32.tmp 2>> $ERROR \
  49.164 +| "$SHOWMAIL" \
  49.165 +> out42.tmp
  49.166 +
  49.167 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  49.168 +>  out4f2.tmp
  49.169 +
  49.170 +   [ `grep "event26@example.com" "out4f2.tmp" | wc -l` = '2' ] \
  49.171 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4f2.tmp" \
  49.172 +&& echo "Success" \
  49.173 +|| echo "Failed"
  49.174 +
  49.175 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy_other" "$SENDER" \
  49.176 +>  out4o2.tmp
  49.177 +
  49.178 +   [ `grep "event26@example.com" "out4o2.tmp" | wc -l` = '2' ] \
  49.179 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4o2.tmp" \
  49.180 +&& echo "Success" \
  49.181 +|| echo "Failed"
  49.182 +
  49.183 +# Test recurrence declining in the outgoing handler.
  49.184 +# Only the first user declines.
  49.185 +
  49.186 +  "$DECLINE_SCRIPT" $DECLINE_ARGS "$USER1" "20141011T100000" "20141011T110000" "event26@example.com" "20141011T100000" 2>> $ERROR \
  49.187 +| tee out5.tmp \
  49.188 +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  49.189 +
  49.190 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  49.191 +>  out5s.tmp
  49.192 +
  49.193 +   [ `grep "event26@example.com" "out5s.tmp" | wc -l` = '1' ] \
  49.194 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out5s.tmp" \
  49.195 +&& echo "Success" \
  49.196 +|| echo "Failed"
  49.197 +
  49.198 +# There should be a recurrence created by the user.
  49.199 +
  49.200 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "object" "event26@example.com" \
  49.201 +>  out5O.tmp
  49.202 +
  49.203 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "recurrence" "event26@example.com" "20141011T100000" \
  49.204 +>  out5R.tmp
  49.205 +
  49.206 +   grep -q 'event26@example.com' "out5O.tmp" \
  49.207 +&& grep -q 'event26@example.com' "out5R.tmp" \
  49.208 +&& echo "Success" \
  49.209 +|| echo "Failed"
  49.210 +
  49.211 +# Test declining in the incoming handler.
  49.212 +
  49.213 +  "$PERSON_SCRIPT" $ARGS < out5.tmp 2>> $ERROR \
  49.214 +| tee out6r.tmp \
  49.215 +| "$SHOWMAIL" \
  49.216 +> out6.tmp
  49.217 +
  49.218 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  49.219 +>  out6f.tmp
  49.220 +
  49.221 +   [ `grep "event26@example.com" "out6f.tmp" | wc -l` = '2' ] \
  49.222 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6f.tmp" \
  49.223 +&& echo "Success" \
  49.224 +|| echo "Failed"
  49.225 +
  49.226 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER1" \
  49.227 +>  out6o.tmp
  49.228 +
  49.229 +   [ `grep "event26@example.com" "out6o.tmp" | wc -l` = '1' ] \
  49.230 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6o.tmp" \
  49.231 +&& echo "Success" \
  49.232 +|| echo "Failed"
  49.233 +
  49.234 +# The second user is still attending the original event.
  49.235 +
  49.236 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER2" \
  49.237 +>  out6o2.tmp
  49.238 +
  49.239 +   [ `grep "event26@example.com" "out6o2.tmp" | wc -l` = '2' ] \
  49.240 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6o2.tmp" \
  49.241 +&& echo "Success" \
  49.242 +|| echo "Failed"
  49.243 +
  49.244 +# There should be a recurrence created by the user.
  49.245 +
  49.246 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event26@example.com" \
  49.247 +>  out6O.tmp
  49.248 +
  49.249 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "recurrence" "event26@example.com" "20141011T100000" \
  49.250 +>  out6R.tmp
  49.251 +
  49.252 +   grep -q 'event26@example.com' "out6O.tmp" \
  49.253 +&& grep -q 'event26@example.com' "out6R.tmp" \
  49.254 +&& echo "Success" \
  49.255 +|| echo "Failed"
  49.256 +
  49.257 +# This should cause the organiser to tell the second user about the recurrence.
  49.258 +
  49.259 +   grep -q 'METHOD:REQUEST' out6.tmp \
  49.260 +&& echo "Success" \
  49.261 +|| echo "Failed"
  49.262 +
  49.263 +  "$PERSON_SCRIPT" $ARGS < out6r.tmp 2>> $ERROR \
  49.264 +| "$SHOWMAIL" \
  49.265 +> out62.tmp
  49.266 +
  49.267 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "object" "event26@example.com" \
  49.268 +>  out6O2.tmp
  49.269 +
  49.270 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "recurrence" "event26@example.com" "20141011T100000" \
  49.271 +>  out6R2.tmp
  49.272 +
  49.273 +# The second user's schedule should remain unchanged.
  49.274 +# NOTE: The nature of the periods might need to change, with the recurrence
  49.275 +# NOTE: taking over the affected period.
  49.276 +
  49.277 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  49.278 +>  out6f2.tmp
  49.279 +
  49.280 +   [ `grep "event26@example.com" "out6f2.tmp" | wc -l` = '2' ] \
  49.281 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6f2.tmp" \
  49.282 +&& echo "Success" \
  49.283 +|| echo "Failed"
  49.284 +
  49.285 +# Test recurrence acceptance in the outgoing handler.
  49.286 +
  49.287 +  "$ACCEPT_SCRIPT" $ACCEPT_ARGS "$USER1" "20141011T100000" "20141011T110000" "event26@example.com" "20141011T100000" 2>> $ERROR \
  49.288 +| tee out7.tmp \
  49.289 +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  49.290 +
  49.291 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  49.292 +>  out7s.tmp
  49.293 +
  49.294 +   [ `grep "event26@example.com" "out7s.tmp" | wc -l` = '2' ] \
  49.295 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out7s.tmp" \
  49.296 +&& echo "Success" \
  49.297 +|| echo "Failed"
  49.298 +
  49.299 +# There should still be a recurrence created by the user.
  49.300 +
  49.301 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "object" "event26@example.com" \
  49.302 +>  out7O.tmp
  49.303 +
  49.304 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "recurrence" "event26@example.com" "20141011T100000" \
  49.305 +>  out7R.tmp
  49.306 +
  49.307 +   grep -q 'event26@example.com' "out7O.tmp" \
  49.308 +&& grep -q 'event26@example.com' "out7R.tmp" \
  49.309 +&& echo "Success" \
  49.310 +|| echo "Failed"
  49.311 +
  49.312 +# Test acceptance in the incoming handler.
  49.313 +
  49.314 +  "$PERSON_SCRIPT" $ARGS < out7.tmp 2>> $ERROR \
  49.315 +| "$SHOWMAIL" \
  49.316 +> out8.tmp
  49.317 +
  49.318 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  49.319 +>  out8f.tmp
  49.320 +
  49.321 +   [ `grep "event26@example.com" "out8f.tmp" | wc -l` = '2' ] \
  49.322 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out8f.tmp" \
  49.323 +&& echo "Success" \
  49.324 +|| echo "Failed"
  49.325 +
  49.326 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER1" \
  49.327 +>  out8o.tmp
  49.328 +
  49.329 +   [ `grep "event26@example.com" "out8o.tmp" | wc -l` = '2' ] \
  49.330 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out8o.tmp" \
  49.331 +&& echo "Success" \
  49.332 +|| echo "Failed"
  49.333 +
  49.334 +# The second user should not have been affected.
  49.335 +
  49.336 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER2" \
  49.337 +>  out8o2.tmp
  49.338 +
  49.339 +   [ `grep "event26@example.com" "out8o2.tmp" | wc -l` = '2' ] \
  49.340 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out8o2.tmp" \
  49.341 +&& echo "Success" \
  49.342 +|| echo "Failed"
  49.343 +
  49.344 +# There should be a recurrence created by the user.
  49.345 +
  49.346 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event26@example.com" \
  49.347 +>  out8O.tmp
  49.348 +
  49.349 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "recurrence" "event26@example.com" "20141011T100000" \
  49.350 +>  out8R.tmp
  49.351 +
  49.352 +   grep -q 'event26@example.com' "out8O.tmp" \
  49.353 +&& grep -q 'event26@example.com' "out8R.tmp" \
  49.354 +&& echo "Success" \
  49.355 +|| echo "Failed"
  49.356 +
  49.357 +# Test recurrence declining in the outgoing handler.
  49.358 +# Now the second user declines the parent event.
  49.359 +
  49.360 +  "$DECLINE_SCRIPT" $DECLINE_ARGS "$USER2" "event26@example.com" 2>> $ERROR \
  49.361 +| tee out9.tmp \
  49.362 +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  49.363 +
  49.364 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  49.365 +>  out9s.tmp
  49.366 +
  49.367 +   [ `grep "event26@example.com" "out9s.tmp" | wc -l` = '0' ] \
  49.368 +&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out9s.tmp" \
  49.369 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out9s.tmp" \
  49.370 +&& echo "Success" \
  49.371 +|| echo "Failed"
  49.372 +
  49.373 +# Test declining in the incoming handler.
  49.374 +
  49.375 +  "$PERSON_SCRIPT" $ARGS < out9.tmp 2>> $ERROR \
  49.376 +| "$SHOWMAIL" \
  49.377 +> out10.tmp
  49.378 +
  49.379 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  49.380 +>  out10f.tmp
  49.381 +
  49.382 +   [ `grep "event26@example.com" "out10f.tmp" | wc -l` = '2' ] \
  49.383 +&& grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out10f.tmp" \
  49.384 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out10f.tmp" \
  49.385 +&& echo "Success" \
  49.386 +|| echo "Failed"
  49.387 +
  49.388 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER2" \
  49.389 +>  out10o.tmp
  49.390 +
  49.391 +   [ `grep "event26@example.com" "out10o.tmp" | wc -l` = '1' ] \
  49.392 +&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out10o.tmp" \
  49.393 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out10o.tmp" \
  49.394 +&& echo "Success" \
  49.395 +|| echo "Failed"
    50.1 --- a/tests/test_person_invitation_recurring.sh	Tue Apr 19 21:20:57 2016 +0200
    50.2 +++ b/tests/test_person_invitation_recurring.sh	Fri Apr 22 16:22:58 2016 +0200
    50.3 @@ -4,16 +4,13 @@
    50.4  
    50.5  USER="mailto:vincent.vole@example.com"
    50.6  SENDER="mailto:paul.boddie@example.com"
    50.7 -FBFILE="$STORE/$USER/freebusy"
    50.8 -FBOTHERFILE="$STORE/$USER/freebusy-other/$SENDER"
    50.9 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   50.10  
   50.11  mkdir -p "$PREFS/$USER"
   50.12  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   50.13  echo 'share' > "$PREFS/$USER/freebusy_sharing"
   50.14  
   50.15  mkdir -p "$PREFS/$SENDER"
   50.16 -echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   50.17 +echo 'Europe/Oslo' > "$PREFS/$SENDER/TZID"
   50.18  
   50.19  # Test free/busy responses.
   50.20  
   50.21 @@ -39,7 +36,10 @@
   50.22  
   50.23  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring.txt" 2>> $ERROR
   50.24  
   50.25 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDERFILE" \
   50.26 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   50.27 +>  out1f.tmp
   50.28 +
   50.29 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out1f.tmp" \
   50.30  && echo "Success" \
   50.31  || echo "Failed"
   50.32  
   50.33 @@ -53,12 +53,17 @@
   50.34  && echo "Success" \
   50.35  || echo "Failed"
   50.36  
   50.37 -   ! [ -e "$FBFILE" ] \
   50.38 -|| ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   50.39 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   50.40 +>  out2f.tmp
   50.41 +
   50.42 +   ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out2f.tmp" \
   50.43  && echo "Success" \
   50.44  || echo "Failed"
   50.45  
   50.46 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBOTHERFILE" \
   50.47 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   50.48 +>  out2o.tmp
   50.49 +
   50.50 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out2o.tmp" \
   50.51  && echo "Success" \
   50.52  || echo "Failed"
   50.53  
   50.54 @@ -68,7 +73,10 @@
   50.55  | tee out3.tmp \
   50.56  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
   50.57  
   50.58 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   50.59 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   50.60 +>  out3f.tmp
   50.61 +
   50.62 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out3f.tmp" \
   50.63  && echo "Success" \
   50.64  || echo "Failed"
   50.65  
   50.66 @@ -76,8 +84,11 @@
   50.67  
   50.68  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-person-recurring-instance.txt" 2>> $ERROR
   50.69  
   50.70 -   [ `grep "event8@example.com" "$FBSENDERFILE" | wc -l` = '2' ] \
   50.71 -&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "$FBSENDERFILE" \
   50.72 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   50.73 +>  out3s.tmp
   50.74 +
   50.75 +   [ `grep "event8@example.com" "out3s.tmp" | wc -l` = '2' ] \
   50.76 +&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "out3s.tmp" \
   50.77  && echo "Success" \
   50.78  || echo "Failed"
   50.79  
   50.80 @@ -91,13 +102,19 @@
   50.81  && echo "Success" \
   50.82  || echo "Failed"
   50.83  
   50.84 -   [ `grep "event8@example.com" "$FBFILE" | wc -l` = '2' ] \
   50.85 -&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "$FBFILE" \
   50.86 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   50.87 +>  out4f.tmp
   50.88 +
   50.89 +   [ `grep "event8@example.com" "out4f.tmp" | wc -l` = '2' ] \
   50.90 +&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "out4f.tmp" \
   50.91  && echo "Success" \
   50.92  || echo "Failed"
   50.93  
   50.94 -   [ `grep "event8@example.com" "$FBOTHERFILE" | wc -l` = '2' ] \
   50.95 -&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "$FBOTHERFILE" \
   50.96 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   50.97 +>  out4o.tmp
   50.98 +
   50.99 +   [ `grep "event8@example.com" "out4o.tmp" | wc -l` = '2' ] \
  50.100 +&& ! grep -q "^20141114T090000Z${TAB}20141114T100000Z" "out4o.tmp" \
  50.101  && echo "Success" \
  50.102  || echo "Failed"
  50.103  
  50.104 @@ -105,8 +122,11 @@
  50.105  
  50.106  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-reschedule-instance.txt" 2>> $ERROR
  50.107  
  50.108 -   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBSENDERFILE" \
  50.109 -&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBSENDERFILE" \
  50.110 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  50.111 +>  out4s.tmp
  50.112 +
  50.113 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4s.tmp" \
  50.114 +&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out4s.tmp" \
  50.115  && echo "Success" \
  50.116  || echo "Failed"
  50.117  
  50.118 @@ -120,13 +140,19 @@
  50.119  && echo "Success" \
  50.120  || echo "Failed"
  50.121  
  50.122 -   ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBFILE" \
  50.123 -&& grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBFILE" \
  50.124 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.125 +>  out5f.tmp
  50.126 +
  50.127 +   ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out5f.tmp" \
  50.128 +&& grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out5f.tmp" \
  50.129  && echo "Success" \
  50.130  || echo "Failed"
  50.131  
  50.132 -   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBOTHERFILE" \
  50.133 -&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBOTHERFILE" \
  50.134 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  50.135 +>  out5o.tmp
  50.136 +
  50.137 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out5o.tmp" \
  50.138 +&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out5o.tmp" \
  50.139  && echo "Success" \
  50.140  || echo "Failed"
  50.141  
  50.142 @@ -136,8 +162,11 @@
  50.143  | tee out6.tmp \
  50.144  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  50.145  
  50.146 -   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBFILE" \
  50.147 -&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBFILE" \
  50.148 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.149 +>  out6f.tmp
  50.150 +
  50.151 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6f.tmp" \
  50.152 +&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out6f.tmp" \
  50.153  && echo "Success" \
  50.154  || echo "Failed"
  50.155  
  50.156 @@ -145,7 +174,10 @@
  50.157  
  50.158  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-person-recurring.txt" 2>> $ERROR
  50.159  
  50.160 -   ! grep -q "event8@example.com" "$FBSENDERFILE" \
  50.161 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  50.162 +>  out6s.tmp
  50.163 +
  50.164 +   ! grep -q "event8@example.com" "out6s.tmp" \
  50.165  && echo "Success" \
  50.166  || echo "Failed"
  50.167  
  50.168 @@ -153,17 +185,23 @@
  50.169  
  50.170    "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-person-recurring.txt" 2>> $ERROR \
  50.171  | "$SHOWMAIL" \
  50.172 -> out6.tmp
  50.173 +> out7.tmp
  50.174  
  50.175 -   ! grep -q 'METHOD:REPLY' out6.tmp \
  50.176 +   ! grep -q 'METHOD:REPLY' out7.tmp \
  50.177  && echo "Success" \
  50.178  || echo "Failed"
  50.179  
  50.180 -   ! grep -q "event8@example.com" "$FBFILE" \
  50.181 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.182 +>  out7f.tmp
  50.183 +
  50.184 +   ! grep -q "event8@example.com" "out7f.tmp" \
  50.185  && echo "Success" \
  50.186  || echo "Failed"
  50.187  
  50.188 -   ! grep -q "event8@example.com" "$FBOTHERFILE" \
  50.189 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  50.190 +>  out7o.tmp
  50.191 +
  50.192 +   ! grep -q "event8@example.com" "out7o.tmp" \
  50.193  && echo "Success" \
  50.194  || echo "Failed"
  50.195  
  50.196 @@ -173,53 +211,74 @@
  50.197  
  50.198  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring.txt" 2>> $ERROR
  50.199  
  50.200 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDERFILE" \
  50.201 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  50.202 +>  out7s.tmp
  50.203 +
  50.204 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out7s.tmp" \
  50.205  && echo "Success" \
  50.206  || echo "Failed"
  50.207  
  50.208    "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring.txt" 2>> $ERROR \
  50.209  | "$SHOWMAIL" \
  50.210 -> out7.tmp
  50.211 +> out8.tmp
  50.212  
  50.213 -   ! grep -q 'METHOD:REPLY' out7.tmp \
  50.214 +   ! grep -q 'METHOD:REPLY' out8.tmp \
  50.215  && echo "Success" \
  50.216  || echo "Failed"
  50.217  
  50.218 -   ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
  50.219 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.220 +>  out8f.tmp
  50.221 +
  50.222 +   ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out8f.tmp" \
  50.223  && echo "Success" \
  50.224  || echo "Failed"
  50.225  
  50.226 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBOTHERFILE" \
  50.227 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  50.228 +>  out8o.tmp
  50.229 +
  50.230 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out8o.tmp" \
  50.231  && echo "Success" \
  50.232  || echo "Failed"
  50.233  
  50.234    "$ACCEPT_SCRIPT" $ACCEPT_ARGS "$USER" "event8@example.com" 2>> $ERROR \
  50.235 -| tee out8.tmp \
  50.236 +| tee out9.tmp \
  50.237  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  50.238  
  50.239 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
  50.240 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.241 +>  out9f.tmp
  50.242 +
  50.243 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out9f.tmp" \
  50.244  && echo "Success" \
  50.245  || echo "Failed"
  50.246  
  50.247  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-person-recurring.txt" 2>> $ERROR
  50.248  
  50.249 -   ! grep -q "event8@example.com" "$FBSENDERFILE" \
  50.250 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  50.251 +>  out9s.tmp
  50.252 +
  50.253 +   ! grep -q "event8@example.com" "out9s.tmp" \
  50.254  && echo "Success" \
  50.255  || echo "Failed"
  50.256  
  50.257    "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-person-recurring.txt" 2>> $ERROR \
  50.258  | "$SHOWMAIL" \
  50.259 -> out9.tmp
  50.260 +> out10.tmp
  50.261  
  50.262 -   ! grep -q 'METHOD:REPLY' out9.tmp \
  50.263 +   ! grep -q 'METHOD:REPLY' out10.tmp \
  50.264  && echo "Success" \
  50.265  || echo "Failed"
  50.266  
  50.267 -   ! grep -q "event8@example.com" "$FBFILE" \
  50.268 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.269 +>  out10f.tmp
  50.270 +
  50.271 +   ! grep -q "event8@example.com" "out10f.tmp" \
  50.272  && echo "Success" \
  50.273  || echo "Failed"
  50.274  
  50.275 -   ! grep -q "event8@example.com" "$FBOTHERFILE" \
  50.276 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  50.277 +>  out10o.tmp
  50.278 +
  50.279 +   ! grep -q "event8@example.com" "out10o.tmp" \
  50.280  && echo "Success" \
  50.281  || echo "Failed"
  50.282  
  50.283 @@ -228,32 +287,44 @@
  50.284  
  50.285  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-day-floating.txt" 2>> $ERROR
  50.286  
  50.287 -   grep -q "^20141211T230000Z${TAB}20141212T230000Z" "$FBSENDERFILE" \
  50.288 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  50.289 +>  out10s.tmp
  50.290 +
  50.291 +   grep -q "^20141211T230000Z${TAB}20141212T230000Z" "out10s.tmp" \
  50.292  && echo "Success" \
  50.293  || echo "Failed"
  50.294  
  50.295    "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-day-floating.txt" 2>> $ERROR \
  50.296  | "$SHOWMAIL" \
  50.297 -> out10.tmp
  50.298 +> out11.tmp
  50.299  
  50.300 -   ! grep -q 'METHOD:REPLY' out10.tmp \
  50.301 +   ! grep -q 'METHOD:REPLY' out11.tmp \
  50.302  && echo "Success" \
  50.303  || echo "Failed"
  50.304  
  50.305 -   ! grep -q "^20141211T220000Z${TAB}20141212T220000Z" "$FBFILE" \
  50.306 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.307 +>  out11f.tmp
  50.308 +
  50.309 +   ! grep -q "^20141211T220000Z${TAB}20141212T220000Z" "out11f.tmp" \
  50.310  && echo "Success" \
  50.311  || echo "Failed"
  50.312  
  50.313  # (The organiser is not attending.)
  50.314  
  50.315 -   ! grep -q "^20141211T220000Z${TAB}20141212T220000Z" "$FBOTHERFILE" \
  50.316 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  50.317 +>  out11o.tmp
  50.318 +
  50.319 +   ! grep -q "^20141211T220000Z${TAB}20141212T220000Z" "out11o.tmp" \
  50.320  && echo "Success" \
  50.321  || echo "Failed"
  50.322  
  50.323    "$ACCEPT_SCRIPT" $ACCEPT_ARGS "$USER" "event12@example.com" 2>> $ERROR \
  50.324 -| tee out11.tmp \
  50.325 +| tee out12.tmp \
  50.326  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  50.327  
  50.328 -   grep -q "^20141211T220000Z${TAB}20141212T220000Z" "$FBFILE" \
  50.329 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  50.330 +>  out12f.tmp
  50.331 +
  50.332 +   grep -q "^20141211T220000Z${TAB}20141212T220000Z" "out12f.tmp" \
  50.333  && echo "Success" \
  50.334  || echo "Failed"
    51.1 --- a/tests/test_person_invitation_refresh.sh	Tue Apr 19 21:20:57 2016 +0200
    51.2 +++ b/tests/test_person_invitation_refresh.sh	Fri Apr 22 16:22:58 2016 +0200
    51.3 @@ -4,9 +4,6 @@
    51.4  
    51.5  USER="mailto:vincent.vole@example.com"
    51.6  SENDER="mailto:paul.boddie@example.com"
    51.7 -FBFILE="$STORE/$USER/freebusy"
    51.8 -FBOTHERFILE="$STORE/$USER/freebusy-other/$SENDER"
    51.9 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   51.10  
   51.11  mkdir -p "$PREFS/$USER"
   51.12  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   51.13 @@ -21,7 +18,10 @@
   51.14  
   51.15  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring.txt" 2>> $ERROR
   51.16  
   51.17 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDERFILE" \
   51.18 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   51.19 +>  out1f.tmp
   51.20 +
   51.21 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out1f.tmp" \
   51.22  && echo "Success" \
   51.23  || echo "Failed"
   51.24  
   51.25 @@ -45,12 +45,17 @@
   51.26  && echo "Success" \
   51.27  || echo "Failed"
   51.28  
   51.29 -   ! [ -e "$FBFILE" ] \
   51.30 -|| ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   51.31 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   51.32 +>  out3f.tmp
   51.33 +
   51.34 +   ! grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out3f.tmp" \
   51.35  && echo "Success" \
   51.36  || echo "Failed"
   51.37  
   51.38 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBOTHERFILE" \
   51.39 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   51.40 +>  out3f.tmp
   51.41 +
   51.42 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out3f.tmp" \
   51.43  && echo "Success" \
   51.44  || echo "Failed"
   51.45  
   51.46 @@ -60,7 +65,10 @@
   51.47  | tee out4.tmp \
   51.48  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
   51.49  
   51.50 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   51.51 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   51.52 +>  out4f.tmp
   51.53 +
   51.54 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out4f.tmp" \
   51.55  && echo "Success" \
   51.56  || echo "Failed"
   51.57  
   51.58 @@ -78,8 +86,11 @@
   51.59  
   51.60  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-reschedule-instance.txt" 2>> $ERROR
   51.61  
   51.62 -   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBSENDERFILE" \
   51.63 -&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBSENDERFILE" \
   51.64 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   51.65 +>  out5f.tmp
   51.66 +
   51.67 +   grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out5f.tmp" \
   51.68 +&& ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out5f.tmp" \
   51.69  && echo "Success" \
   51.70  || echo "Failed"
   51.71  
   51.72 @@ -102,8 +113,14 @@
   51.73  | "$SHOWMAIL" \
   51.74  > out6a.tmp
   51.75  
   51.76 -   [ -e "$STORE/$USER/objects/event8@example.com" ] \
   51.77 -&& [ -e "$STORE/$USER/recurrences/event8@example.com/20141010T080000Z" ] \
   51.78 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event8@example.com" \
   51.79 +>  out6O.tmp
   51.80 +
   51.81 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "recurrence" "event8@example.com" "20141010T080000Z" \
   51.82 +>  out6R.tmp
   51.83 +
   51.84 +   grep -q 'event8@example.com' "out6O.tmp" \
   51.85 +&& grep -q 'event8@example.com' "out6R.tmp" \
   51.86  && echo "Success" \
   51.87  || echo "Failed"
   51.88  
   51.89 @@ -112,14 +129,26 @@
   51.90  
   51.91  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-person-recurring-rescheduled-instance.txt" 2>> $ERROR
   51.92  
   51.93 -   [ -e "$STORE/$SENDER/objects/event8@example.com" ] \
   51.94 -&& ! [ -e "$STORE/$SENDER/recurrences/event8@example.com/20141010T080000Z" ] \
   51.95 -&& [ -e "$STORE/$SENDER/cancellations/recurrences/event8@example.com/20141010T080000Z" ] \
   51.96 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event8@example.com" \
   51.97 +>  out6O2.tmp
   51.98 +
   51.99 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "recurrence" "event8@example.com" "20141010T080000Z" \
  51.100 +>  out6R2.tmp
  51.101 +
  51.102 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "cancelled_recurrences" "event8@example.com" \
  51.103 +>  out6C.tmp
  51.104 +
  51.105 +   grep -q 'event8@example.com' "out6O2.tmp" \
  51.106 +&& ! grep -q 'event8@example.com' "out6R2.tmp" \
  51.107 +&& grep -q '20141010T080000Z' "out6C.tmp" \
  51.108  && echo "Success" \
  51.109  || echo "Failed"
  51.110  
  51.111 -   ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBSENDERFILE" \
  51.112 -&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBSENDERFILE" \
  51.113 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  51.114 +>  out6f.tmp
  51.115 +
  51.116 +   ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out6f.tmp" \
  51.117 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6f.tmp" \
  51.118  && echo "Success" \
  51.119  || echo "Failed"
  51.120  
  51.121 @@ -143,9 +172,18 @@
  51.122  | "$SHOWMAIL" \
  51.123  > out7a.tmp
  51.124  
  51.125 -   [ -e "$STORE/$USER/objects/event8@example.com" ] \
  51.126 -&& ! [ -e "$STORE/$USER/recurrences/event8@example.com/20141010T080000Z" ] \
  51.127 -&& [ -e "$STORE/$USER/cancellations/recurrences/event8@example.com/20141010T080000Z" ] \
  51.128 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "object" "event8@example.com" \
  51.129 +>  out7O.tmp
  51.130 +
  51.131 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "recurrence" "event8@example.com" "20141010T080000Z" \
  51.132 +>  out7R.tmp
  51.133 +
  51.134 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "cancelled_recurrences" "event8@example.com" \
  51.135 +>  out7C.tmp
  51.136 +
  51.137 +   grep -q 'event8@example.com' "out7O.tmp" \
  51.138 +&& ! grep -q 'event8@example.com' "out7R.tmp" \
  51.139 +&& grep -q '20141010T080000Z' "out7C.tmp" \
  51.140  && echo "Success" \
  51.141  || echo "Failed"
  51.142  
  51.143 @@ -153,8 +191,11 @@
  51.144  
  51.145  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-add-person-recurring-rescheduled-instance.txt" 2>> $ERROR
  51.146  
  51.147 -   grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBSENDERFILE" \
  51.148 -&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBSENDERFILE" \
  51.149 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  51.150 +>  out7f.tmp
  51.151 +
  51.152 +   grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out7f.tmp" \
  51.153 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out7f.tmp" \
  51.154  && echo "Success" \
  51.155  || echo "Failed"
  51.156  
  51.157 @@ -162,17 +203,32 @@
  51.158  | "$SHOWMAIL" \
  51.159  > out8.tmp
  51.160  
  51.161 -   [ -e "$STORE/$USER/objects/event8@example.com" ] \
  51.162 -&& [ -e "$STORE/$USER/recurrences/event8@example.com/20141010T080000Z" ] \
  51.163 -&& ! [ -e "$STORE/$USER/cancellations/recurrences/event8@example.com/20141010T080000Z" ] \
  51.164 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "object" "event8@example.com" \
  51.165 +>  out8O.tmp
  51.166 +
  51.167 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "recurrence" "event8@example.com" "20141010T080000Z" \
  51.168 +>  out8R.tmp
  51.169 +
  51.170 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "cancelled_recurrences" "event8@example.com" \
  51.171 +>  out8C.tmp
  51.172 +
  51.173 +   grep -q 'event8@example.com' "out8O.tmp" \
  51.174 +&& grep -q 'event8@example.com' "out8R.tmp" \
  51.175 +&& ! grep -q '20141010T080000Z' "out8C.tmp" \
  51.176  && echo "Success" \
  51.177  || echo "Failed"
  51.178  
  51.179 -   ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBFILE" \
  51.180 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  51.181 +>  out8f.tmp
  51.182 +
  51.183 +   ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out8f.tmp" \
  51.184  && echo "Success" \
  51.185  || echo "Failed"
  51.186  
  51.187 -   grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBOTHERFILE" \
  51.188 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
  51.189 +>  out8f.tmp
  51.190 +
  51.191 +   grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out8f.tmp" \
  51.192  && echo "Success" \
  51.193  || echo "Failed"
  51.194  
  51.195 @@ -182,7 +238,10 @@
  51.196  | tee out9.tmp \
  51.197  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  51.198  
  51.199 -   grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBFILE" \
  51.200 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  51.201 +>  out9f.tmp
  51.202 +
  51.203 +   grep -q "^20141010T080000Z${TAB}20141010T090000Z" "out9f.tmp" \
  51.204  && echo "Success" \
  51.205  || echo "Failed"
  51.206  
  51.207 @@ -205,7 +264,13 @@
  51.208  | "$SHOWMAIL" \
  51.209  > out11.tmp
  51.210  
  51.211 -   [ -e "$STORE/$USER/objects/event8@example.com" ] \
  51.212 -&& [ -e "$STORE/$USER/recurrences/event8@example.com/20141010T080000Z" ] \
  51.213 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "object" "event8@example.com" \
  51.214 +>  out11O.tmp
  51.215 +
  51.216 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "recurrence" "event8@example.com" "20141010T080000Z" \
  51.217 +>  out11R.tmp
  51.218 +
  51.219 +   grep -q 'event8@example.com' "out11O.tmp" \
  51.220 +&& grep -q 'event8@example.com' "out11R.tmp" \
  51.221  && echo "Success" \
  51.222  || echo "Failed"
    52.1 --- a/tests/test_person_non_participation.sh	Tue Apr 19 21:20:57 2016 +0200
    52.2 +++ b/tests/test_person_non_participation.sh	Fri Apr 22 16:22:58 2016 +0200
    52.3 @@ -5,9 +5,6 @@
    52.4  USER="mailto:vincent.vole@example.com"
    52.5  IMPOSTER="mailto:oliver.otter@example.com"
    52.6  SENDER="mailto:paul.boddie@example.com"
    52.7 -FBFILE="$STORE/$USER/freebusy"
    52.8 -FBOTHERFILE="$STORE/$USER/freebusy-other/$SENDER"
    52.9 -FBIMPOSTERFILE="$STORE/$SENDER/freebusy-other/$IMPOSTER"
   52.10  
   52.11  mkdir -p "$PREFS/$USER"
   52.12  echo 'no' > "$PREFS/$USER/participating"
   52.13 @@ -27,7 +24,10 @@
   52.14  
   52.15  "$OUTGOING_SCRIPT" < "$TEMPLATES/event-request-person.txt" $ARGS 2>> $ERROR
   52.16  
   52.17 -   [ -e "$STORE/$SENDER/objects/event6@example.com" ] \
   52.18 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event6@example.com" \
   52.19 +>  out0O.tmp
   52.20 +
   52.21 +   grep -q "event6@example.com" "out0O.tmp" \
   52.22  && echo "Success" \
   52.23  || echo "Failed"
   52.24  
   52.25 @@ -41,11 +41,17 @@
   52.26  && echo "Success" \
   52.27  || echo "Failed"
   52.28  
   52.29 -   ! [ -e "$FBFILE" ] \
   52.30 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   52.31 +>  out2f.tmp
   52.32 +
   52.33 +   ! grep -q "event6@example.com" "out2f.tmp" \
   52.34  && echo "Success" \
   52.35  || echo "Failed"
   52.36  
   52.37 -   ! [ -e "$FBOTHERFILE" ] \
   52.38 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   52.39 +>  out2s.tmp
   52.40 +
   52.41 +   ! grep -q "event6@example.com" "out2s.tmp" \
   52.42  && echo "Success" \
   52.43  || echo "Failed"
   52.44  
   52.45 @@ -55,8 +61,10 @@
   52.46  | tee out3.tmp \
   52.47  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
   52.48  
   52.49 -   ! [ -e "$FBFILE" ] \
   52.50 -|| ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   52.51 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   52.52 +>  out2f.tmp
   52.53 +
   52.54 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \
   52.55  && echo "Success" \
   52.56  || echo "Failed"
   52.57  
   52.58 @@ -68,12 +76,16 @@
   52.59  | "$SHOWMAIL" \
   52.60  > out5.tmp
   52.61  
   52.62 -   [ -e "$STORE/$SENDER/objects/event6@example.com" ] \
   52.63 -&& ! grep -q "otter" "$STORE/$SENDER/objects/event6@example.com" \
   52.64 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "object" "event6@example.com" \
   52.65 +>  out5O.tmp
   52.66 +
   52.67 +   ! grep -q "otter" "out5O.tmp" \
   52.68  && echo "Success" \
   52.69  || echo "Failed"
   52.70  
   52.71 -   ( ! [ -e "$FBIMPOSTERFILE" ] \
   52.72 -  || ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBIMPOSTERFILE") \
   52.73 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$IMPOSTER" \
   52.74 +>  out5s.tmp
   52.75 +
   52.76 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out5s.tmp" \
   52.77  && echo "Success" \
   52.78  || echo "Failed"
    53.1 --- a/tests/test_resource_invitation_add.sh	Tue Apr 19 21:20:57 2016 +0200
    53.2 +++ b/tests/test_resource_invitation_add.sh	Fri Apr 22 16:22:58 2016 +0200
    53.3 @@ -4,9 +4,6 @@
    53.4  
    53.5  USER="mailto:resource-room-confroom@example.com"
    53.6  SENDER="mailto:paul.boddie@example.com"
    53.7 -FBFILE="$STORE/$USER/freebusy"
    53.8 -FBSENDERFILE="$STORE/$SENDER/freebusy"
    53.9 -FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
   53.10  
   53.11  mkdir -p "$PREFS/$USER"
   53.12  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   53.13 @@ -20,7 +17,9 @@
   53.14  
   53.15  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring.txt" 2>> $ERROR
   53.16  
   53.17 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDERFILE" \
   53.18 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   53.19 +|  tee out0f.tmp \
   53.20 +|  grep -q "^20141212T090000Z${TAB}20141212T100000Z" \
   53.21  && echo "Success" \
   53.22  || echo "Failed"
   53.23  
   53.24 @@ -34,7 +33,9 @@
   53.25  && echo "Success" \
   53.26  || echo "Failed"
   53.27  
   53.28 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   53.29 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   53.30 +| tee out1f.tmp \
   53.31 +| grep -q "^20141212T090000Z${TAB}20141212T100000Z" \
   53.32  && echo "Success" \
   53.33  || echo "Failed"
   53.34  
   53.35 @@ -44,7 +45,9 @@
   53.36  | "$SHOWMAIL" \
   53.37  > out2.tmp
   53.38  
   53.39 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDEROTHERFILE" \
   53.40 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \
   53.41 +|  tee out2o.tmp \
   53.42 +|  grep -q "^20141212T090000Z${TAB}20141212T100000Z" \
   53.43  && echo "Success" \
   53.44  || echo "Failed"
   53.45  
   53.46 @@ -52,8 +55,11 @@
   53.47  
   53.48  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-add-recurring.txt" 2>> $ERROR
   53.49  
   53.50 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDERFILE" \
   53.51 -&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBSENDERFILE" \
   53.52 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   53.53 +>  out2f.tmp
   53.54 +
   53.55 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out2f.tmp" \
   53.56 +&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out2f.tmp" \
   53.57  && echo "Success" \
   53.58  || echo "Failed"
   53.59  
   53.60 @@ -67,8 +73,11 @@
   53.61  && echo "Success" \
   53.62  || echo "Failed"
   53.63  
   53.64 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   53.65 -&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBFILE" \
   53.66 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   53.67 +>  out3f.tmp
   53.68 +
   53.69 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out3f.tmp" \
   53.70 +&& ! grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out3f.tmp" \
   53.71  && echo "Success" \
   53.72  || echo "Failed"
   53.73  
   53.74 @@ -94,8 +103,11 @@
   53.75  && echo "Success" \
   53.76  || echo "Failed"
   53.77  
   53.78 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBFILE" \
   53.79 -&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBFILE" \
   53.80 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   53.81 +>  out5f.tmp
   53.82 +
   53.83 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out5f.tmp" \
   53.84 +&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out5f.tmp" \
   53.85  && echo "Success" \
   53.86  || echo "Failed"
   53.87  
   53.88 @@ -105,7 +117,10 @@
   53.89  | "$SHOWMAIL" \
   53.90  > out6.tmp
   53.91  
   53.92 -   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "$FBSENDEROTHERFILE" \
   53.93 -&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "$FBSENDEROTHERFILE" \
   53.94 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \
   53.95 +>  out6o.tmp
   53.96 +
   53.97 +   grep -q "^20141212T090000Z${TAB}20141212T100000Z" "out6o.tmp" \
   53.98 +&& grep -q "^20150109T090000Z${TAB}20150109T100000Z" "out6o.tmp" \
   53.99  && echo "Success" \
  53.100  || echo "Failed"
    54.1 --- a/tests/test_resource_invitation_constraints.sh	Tue Apr 19 21:20:57 2016 +0200
    54.2 +++ b/tests/test_resource_invitation_constraints.sh	Fri Apr 22 16:22:58 2016 +0200
    54.3 @@ -5,12 +5,6 @@
    54.4  USER="mailto:resource-room-sauna@example.com"
    54.5  SENDER="mailto:paul.boddie@example.com"
    54.6  RIVALSENDER="mailto:vincent.vole@example.com"
    54.7 -FBFILE="$STORE/$USER/freebusy"
    54.8 -FBOFFERFILE="$STORE/$USER/freebusy-offers"
    54.9 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   54.10 -FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
   54.11 -FBSENDERREQUESTS="$STORE/$SENDER/requests"
   54.12 -FBRIVALSENDERFILE="$STORE/$RIVALSENDER/freebusy"
   54.13  
   54.14  mkdir -p "$PREFS/$USER"
   54.15  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   54.16 @@ -32,7 +26,9 @@
   54.17  
   54.18  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-bad.txt" 2>> $ERROR
   54.19  
   54.20 -   grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
   54.21 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   54.22 +|  tee out0f.tmp \
   54.23 +|  grep -q "^20141126T151000Z${TAB}20141126T154500Z" \
   54.24  && echo "Success" \
   54.25  || echo "Failed"
   54.26  
   54.27 @@ -48,12 +44,16 @@
   54.28  && echo "Success" \
   54.29  || echo "Failed"
   54.30  
   54.31 -   ! [ -e "$FBFILE" ] \
   54.32 -|| ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBFILE" \
   54.33 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   54.34 +>  out1f.tmp
   54.35 +
   54.36 +   ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out1f.tmp" \
   54.37  && echo "Success" \
   54.38  || echo "Failed"
   54.39  
   54.40 -   grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
   54.41 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
   54.42 +|  tee out1o.tmp \
   54.43 +|  grep -q "^20141126T151500Z${TAB}20141126T154500Z" \
   54.44  && echo "Success" \
   54.45  || echo "Failed"
   54.46  
   54.47 @@ -64,20 +64,28 @@
   54.48  | "$SHOWMAIL" \
   54.49  > out2.tmp
   54.50  
   54.51 -   ( ! [ -e "$FBSENDEROTHERFILE" ] \
   54.52 -  || ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBSENDEROTHERFILE") \
   54.53 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \
   54.54 +>  out2f.tmp
   54.55 +
   54.56 +   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out2f.tmp" \
   54.57  && echo "Success" \
   54.58  || echo "Failed"
   54.59  
   54.60 -   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161000' "$STORE/$SENDER/objects/event13@example.com" \
   54.61 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event13@example.com" \
   54.62 +|  tee out2O.tmp \
   54.63 +|  grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161000' \
   54.64  && echo "Success" \
   54.65  || echo "Failed"
   54.66  
   54.67 -   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161500' "$STORE/$SENDER/counters/objects/event13@example.com/$USER" \
   54.68 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "countered_object" "event13@example.com" "$USER" \
   54.69 +|  tee out2C.tmp \
   54.70 +|  grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161500' \
   54.71  && echo "Success" \
   54.72  || echo "Failed"
   54.73  
   54.74 -   grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
   54.75 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "requests" \
   54.76 +|  tee out2r.tmp \
   54.77 +|  grep -q 'event13@example.com' \
   54.78  && echo "Success" \
   54.79  || echo "Failed"
   54.80  
   54.81 @@ -85,8 +93,11 @@
   54.82  
   54.83  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-rival.txt" 2>> $ERROR
   54.84  
   54.85 -   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
   54.86 -&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
   54.87 +   "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "freebusy" \
   54.88 +>  out2R.tmp
   54.89 +
   54.90 +   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out2R.tmp" \
   54.91 +&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out2R.tmp" \
   54.92  && echo "Success" \
   54.93  || echo "Failed"
   54.94  
   54.95 @@ -108,12 +119,17 @@
   54.96  | "$SHOWMAIL" \
   54.97  > out4.tmp
   54.98  
   54.99 -   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
  54.100 -&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
  54.101 +   "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "freebusy" \
  54.102 +>  out4R.tmp
  54.103 +
  54.104 +   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out4R.tmp" \
  54.105 +&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out4R.tmp" \
  54.106  && echo "Success" \
  54.107  || echo "Failed"
  54.108  
  54.109 -   grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' "$STORE/$RIVALSENDER/objects/event18@example.com" \
  54.110 +   "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "object" "event18@example.com" \
  54.111 +|  tee out4O.tmp \
  54.112 +|  grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' \
  54.113  && echo "Success" \
  54.114  || echo "Failed"
  54.115  
  54.116 @@ -134,9 +150,12 @@
  54.117  
  54.118  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR
  54.119  
  54.120 -   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  54.121 -&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  54.122 -&& ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  54.123 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  54.124 +>  out5f.tmp
  54.125 +
  54.126 +   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out5f.tmp" \
  54.127 +&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out5f.tmp" \
  54.128 +&& ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out5f.tmp" \
  54.129  && echo "Success" \
  54.130  || echo "Failed"
  54.131  
  54.132 @@ -144,7 +163,10 @@
  54.133  && echo "Success" \
  54.134  || echo "Failed"
  54.135  
  54.136 -   ! grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
  54.137 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "requests" \
  54.138 +>  out5r.tmp
  54.139 +
  54.140 +   ! grep -q 'event13@example.com' "out5r.tmp" \
  54.141  && echo "Success" \
  54.142  || echo "Failed"
  54.143  
  54.144 @@ -159,13 +181,18 @@
  54.145  && echo "Success" \
  54.146  || echo "Failed"
  54.147  
  54.148 -   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBFILE" \
  54.149 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  54.150 +|  tee out6f.tmp \
  54.151 +|  grep -q "^20141126T150000Z${TAB}20141126T154500Z" \
  54.152  && echo "Success" \
  54.153  || echo "Failed"
  54.154  
  54.155 -   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
  54.156 -&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
  54.157 -&& ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
  54.158 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
  54.159 +>  out6o.tmp
  54.160 +
  54.161 +   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out6o.tmp" \
  54.162 +&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out6o.tmp" \
  54.163 +&& ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out6o.tmp" \
  54.164  && echo "Success" \
  54.165  || echo "Failed"
  54.166  
  54.167 @@ -176,10 +203,13 @@
  54.168  | "$SHOWMAIL" \
  54.169  > out7.tmp
  54.170  
  54.171 -   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  54.172 -&& ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  54.173 -&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  54.174 -&& ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  54.175 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  54.176 +>  out7f.tmp
  54.177 +
  54.178 +   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out7f.tmp" \
  54.179 +&& ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out7f.tmp" \
  54.180 +&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out7f.tmp" \
  54.181 +&& ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out7f.tmp" \
  54.182  && echo "Success" \
  54.183  || echo "Failed"
  54.184  
  54.185 @@ -187,7 +217,10 @@
  54.186  && echo "Success" \
  54.187  || echo "Failed"
  54.188  
  54.189 -   ! grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
  54.190 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "requests" \
  54.191 +>  out7r.tmp
  54.192 +
  54.193 +   ! grep -q 'event13@example.com' "out7r.tmp" \
  54.194  && echo "Success" \
  54.195  || echo "Failed"
  54.196  
    55.1 --- a/tests/test_resource_invitation_constraints_alternative.sh	Tue Apr 19 21:20:57 2016 +0200
    55.2 +++ b/tests/test_resource_invitation_constraints_alternative.sh	Fri Apr 22 16:22:58 2016 +0200
    55.3 @@ -5,12 +5,6 @@
    55.4  USER="mailto:resource-room-sauna@example.com"
    55.5  SENDER="mailto:paul.boddie@example.com"
    55.6  RIVALSENDER="mailto:vincent.vole@example.com"
    55.7 -FBFILE="$STORE/$USER/freebusy"
    55.8 -FBOFFERFILE="$STORE/$USER/freebusy-offers"
    55.9 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   55.10 -FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
   55.11 -FBSENDERREQUESTS="$STORE/$SENDER/requests"
   55.12 -FBRIVALSENDERFILE="$STORE/$RIVALSENDER/freebusy"
   55.13  
   55.14  mkdir -p "$PREFS/$USER"
   55.15  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   55.16 @@ -32,7 +26,9 @@
   55.17  
   55.18  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-bad.txt" 2>> $ERROR
   55.19  
   55.20 -   grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
   55.21 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   55.22 +|  tee out0f.tmp \
   55.23 +|  grep -q "^20141126T151000Z${TAB}20141126T154500Z" \
   55.24  && echo "Success" \
   55.25  || echo "Failed"
   55.26  
   55.27 @@ -47,12 +43,16 @@
   55.28  && echo "Success" \
   55.29  || echo "Failed"
   55.30  
   55.31 -   ! [ -e "$FBFILE" ] \
   55.32 -|| ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBFILE" \
   55.33 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   55.34 +>  out1f.tmp
   55.35 +
   55.36 +   ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out1f.tmp" \
   55.37  && echo "Success" \
   55.38  || echo "Failed"
   55.39  
   55.40 -   grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
   55.41 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
   55.42 +|  tee out1o.tmp \
   55.43 +|  grep -q "^20141126T151500Z${TAB}20141126T154500Z" \
   55.44  && echo "Success" \
   55.45  || echo "Failed"
   55.46  
   55.47 @@ -63,21 +63,29 @@
   55.48  | "$SHOWMAIL" \
   55.49  > out2.tmp
   55.50  
   55.51 -   [ ! -e "$FBSENDEROTHERFILE" ] \
   55.52 -|| ( ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBSENDEROTHERFILE" \
   55.53 -  && ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBSENDEROTHERFILE" ) \
   55.54 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \
   55.55 +>  out2f.tmp
   55.56 +
   55.57 +   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out2f.tmp" \
   55.58 +&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out2f.tmp"  \
   55.59  && echo "Success" \
   55.60  || echo "Failed"
   55.61  
   55.62 -   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161000' "$STORE/$SENDER/objects/event13@example.com" \
   55.63 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "object" "event13@example.com" \
   55.64 +|  tee out2O.tmp \
   55.65 +|  grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161000' \
   55.66  && echo "Success" \
   55.67  || echo "Failed"
   55.68  
   55.69 -   grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161500' "$STORE/$SENDER/counters/objects/event13@example.com/$USER" \
   55.70 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "countered_object" "event13@example.com" "$USER" \
   55.71 +|  tee out2C.tmp \
   55.72 +|  grep -q 'DTSTART;TZID=Europe/Oslo.*:20141126T161500' \
   55.73  && echo "Success" \
   55.74  || echo "Failed"
   55.75  
   55.76 -   grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
   55.77 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "requests" \
   55.78 +|  tee out2R.tmp \
   55.79 +|  grep -q 'event13@example.com' \
   55.80  && echo "Success" \
   55.81  || echo "Failed"
   55.82  
   55.83 @@ -94,12 +102,17 @@
   55.84  | "$SHOWMAIL" \
   55.85  > out4.tmp
   55.86  
   55.87 -   ! [ -e "$FBFILE" ] \
   55.88 -|| ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBFILE" \
   55.89 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   55.90 +>  out4f.tmp
   55.91 +
   55.92 +   ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out4f.tmp" \
   55.93  && echo "Success" \
   55.94  || echo "Failed"
   55.95  
   55.96 -   ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
   55.97 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
   55.98 +>  out4o.tmp
   55.99 +
  55.100 +   ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out4o.tmp" \
  55.101  && echo "Success" \
  55.102  || echo "Failed"
  55.103  
  55.104 @@ -107,8 +120,11 @@
  55.105  
  55.106  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-rival.txt" 2>> $ERROR
  55.107  
  55.108 -   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
  55.109 -&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
  55.110 +   "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "freebusy" \
  55.111 +>  out4r.tmp
  55.112 +
  55.113 +   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out4r.tmp" \
  55.114 +&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out4r.tmp" \
  55.115  && echo "Success" \
  55.116  || echo "Failed"
  55.117  
  55.118 @@ -123,11 +139,16 @@
  55.119  && echo "Success" \
  55.120  || echo "Failed"
  55.121  
  55.122 -   grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBFILE" \
  55.123 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  55.124 +|  tee out5f.tmp \
  55.125 +|  grep -q "^20141126T153000Z${TAB}20141126T154500Z" \
  55.126  && echo "Success" \
  55.127  || echo "Failed"
  55.128  
  55.129 -   ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
  55.130 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
  55.131 +>  out5o.tmp
  55.132 +
  55.133 +   ! grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out5o.tmp" \
  55.134  && echo "Success" \
  55.135  || echo "Failed"
  55.136  
  55.137 @@ -137,13 +158,18 @@
  55.138  | "$SHOWMAIL" \
  55.139  > out6.tmp
  55.140  
  55.141 -   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
  55.142 -&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
  55.143 -&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "$FBRIVALSENDERFILE" \
  55.144 +   "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "freebusy" \
  55.145 +>  out6r.tmp
  55.146 +
  55.147 +   ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out6r.tmp" \
  55.148 +&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out6r.tmp" \
  55.149 +&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out6r.tmp" \
  55.150  && echo "Success" \
  55.151  || echo "Failed"
  55.152  
  55.153 -   grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' "$STORE/$RIVALSENDER/objects/event18@example.com" \
  55.154 +   "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "object" "event18@example.com" \
  55.155 +|  tee out6O.tmp \
  55.156 +|  grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' \
  55.157  && echo "Success" \
  55.158  || echo "Failed"
  55.159  
    56.1 --- a/tests/test_resource_invitation_constraints_multiple.sh	Tue Apr 19 21:20:57 2016 +0200
    56.2 +++ b/tests/test_resource_invitation_constraints_multiple.sh	Fri Apr 22 16:22:58 2016 +0200
    56.3 @@ -5,9 +5,6 @@
    56.4  USER="mailto:resource-room-sauna@example.com"
    56.5  SENDER="mailto:paul.boddie@example.com"
    56.6  OUTSIDESENDER="mailto:paul.boddie@example.net"
    56.7 -FBFILE="$STORE/$USER/freebusy"
    56.8 -FBSENDERFILE="$STORE/$SENDER/freebusy"
    56.9 -FBOUTSIDESENDERFILE="$STORE/$OUTSIDESENDER/freebusy"
   56.10  
   56.11  mkdir -p "$PREFS/$USER"
   56.12  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   56.13 @@ -32,7 +29,9 @@
   56.14  
   56.15  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-outsider.txt" 2>> $ERROR
   56.16  
   56.17 -   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOUTSIDESENDERFILE" \
   56.18 +   "$LIST_SCRIPT" $LIST_ARGS "$OUTSIDESENDER" "freebusy" \
   56.19 +|  tee out0f.tmp \
   56.20 +|  grep -q "^20141126T150000Z${TAB}20141126T154500Z" \
   56.21  && echo "Success" \
   56.22  || echo "Failed"
   56.23  
   56.24 @@ -48,8 +47,10 @@
   56.25  && echo "Success" \
   56.26  || echo "Failed"
   56.27  
   56.28 -   ! [ -e "$FBFILE" ] \
   56.29 -|| ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBFILE" \
   56.30 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   56.31 +>  out1f.tmp
   56.32 +
   56.33 +   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out1f.tmp" \
   56.34  && echo "Success" \
   56.35  || echo "Failed"
   56.36  
   56.37 @@ -71,8 +72,9 @@
   56.38  && echo "Success" \
   56.39  || echo "Failed"
   56.40  
   56.41 -   [ -e "$FBFILE" ] \
   56.42 -&& grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBFILE" \
   56.43 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   56.44 +|  tee out2f.tmp \
   56.45 +|  grep -q "^20141126T150000Z${TAB}20141126T154500Z" \
   56.46  && echo "Success" \
   56.47  || echo "Failed"
   56.48  
   56.49 @@ -105,7 +107,9 @@
   56.50  
   56.51  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-acl.txt" 2>> $ERROR
   56.52  
   56.53 -   grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \
   56.54 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   56.55 +|  tee out3f.tmp \
   56.56 +|  grep -q "^20141126T160000Z${TAB}20141126T164500Z" \
   56.57  && echo "Success" \
   56.58  || echo "Failed"
   56.59  
   56.60 @@ -121,8 +125,10 @@
   56.61  && echo "Success" \
   56.62  || echo "Failed"
   56.63  
   56.64 -   ! [ -e "$FBFILE" ] \
   56.65 -|| ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \
   56.66 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   56.67 +>  out4f.tmp
   56.68 +
   56.69 +   ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out4f.tmp" \
   56.70  && echo "Success" \
   56.71  || echo "Failed"
   56.72  
   56.73 @@ -136,7 +142,9 @@
   56.74  
   56.75  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-acl.txt" 2>> $ERROR
   56.76  
   56.77 -   grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \
   56.78 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   56.79 +|  tee out4s.tmp \
   56.80 +|  grep -q "^20141126T160000Z${TAB}20141126T164500Z" \
   56.81  && echo "Success" \
   56.82  || echo "Failed"
   56.83  
   56.84 @@ -152,8 +160,10 @@
   56.85  && echo "Success" \
   56.86  || echo "Failed"
   56.87  
   56.88 -   ! [ -e "$FBFILE" ] \
   56.89 -|| ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \
   56.90 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   56.91 +>  out5f.tmp
   56.92 +
   56.93 +   ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out5f.tmp" \
   56.94  && echo "Success" \
   56.95  || echo "Failed"
   56.96  
   56.97 @@ -168,7 +178,9 @@
   56.98  
   56.99  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-acl.txt" 2>> $ERROR
  56.100  
  56.101 -   grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \
  56.102 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  56.103 +|  tee out5s.tmp \
  56.104 +|  grep -q "^20141126T160000Z${TAB}20141126T164500Z" \
  56.105  && echo "Success" \
  56.106  || echo "Failed"
  56.107  
  56.108 @@ -184,8 +196,10 @@
  56.109  && echo "Success" \
  56.110  || echo "Failed"
  56.111  
  56.112 -   ! [ -e "$FBFILE" ] \
  56.113 -|| ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \
  56.114 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  56.115 +>  out6f.tmp
  56.116 +
  56.117 +   ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out6f.tmp" \
  56.118  && echo "Success" \
  56.119  || echo "Failed"
  56.120  
  56.121 @@ -214,8 +228,9 @@
  56.122  && echo "Success" \
  56.123  || echo "Failed"
  56.124  
  56.125 -   ! [ -e "$FBFILE" ] \
  56.126 -|| grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \
  56.127 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  56.128 +|  tee out7f.tmp \
  56.129 +|  grep -q "^20141126T160000Z${TAB}20141126T164500Z" \
  56.130  && echo "Success" \
  56.131  || echo "Failed"
  56.132  
    57.1 --- a/tests/test_resource_invitation_constraints_next_free.sh	Tue Apr 19 21:20:57 2016 +0200
    57.2 +++ b/tests/test_resource_invitation_constraints_next_free.sh	Fri Apr 22 16:22:58 2016 +0200
    57.3 @@ -5,13 +5,6 @@
    57.4  USER="mailto:resource-room-sauna@example.com"
    57.5  SENDER="mailto:paul.boddie@example.com"
    57.6  RIVALSENDER="mailto:vincent.vole@example.com"
    57.7 -FBFILE="$STORE/$USER/freebusy"
    57.8 -FBOTHERFILE="$STORE/$USER/freebusy-other/$SENDER"
    57.9 -FBOFFERFILE="$STORE/$USER/freebusy-offers"
   57.10 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   57.11 -FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER"
   57.12 -FBSENDERREQUESTS="$STORE/$SENDER/requests"
   57.13 -FBRIVALSENDERFILE="$STORE/$RIVALSENDER/freebusy"
   57.14  
   57.15  mkdir -p "$PREFS/$USER"
   57.16  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   57.17 @@ -32,8 +25,11 @@
   57.18  
   57.19  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-busy.txt" 2>> $ERROR
   57.20  
   57.21 -   [ `grep "event19@example.com" "$FBRIVALSENDERFILE" | wc -l` = '5' ] \
   57.22 -&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBRIVALSENDERFILE" \
   57.23 +   "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "freebusy" \
   57.24 +>  out0s.tmp
   57.25 +
   57.26 +   [ `grep "event19@example.com" "out0s.tmp" | wc -l` = '5' ] \
   57.27 +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out0s.tmp" \
   57.28  && echo "Success" \
   57.29  || echo "Failed"
   57.30  
   57.31 @@ -49,8 +45,11 @@
   57.32  && echo "Success" \
   57.33  || echo "Failed"
   57.34  
   57.35 -   [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \
   57.36 -&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   57.37 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   57.38 +>  out1f.tmp
   57.39 +
   57.40 +   [ `grep "event19@example.com" "out1f.tmp" | wc -l` = '5' ] \
   57.41 +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f.tmp" \
   57.42  && echo "Success" \
   57.43  || echo "Failed"
   57.44  
   57.45 @@ -67,8 +66,11 @@
   57.46  | "$SHOWMAIL" \
   57.47  > out3.tmp
   57.48  
   57.49 -   grep -q "^20141126T160000Z${TAB}20141126T170000Z" "$FBOTHERFILE" \
   57.50 -&& grep -q "^20141126T180000Z${TAB}20141126T190000Z" "$FBOTHERFILE" \
   57.51 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
   57.52 +>  out3f.tmp
   57.53 +
   57.54 +   grep -q "^20141126T160000Z${TAB}20141126T170000Z" "out3f.tmp" \
   57.55 +&& grep -q "^20141126T180000Z${TAB}20141126T190000Z" "out3f.tmp" \
   57.56  && echo "Success" \
   57.57  || echo "Failed"
   57.58  
   57.59 @@ -76,7 +78,9 @@
   57.60  
   57.61  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-good.txt" 2>> $ERROR
   57.62  
   57.63 -   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
   57.64 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   57.65 +|  tee out3s.tmp \
   57.66 +|  grep -q "^20141126T150000Z${TAB}20141126T154500Z" \
   57.67  && echo "Success" \
   57.68  || echo "Failed"
   57.69  
   57.70 @@ -94,12 +98,15 @@
   57.71  && echo "Success" \
   57.72  || echo "Failed"
   57.73  
   57.74 -   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
   57.75 -&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \
   57.76 -&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "$FBOFFERFILE" \
   57.77 -&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "$FBOFFERFILE" \
   57.78 -&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "$FBOFFERFILE" \
   57.79 -&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "$FBOFFERFILE" \
   57.80 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
   57.81 +>  out6o.tmp
   57.82 +
   57.83 +   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out6o.tmp" \
   57.84 +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out6o.tmp" \
   57.85 +&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "out6o.tmp" \
   57.86 +&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "out6o.tmp" \
   57.87 +&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "out6o.tmp" \
   57.88 +&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "out6o.tmp" \
   57.89  && echo "Success" \
   57.90  || echo "Failed"
   57.91  
   57.92 @@ -110,16 +117,23 @@
   57.93  | "$SHOWMAIL" \
   57.94  > out7.tmp
   57.95  
   57.96 -   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
   57.97 -&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \
   57.98 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   57.99 +>  out7s.tmp
  57.100 +
  57.101 +   grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out7s.tmp" \
  57.102 +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out7s.tmp" \
  57.103  && echo "Success" \
  57.104  || echo "Failed"
  57.105  
  57.106 -   [ -e "$STORE/$SENDER/counters/objects/event13@example.com/$USER" ] \
  57.107 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "countered_object" "event13@example.com" "$USER" \
  57.108 +|  tee out7C.tmp \
  57.109 +|  grep -q "event13@example.com" \
  57.110  && echo "Success" \
  57.111  || echo "Failed"
  57.112  
  57.113 -   grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
  57.114 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "requests" \
  57.115 +|  tee out7R.tmp \
  57.116 +|  grep -q 'event13@example.com' \
  57.117  && echo "Success" \
  57.118  || echo "Failed"
  57.119  
  57.120 @@ -132,16 +146,25 @@
  57.121  
  57.122  "$OUTGOING_SCRIPT" $ARGS < out8.tmp 2>> $ERROR
  57.123  
  57.124 -   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \
  57.125 -&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "$FBSENDERFILE" \
  57.126 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  57.127 +>  out8s.tmp
  57.128 +
  57.129 +   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out8s.tmp" \
  57.130 +&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "out8s.tmp" \
  57.131  && echo "Success" \
  57.132  || echo "Failed"
  57.133  
  57.134 -   ! [ -e "$STORE/$SENDER/counters/objects/event13@example.com/$USER" ] \
  57.135 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "countered_object" "event13@example.com" "$USER" \
  57.136 +>  out8C.tmp
  57.137 +
  57.138 +   ! grep -q "event13@example.com" "out8C.tmp" \
  57.139  && echo "Success" \
  57.140  || echo "Failed"
  57.141  
  57.142 -   ! grep -q 'event13@example.com' "$FBSENDERREQUESTS" \
  57.143 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "requests" \
  57.144 +>  out8R.tmp
  57.145 +
  57.146 +   ! grep -q 'event13@example.com' "out8R.tmp" \
  57.147  && echo "Success" \
  57.148  || echo "Failed"
  57.149  
  57.150 @@ -154,18 +177,24 @@
  57.151  && echo "Success" \
  57.152  || echo "Failed"
  57.153  
  57.154 -   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
  57.155 -&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \
  57.156 -&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "$FBOFFERFILE" \
  57.157 -&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "$FBOFFERFILE" \
  57.158 -&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "$FBOFFERFILE" \
  57.159 -&& ! grep -q "^20141126T200000Z${TAB}20141126T204500Z" "$FBOFFERFILE" \
  57.160 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
  57.161 +>  out9o.tmp
  57.162 +
  57.163 +   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out9o.tmp" \
  57.164 +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out9o.tmp" \
  57.165 +&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "out9o.tmp" \
  57.166 +&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "out9o.tmp" \
  57.167 +&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "out9o.tmp" \
  57.168 +&& ! grep -q "^20141126T200000Z${TAB}20141126T204500Z" "out9o.tmp" \
  57.169  && echo "Success" \
  57.170  || echo "Failed"
  57.171  
  57.172 -   [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \
  57.173 -&& [ `grep "event13@example.com" "$FBFILE" | wc -l` = '1' ] \
  57.174 -&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "$FBFILE" \
  57.175 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  57.176 +>  out9f.tmp
  57.177 +
  57.178 +   [ `grep "event19@example.com" "out9f.tmp" | wc -l` = '5' ] \
  57.179 +&& [ `grep "event13@example.com" "out9f.tmp" | wc -l` = '1' ] \
  57.180 +&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "out9f.tmp" \
  57.181  && echo "Success" \
  57.182  || echo "Failed"
  57.183  
  57.184 @@ -180,18 +209,24 @@
  57.185  && echo "Success" \
  57.186  || echo "Failed"
  57.187  
  57.188 -   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
  57.189 -&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \
  57.190 -&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "$FBOFFERFILE" \
  57.191 -&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "$FBOFFERFILE" \
  57.192 -&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "$FBOFFERFILE" \
  57.193 -&& ! grep -q "^20141126T200000Z${TAB}20141126T204500Z" "$FBOFFERFILE" \
  57.194 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
  57.195 +>  out10o.tmp
  57.196 +
  57.197 +   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out10o.tmp" \
  57.198 +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out10o.tmp" \
  57.199 +&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "out10o.tmp" \
  57.200 +&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "out10o.tmp" \
  57.201 +&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "out10o.tmp" \
  57.202 +&& ! grep -q "^20141126T200000Z${TAB}20141126T204500Z" "out10o.tmp" \
  57.203  && echo "Success" \
  57.204  || echo "Failed"
  57.205  
  57.206 -   [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \
  57.207 -&& [ `grep "event13@example.com" "$FBFILE" | wc -l` = '1' ] \
  57.208 -&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "$FBFILE" \
  57.209 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  57.210 +>  out10f.tmp
  57.211 +
  57.212 +   [ `grep "event19@example.com" "out10f.tmp" | wc -l` = '5' ] \
  57.213 +&& [ `grep "event13@example.com" "out10f.tmp" | wc -l` = '1' ] \
  57.214 +&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "out10f.tmp" \
  57.215  && echo "Success" \
  57.216  || echo "Failed"
  57.217  
  57.218 @@ -212,17 +247,23 @@
  57.219  
  57.220  # Note that the duration is different now.
  57.221  
  57.222 -   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOFFERFILE" \
  57.223 -&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBOFFERFILE" \
  57.224 -&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "$FBOFFERFILE" \
  57.225 -&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "$FBOFFERFILE" \
  57.226 -&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "$FBOFFERFILE" \
  57.227 -&& grep -q "^20141126T200000Z${TAB}20141126T203000Z" "$FBOFFERFILE" \
  57.228 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_offers" \
  57.229 +>  out12o.tmp
  57.230 +
  57.231 +   ! grep -q "^20141126T150000Z${TAB}20141126T154500Z" "out12o.tmp" \
  57.232 +&& ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "out12o.tmp" \
  57.233 +&& ! grep -q "^20141126T170000Z${TAB}20141126T174500Z" "out12o.tmp" \
  57.234 +&& ! grep -q "^20141126T180000Z${TAB}20141126T184500Z" "out12o.tmp" \
  57.235 +&& ! grep -q "^20141126T190000Z${TAB}20141126T194500Z" "out12o.tmp" \
  57.236 +&& grep -q "^20141126T200000Z${TAB}20141126T203000Z" "out12o.tmp" \
  57.237  && echo "Success" \
  57.238  || echo "Failed"
  57.239  
  57.240 -   [ `grep "event19@example.com" "$FBFILE" | wc -l` = '5' ] \
  57.241 -&& [ `grep "event13@example.com" "$FBFILE" | wc -l` = '1' ] \
  57.242 -&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "$FBFILE" \
  57.243 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  57.244 +>  out12f.tmp
  57.245 +
  57.246 +   [ `grep "event19@example.com" "out12f.tmp" | wc -l` = '5' ] \
  57.247 +&& [ `grep "event13@example.com" "out12f.tmp" | wc -l` = '1' ] \
  57.248 +&& grep -q "^20141126T200000Z${TAB}20141126T204500Z" "out12f.tmp" \
  57.249  && echo "Success" \
  57.250  || echo "Failed"
    58.1 --- a/tests/test_resource_invitation_constraints_quota.sh	Tue Apr 19 21:20:57 2016 +0200
    58.2 +++ b/tests/test_resource_invitation_constraints_quota.sh	Fri Apr 22 16:22:58 2016 +0200
    58.3 @@ -5,11 +5,7 @@
    58.4  USER1="mailto:resource-car-porsche911@example.com"
    58.5  USER2="mailto:resource-car-fiat500@example.com"
    58.6  SENDER="mailto:paul.boddie@example.com"
    58.7 -FBFILE1="$STORE/$USER1/freebusy"
    58.8 -FBFILE2="$STORE/$USER2/freebusy"
    58.9 -FBSENDERFILE="$STORE/$SENDER/freebusy"
   58.10  QUOTA=cars
   58.11 -JOURNALFILE="$JOURNAL/$QUOTA/journal/$SENDER"
   58.12  
   58.13  mkdir -p "$PREFS/$USER1"
   58.14  echo 'Europe/Oslo' > "$PREFS/$USER1/TZID"
   58.15 @@ -27,8 +23,7 @@
   58.16  check_quota $QUOTA
   58.17  EOF
   58.18  
   58.19 -mkdir -p "$JOURNAL/$QUOTA"
   58.20 -echo '* PT1H' > "$JOURNAL/$QUOTA/limits"
   58.21 +"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT1H' $SET_QUOTA_LIMIT_ARGS
   58.22  
   58.23    "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car.txt" 2>> $ERROR \
   58.24  | "$SHOWMAIL" \
   58.25 @@ -43,7 +38,9 @@
   58.26  
   58.27  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR
   58.28  
   58.29 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
   58.30 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   58.31 +|  tee out0s.tmp \
   58.32 +|  grep -q "^20141126T150000Z${TAB}20141126T160000Z" \
   58.33  && echo "Success" \
   58.34  || echo "Failed"
   58.35  
   58.36 @@ -59,15 +56,17 @@
   58.37  && echo "Success" \
   58.38  || echo "Failed"
   58.39  
   58.40 -   [ -e "$FBFILE1" ] \
   58.41 -&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
   58.42 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
   58.43 +|  tee out1f.tmp \
   58.44 +|  grep -q "^20141126T150000Z${TAB}20141126T160000Z" \
   58.45  && echo "Success" \
   58.46  || echo "Failed"
   58.47  
   58.48  # Check the quota (event is confirmed).
   58.49  
   58.50 -   [ -e "$JOURNALFILE" ] \
   58.51 -&& grep -q "event21@example.com" "$JOURNALFILE" \
   58.52 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
   58.53 +|  tee out1e.tmp \
   58.54 +|  grep -q "event21@example.com" \
   58.55  && echo "Success" \
   58.56  || echo "Failed"
   58.57  
   58.58 @@ -75,7 +74,9 @@
   58.59  
   58.60  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-conflict.txt" 2>> $ERROR
   58.61  
   58.62 -   grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBSENDERFILE" \
   58.63 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   58.64 +|  tee out1s.tmp \
   58.65 +|  grep -q "^20141126T153000Z${TAB}20141126T163000Z" \
   58.66  && echo "Success" \
   58.67  || echo "Failed"
   58.68  
   58.69 @@ -91,28 +92,34 @@
   58.70  && echo "Success" \
   58.71  || echo "Failed"
   58.72  
   58.73 -   ! [ -e "$FBFILE2" ] \
   58.74 -|| ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBFILE2" \
   58.75 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
   58.76 +>  out2f.tmp
   58.77 +
   58.78 +   ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out2f.tmp" \
   58.79  && echo "Success" \
   58.80  || echo "Failed"
   58.81  
   58.82  # Check the quota (event is not confirmed).
   58.83  
   58.84 -   [ -e "$JOURNALFILE" ] \
   58.85 -&& grep -q "event21@example.com" "$JOURNALFILE" \
   58.86 -&& ! grep -q "event22@example.com" "$JOURNALFILE" \
   58.87 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
   58.88 +>  out2e.tmp
   58.89 +
   58.90 +   grep -q "event21@example.com" "out2e.tmp" \
   58.91 +&& ! grep -q "event22@example.com" "out2e.tmp" \
   58.92  && echo "Success" \
   58.93  || echo "Failed"
   58.94  
   58.95  # Increase the quota.
   58.96  
   58.97 -echo '* PT2H' > "$JOURNAL/$QUOTA/limits"
   58.98 +"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT2H' $SET_QUOTA_LIMIT_ARGS
   58.99  
  58.100  # Attempt to schedule the event again.
  58.101  
  58.102  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-conflict.txt" 2>> $ERROR
  58.103  
  58.104 -   grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBSENDERFILE" \
  58.105 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  58.106 +|  tee out2s.tmp \
  58.107 +|  grep -q "^20141126T153000Z${TAB}20141126T163000Z" \
  58.108  && echo "Success" \
  58.109  || echo "Failed"
  58.110  
  58.111 @@ -128,16 +135,19 @@
  58.112  && echo "Success" \
  58.113  || echo "Failed"
  58.114  
  58.115 -   [ -e "$FBFILE2" ] \
  58.116 -&& grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBFILE2" \
  58.117 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  58.118 +|  tee out3f.tmp \
  58.119 +|  grep -q "^20141126T153000Z${TAB}20141126T163000Z" \
  58.120  && echo "Success" \
  58.121  || echo "Failed"
  58.122  
  58.123  # Check the quota (event is confirmed).
  58.124  
  58.125 -   [ -e "$JOURNALFILE" ] \
  58.126 -&& grep -q "event21@example.com" "$JOURNALFILE" \
  58.127 -&& grep -q "event22@example.com" "$JOURNALFILE" \
  58.128 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  58.129 +>  out3e.tmp
  58.130 +
  58.131 +   grep -q "event21@example.com" "out3e.tmp" \
  58.132 +&& grep -q "event22@example.com" "out3e.tmp" \
  58.133  && echo "Success" \
  58.134  || echo "Failed"
  58.135  
  58.136 @@ -145,7 +155,10 @@
  58.137  
  58.138  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-car.txt" 2>> $ERROR
  58.139  
  58.140 -   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
  58.141 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  58.142 +>  out3s.tmp
  58.143 +
  58.144 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out3s.tmp" \
  58.145  && echo "Success" \
  58.146  || echo "Failed"
  58.147  
  58.148 @@ -160,15 +173,20 @@
  58.149  && echo "Success" \
  58.150  || echo "Failed"
  58.151  
  58.152 -   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
  58.153 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  58.154 +>  out4f.tmp
  58.155 +
  58.156 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out4f.tmp" \
  58.157  && echo "Success" \
  58.158  || echo "Failed"
  58.159  
  58.160  # Check the quota (event is retracted).
  58.161  
  58.162 -   [ -e "$JOURNALFILE" ] \
  58.163 -&& ! grep -q "event21@example.com" "$JOURNALFILE" \
  58.164 -&& grep -q "event22@example.com" "$JOURNALFILE" \
  58.165 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  58.166 +>  out4e.tmp
  58.167 +
  58.168 +   ! grep -q "event21@example.com" "out4e.tmp" \
  58.169 +&& grep -q "event22@example.com" "out4e.tmp" \
  58.170  && echo "Success" \
  58.171  || echo "Failed"
  58.172  
  58.173 @@ -198,16 +216,19 @@
  58.174  && echo "Success" \
  58.175  || echo "Failed"
  58.176  
  58.177 -   [ -e "$FBFILE2" ] \
  58.178 -&& grep -q "^20141126T153000Z${TAB}20141126T163000Z" "$FBFILE2" \
  58.179 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  58.180 +|  tee out5f.tmp \
  58.181 +|  grep -q "^20141126T153000Z${TAB}20141126T163000Z" \
  58.182  && echo "Success" \
  58.183  || echo "Failed"
  58.184  
  58.185  # Check the quota (event is still confirmed).
  58.186  
  58.187 -   [ -e "$JOURNALFILE" ] \
  58.188 -&& ! grep -q "event21@example.com" "$JOURNALFILE" \
  58.189 -&& grep -q "event22@example.com" "$JOURNALFILE" \
  58.190 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  58.191 +>  out5e.tmp
  58.192 +
  58.193 +   ! grep -q "event21@example.com" "out5e.tmp" \
  58.194 +&& grep -q "event22@example.com" "out5e.tmp" \
  58.195  && echo "Success" \
  58.196  || echo "Failed"
  58.197  
  58.198 @@ -215,7 +236,9 @@
  58.199  
  58.200  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR
  58.201  
  58.202 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
  58.203 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  58.204 +|  tee out5s.tmp \
  58.205 +|  grep -q "^20141126T150000Z${TAB}20141126T160000Z" \
  58.206  && echo "Success" \
  58.207  || echo "Failed"
  58.208  
  58.209 @@ -231,15 +254,20 @@
  58.210  && echo "Success" \
  58.211  || echo "Failed"
  58.212  
  58.213 -   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
  58.214 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  58.215 +>  out6f.tmp
  58.216 +
  58.217 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out6f.tmp" \
  58.218  && echo "Success" \
  58.219  || echo "Failed"
  58.220  
  58.221  # Check the quota (event is still retracted and not newly confirmed).
  58.222  
  58.223 -   [ -e "$JOURNALFILE" ] \
  58.224 -&& ! grep -q "event21@example.com" "$JOURNALFILE" \
  58.225 -&& grep -q "event22@example.com" "$JOURNALFILE" \
  58.226 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  58.227 +>  out6e.tmp
  58.228 +
  58.229 +   ! grep -q "event21@example.com" "out6e.tmp" \
  58.230 +&& grep -q "event22@example.com" "out6e.tmp" \
  58.231  && echo "Success" \
  58.232  || echo "Failed"
  58.233  
  58.234 @@ -247,8 +275,11 @@
  58.235  
  58.236  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-moved.txt" 2>> $ERROR
  58.237  
  58.238 -   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
  58.239 -&& grep -q "^20141126T143000Z${TAB}20141126T153000Z" "$FBSENDERFILE" \
  58.240 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  58.241 +>  out6s.tmp
  58.242 +
  58.243 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out6s.tmp" \
  58.244 +&& grep -q "^20141126T143000Z${TAB}20141126T153000Z" "out6s.tmp" \
  58.245  && echo "Success" \
  58.246  || echo "Failed"
  58.247  
  58.248 @@ -264,27 +295,33 @@
  58.249  && echo "Success" \
  58.250  || echo "Failed"
  58.251  
  58.252 -   grep -q "^20141126T143000Z${TAB}20141126T153000Z" "$FBFILE1" \
  58.253 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  58.254 +|  tee out7f.tmp \
  58.255 +|  grep -q "^20141126T143000Z${TAB}20141126T153000Z" \
  58.256  && echo "Success" \
  58.257  || echo "Failed"
  58.258  
  58.259  # Check the quota (event is newly confirmed).
  58.260  
  58.261 -   [ -e "$JOURNALFILE" ] \
  58.262 -&& grep -q "event21@example.com" "$JOURNALFILE" \
  58.263 -&& grep -q "event22@example.com" "$JOURNALFILE" \
  58.264 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  58.265 +>  out7e.tmp
  58.266 +
  58.267 +   grep -q "event21@example.com" "out7e.tmp" \
  58.268 +&& grep -q "event22@example.com" "out7e.tmp" \
  58.269  && echo "Success" \
  58.270  || echo "Failed"
  58.271  
  58.272  # Increase the quota.
  58.273  
  58.274 -echo '* PT3H' > "$JOURNAL/$QUOTA/limits"
  58.275 +"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT3H' $SET_QUOTA_LIMIT_ARGS
  58.276  
  58.277  # Attempt to schedule an event involving both resources.
  58.278  
  58.279  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars.txt" 2>> $ERROR
  58.280  
  58.281 -   grep -q "^20141127T150000Z${TAB}20141127T160000Z" "$FBSENDERFILE" \
  58.282 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  58.283 +|  tee out7s.tmp \
  58.284 +|  grep -q "^20141127T150000Z${TAB}20141127T160000Z" \
  58.285  && echo "Success" \
  58.286  || echo "Failed"
  58.287  
  58.288 @@ -311,16 +348,23 @@
  58.289  && echo "Success" \
  58.290  || echo "Failed"
  58.291  
  58.292 -   ( grep -q "^20141127T150000Z${TAB}20141127T160000Z" "$FBFILE1" \
  58.293 -     && ! grep -q "^20141127T150000Z${TAB}20141127T160000Z" "$FBFILE2" ) \
  58.294 -|| ( ! grep -q "^20141127T150000Z${TAB}20141127T160000Z" "$FBFILE1" \
  58.295 -     && grep -q "^20141127T150000Z${TAB}20141127T160000Z" "$FBFILE2" ) \
  58.296 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  58.297 +>  out8f.tmp
  58.298 +
  58.299 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  58.300 +>  out8f2.tmp
  58.301 +
  58.302 +   ( grep -q "^20141127T150000Z${TAB}20141127T160000Z" "out8f.tmp" \
  58.303 +     && ! grep -q "^20141127T150000Z${TAB}20141127T160000Z" "out8f2.tmp" ) \
  58.304 +|| ( ! grep -q "^20141127T150000Z${TAB}20141127T160000Z" "out8f.tmp" \
  58.305 +     && grep -q "^20141127T150000Z${TAB}20141127T160000Z" "out8f2.tmp" ) \
  58.306  && echo "Success" \
  58.307  || echo "Failed"
  58.308  
  58.309  # Check the quota (event is confirmed, but only for one resource).
  58.310  
  58.311 -   [ -e "$JOURNALFILE" ] \
  58.312 -&& grep -q "event23@example.com" "$JOURNALFILE" \
  58.313 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  58.314 +|  tee out8e.tmp \
  58.315 +|  grep -q "event23@example.com" \
  58.316  && echo "Success" \
  58.317  || echo "Failed"
    59.1 --- a/tests/test_resource_invitation_constraints_quota_recurring.sh	Tue Apr 19 21:20:57 2016 +0200
    59.2 +++ b/tests/test_resource_invitation_constraints_quota_recurring.sh	Fri Apr 22 16:22:58 2016 +0200
    59.3 @@ -4,10 +4,7 @@
    59.4  
    59.5  USER="mailto:resource-car-porsche911@example.com"
    59.6  SENDER="mailto:paul.boddie@example.com"
    59.7 -FBFILE="$STORE/$USER/freebusy"
    59.8 -FBSENDERFILE="$STORE/$SENDER/freebusy"
    59.9  QUOTA="$USER"
   59.10 -JOURNALFILE="$JOURNAL/$QUOTA/journal/$SENDER"
   59.11  
   59.12  mkdir -p "$PREFS/$USER"
   59.13  echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
   59.14 @@ -19,8 +16,7 @@
   59.15  
   59.16  # Employ a user-specific quota (no argument with the functions above).
   59.17  
   59.18 -mkdir -p "$JOURNAL/$QUOTA"
   59.19 -echo '* PT10H' > "$JOURNAL/$QUOTA/limits"
   59.20 +"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT10H' $SET_QUOTA_LIMIT_ARGS
   59.21  
   59.22    "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car-all.txt" 2>> $ERROR \
   59.23  | "$SHOWMAIL" \
   59.24 @@ -35,8 +31,11 @@
   59.25  
   59.26  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-recurring.txt" 2>> $ERROR
   59.27  
   59.28 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
   59.29 -&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBSENDERFILE" \
   59.30 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   59.31 +>  out0f.tmp
   59.32 +
   59.33 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out0f.tmp" \
   59.34 +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out0f.tmp" \
   59.35  && echo "Success" \
   59.36  || echo "Failed"
   59.37  
   59.38 @@ -52,16 +51,20 @@
   59.39  && echo "Success" \
   59.40  || echo "Failed"
   59.41  
   59.42 -   ! [ -e "$FBFILE" ] \
   59.43 -|| ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   59.44 -  && ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBFILE" ) \
   59.45 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   59.46 +>  out1f.tmp
   59.47 +
   59.48 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f.tmp" \
   59.49 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1f.tmp" \
   59.50  && echo "Success" \
   59.51  || echo "Failed"
   59.52  
   59.53  # Check the quota (event is not confirmed).
   59.54  
   59.55 -   ! [ -e "$JOURNALFILE" ] \
   59.56 -|| ! grep -q "event24@example.com" "$JOURNALFILE" \
   59.57 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
   59.58 +>  out1e.tmp
   59.59 +
   59.60 +   ! grep -q "event24@example.com" "out1e.tmp" \
   59.61  && echo "Success" \
   59.62  || echo "Failed"
   59.63  
   59.64 @@ -70,9 +73,12 @@
   59.65    sed 's/FREQ=DAILY/FREQ=DAILY;COUNT=11/;' "$TEMPLATES/event-request-car-recurring.txt" \
   59.66  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
   59.67  
   59.68 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
   59.69 -&& grep -q "^20141205T150000Z${TAB}20141205T160000Z" "$FBSENDERFILE" \
   59.70 -&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBSENDERFILE" \
   59.71 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
   59.72 +>  out1s.tmp
   59.73 +
   59.74 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1s.tmp" \
   59.75 +&& grep -q "^20141205T150000Z${TAB}20141205T160000Z" "out1s.tmp" \
   59.76 +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1s.tmp" \
   59.77  && echo "Success" \
   59.78  || echo "Failed"
   59.79  
   59.80 @@ -89,17 +95,21 @@
   59.81  && echo "Success" \
   59.82  || echo "Failed"
   59.83  
   59.84 -   ! [ -e "$FBFILE" ] \
   59.85 -|| ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
   59.86 -  && ! grep -q "^20141205T150000Z${TAB}20141205T160000Z" "$FBFILE" \
   59.87 -  && ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBFILE" ) \
   59.88 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
   59.89 +>  out2f.tmp
   59.90 +
   59.91 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \
   59.92 +&& ! grep -q "^20141205T150000Z${TAB}20141205T160000Z" "out2f.tmp" \
   59.93 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out2f.tmp"  \
   59.94  && echo "Success" \
   59.95  || echo "Failed"
   59.96  
   59.97  # Check the quota (event is confirmed).
   59.98  
   59.99 -   ! [ -e "$JOURNALFILE" ] \
  59.100 -|| ! grep -q "event24@example.com" "$JOURNALFILE" \
  59.101 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  59.102 +>  out2e.tmp
  59.103 +
  59.104 +   ! grep -q "event24@example.com" "out2e.tmp" \
  59.105  && echo "Success" \
  59.106  || echo "Failed"
  59.107  
  59.108 @@ -108,9 +118,12 @@
  59.109    sed 's/FREQ=DAILY/FREQ=DAILY;COUNT=10/;' "$TEMPLATES/event-request-car-recurring.txt" \
  59.110  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  59.111  
  59.112 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE" \
  59.113 -&& grep -q "^20141205T150000Z${TAB}20141205T160000Z" "$FBSENDERFILE" \
  59.114 -&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBSENDERFILE" \
  59.115 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
  59.116 +>  out2s.tmp
  59.117 +
  59.118 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2s.tmp" \
  59.119 +&& grep -q "^20141205T150000Z${TAB}20141205T160000Z" "out2s.tmp" \
  59.120 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out2s.tmp" \
  59.121  && echo "Success" \
  59.122  || echo "Failed"
  59.123  
  59.124 @@ -118,25 +131,29 @@
  59.125  
  59.126    sed 's/FREQ=DAILY/FREQ=DAILY;COUNT=10/;' "$TEMPLATES/event-request-car-recurring.txt" \
  59.127  | "$RESOURCE_SCRIPT" $ARGS 2>> $ERROR \
  59.128 -| tee out2r.tmp \
  59.129 +| tee out3r.tmp \
  59.130  | "$SHOWMAIL" \
  59.131 -> out2.tmp
  59.132 +> out3.tmp
  59.133  
  59.134 -   grep -q 'METHOD:REPLY' out2.tmp \
  59.135 -&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out2.tmp \
  59.136 +   grep -q 'METHOD:REPLY' out3.tmp \
  59.137 +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out3.tmp \
  59.138  && echo "Success" \
  59.139  || echo "Failed"
  59.140  
  59.141 -   [ -e "$FBFILE" ] \
  59.142 -&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE" \
  59.143 -&& grep -q "^20141205T150000Z${TAB}20141205T160000Z" "$FBFILE" \
  59.144 -&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBFILE" \
  59.145 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
  59.146 +>  out3f.tmp
  59.147 +
  59.148 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out3f.tmp" \
  59.149 +&& grep -q "^20141205T150000Z${TAB}20141205T160000Z" "out3f.tmp" \
  59.150 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out3f.tmp" \
  59.151  && echo "Success" \
  59.152  || echo "Failed"
  59.153  
  59.154  # Check the quota (event is confirmed).
  59.155  
  59.156 -   [ -e "$JOURNALFILE" ] \
  59.157 -&& grep -q "event24@example.com" "$JOURNALFILE" \
  59.158 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \
  59.159 +>  out3e.tmp
  59.160 +
  59.161 +   grep -q "event24@example.com" "out3e.tmp" \
  59.162  && echo "Success" \
  59.163  || echo "Failed"
    60.1 --- a/tests/test_resource_invitation_constraints_quota_recurring_limits.sh	Tue Apr 19 21:20:57 2016 +0200
    60.2 +++ b/tests/test_resource_invitation_constraints_quota_recurring_limits.sh	Fri Apr 22 16:22:58 2016 +0200
    60.3 @@ -8,13 +8,7 @@
    60.4  SENDER2="mailto:vincent.vole@example.com"
    60.5  SENDERADDRESS1="paul.boddie@example.com"
    60.6  SENDERADDRESS2="vincent.vole@example.com"
    60.7 -FBFILE1="$STORE/$USER1/freebusy"
    60.8 -FBFILE2="$STORE/$USER2/freebusy"
    60.9 -FBSENDERFILE1="$STORE/$SENDER1/freebusy"
   60.10 -FBSENDERFILE2="$STORE/$SENDER2/freebusy"
   60.11  QUOTA=cars
   60.12 -JOURNALFILE1="$JOURNAL/$QUOTA/journal/$SENDER1"
   60.13 -JOURNALFILE2="$JOURNAL/$QUOTA/journal/$SENDER2"
   60.14  
   60.15  mkdir -p "$PREFS/$USER1"
   60.16  echo 'Europe/Oslo' > "$PREFS/$USER1/TZID"
   60.17 @@ -32,11 +26,8 @@
   60.18  check_quota $QUOTA
   60.19  EOF
   60.20  
   60.21 -mkdir -p "$JOURNAL/$QUOTA"
   60.22 -cat > "$JOURNAL/$QUOTA/limits" <<EOF
   60.23 -mailto:vincent.vole@example.com PT10H
   60.24 -* PT5H
   60.25 -EOF
   60.26 +"$SET_QUOTA_LIMIT" "$QUOTA" 'mailto:vincent.vole@example.com' 'PT10H' $SET_QUOTA_LIMIT_ARGS
   60.27 +"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT5H' $SET_QUOTA_LIMIT_ARGS
   60.28  
   60.29    "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car-all.txt" 2>> $ERROR \
   60.30  | "$SHOWMAIL" \
   60.31 @@ -51,8 +42,11 @@
   60.32  
   60.33  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR
   60.34  
   60.35 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE1" \
   60.36 -&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBSENDERFILE1" \
   60.37 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER1" "freebusy" \
   60.38 +>  out0f.tmp
   60.39 +
   60.40 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out0f.tmp" \
   60.41 +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out0f.tmp" \
   60.42  && echo "Success" \
   60.43  || echo "Failed"
   60.44  
   60.45 @@ -68,19 +62,25 @@
   60.46  && echo "Success" \
   60.47  || echo "Failed"
   60.48  
   60.49 -   ( ! [ -e "$FBFILE1" ] \
   60.50 -  || ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
   60.51 -    && ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBFILE1" )) \
   60.52 -&& ( ! [ -e "$FBFILE2" ] \
   60.53 -  || ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE2" \
   60.54 -    && ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBFILE2" )) \
   60.55 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
   60.56 +>  out1f.tmp
   60.57 +
   60.58 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
   60.59 +>  out1f2.tmp
   60.60 +
   60.61 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f.tmp" \
   60.62 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1f.tmp" \
   60.63 +&& ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f2.tmp" \
   60.64 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1f2.tmp" \
   60.65  && echo "Success" \
   60.66  || echo "Failed"
   60.67  
   60.68  # Check the quota (event is not confirmed).
   60.69  
   60.70 -   ! [ -e "$JOURNALFILE1" ] \
   60.71 -|| ! grep -q "event25@example.com" "$JOURNALFILE1" \
   60.72 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \
   60.73 +>  out1e.tmp
   60.74 +
   60.75 +   ! grep -q "event25@example.com" "out1e.tmp" \
   60.76  && echo "Success" \
   60.77  || echo "Failed"
   60.78  
   60.79 @@ -89,8 +89,11 @@
   60.80    sed 's/FREQ=DAILY/FREQ=DAILY;COUNT=5/;' "$TEMPLATES/event-request-cars-recurring.txt" \
   60.81  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
   60.82  
   60.83 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE1" \
   60.84 -&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBSENDERFILE1" \
   60.85 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER1" "freebusy" \
   60.86 +>  out1s.tmp
   60.87 +
   60.88 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1s.tmp" \
   60.89 +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out1s.tmp" \
   60.90  && echo "Success" \
   60.91  || echo "Failed"
   60.92  
   60.93 @@ -118,25 +121,29 @@
   60.94  && echo "Success" \
   60.95  || echo "Failed"
   60.96  
   60.97 -   (( ! [ -e "$FBFILE1" ] \
   60.98 -   || ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
   60.99 -     && ! grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBFILE1" )) \
  60.100 -     && [ -e "$FBFILE2" ] \
  60.101 -     && grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE2" \
  60.102 -     && grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBFILE2" ) \
  60.103 -|| (( ! [ -e "$FBFILE2" ] \
  60.104 -   || ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE2" \
  60.105 -     && ! grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBFILE2" )) \
  60.106 -     && [ -e "$FBFILE1" ] \
  60.107 -     && grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
  60.108 -     && grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBFILE1" ) \
  60.109 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  60.110 +>  out2f.tmp
  60.111 +
  60.112 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  60.113 +>  out2f2.tmp
  60.114 +
  60.115 +    ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \
  60.116 +   && ! grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f.tmp" \
  60.117 +   && grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f2.tmp" \
  60.118 +   && grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f2.tmp" ) \
  60.119 +||  ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f2.tmp" \
  60.120 +   && ! grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f2.tmp" \
  60.121 +   && grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \
  60.122 +   && grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f.tmp" ) \
  60.123  && echo "Success" \
  60.124  || echo "Failed"
  60.125  
  60.126  # Check the quota (event is confirmed for one resource).
  60.127  
  60.128 -   ! [ -e "$JOURNALFILE1" ] \
  60.129 -|| grep -q "event25@example.com" "$JOURNALFILE1" \
  60.130 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \
  60.131 +>  out2e.tmp
  60.132 +
  60.133 +   grep -q "event25@example.com" "out2e.tmp" \
  60.134  && echo "Success" \
  60.135  || echo "Failed"
  60.136  
  60.137 @@ -144,8 +151,11 @@
  60.138  
  60.139  "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-cars-recurring.txt" 2>> $ERROR
  60.140  
  60.141 -   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE1" \
  60.142 -&& ! grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBSENDERFILE1" \
  60.143 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER1" "freebusy" \
  60.144 +>  out2s.tmp
  60.145 +
  60.146 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2s.tmp" \
  60.147 +&& ! grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2s.tmp" \
  60.148  && echo "Success" \
  60.149  || echo "Failed"
  60.150  
  60.151 @@ -160,19 +170,25 @@
  60.152  && echo "Success" \
  60.153  || echo "Failed"
  60.154  
  60.155 -   ( ! [ -e "$FBFILE1" ] \
  60.156 -  || ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
  60.157 -    && ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBFILE1" )) \
  60.158 -&& ( ! [ -e "$FBFILE2" ] \
  60.159 -  || ( ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE2" \
  60.160 -    && ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "$FBFILE2" )) \
  60.161 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  60.162 +>  out3f.tmp
  60.163 +
  60.164 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  60.165 +>  out3f2.tmp
  60.166 +
  60.167 +   ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out3f.tmp" \
  60.168 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out3f.tmp" \
  60.169 +&& ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out3f2.tmp" \
  60.170 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out3f2.tmp" \
  60.171  && echo "Success" \
  60.172  || echo "Failed"
  60.173  
  60.174  # Check the quota (event is retracted).
  60.175  
  60.176 -   ! [ -e "$JOURNALFILE1" ] \
  60.177 -|| ! grep -q "event25@example.com" "$JOURNALFILE1" \
  60.178 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \
  60.179 +>  out3e.tmp
  60.180 +
  60.181 +   ! grep -q "event25@example.com" "out3e.tmp" \
  60.182  && echo "Success" \
  60.183  || echo "Failed"
  60.184  
  60.185 @@ -182,8 +198,11 @@
  60.186  | sed "s/$SENDERADDRESS1/$SENDERADDRESS2/;" \
  60.187  | "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
  60.188  
  60.189 -   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBSENDERFILE2" \
  60.190 -&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBSENDERFILE2" \
  60.191 +   "$LIST_SCRIPT" $LIST_ARGS "$SENDER2" "freebusy" \
  60.192 +>  out3s.tmp
  60.193 +
  60.194 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out3s.tmp" \
  60.195 +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out3s.tmp" \
  60.196  && echo "Success" \
  60.197  || echo "Failed"
  60.198  
  60.199 @@ -211,18 +230,24 @@
  60.200  && echo "Success" \
  60.201  || echo "Failed"
  60.202  
  60.203 -   [ -e "$FBFILE1" ] \
  60.204 -&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE1" \
  60.205 -&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBFILE1" \
  60.206 -&& [ -e "$FBFILE2" ] \
  60.207 -&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "$FBFILE2" \
  60.208 -&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "$FBFILE2" \
  60.209 +   "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
  60.210 +>  out4f.tmp
  60.211 +
  60.212 +   "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
  60.213 +>  out4f2.tmp
  60.214 +
  60.215 +   grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out4f.tmp" \
  60.216 +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out4f.tmp" \
  60.217 +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out4f2.tmp" \
  60.218 +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out4f2.tmp" \
  60.219  && echo "Success" \
  60.220  || echo "Failed"
  60.221  
  60.222  # Check the quota (event is confirmed for both resources).
  60.223  
  60.224 -   [ -e "$JOURNALFILE2" ] \
  60.225 -&& grep -q "event25@example.com" "$JOURNALFILE2" \
  60.226 +   "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER2" \
  60.227 +>  out4e.tmp
  60.228 +
  60.229 +   grep -q "event25@example.com" "out4e.tmp" \
  60.230  && echo "Success" \
  60.231  || echo "Failed"
    61.1 --- a/tests/test_resource_invitation_recurring_indefinitely.sh	Tue Apr 19 21:20:57 2016 +0200
    61.2 +++ b/tests/test_resource_invitation_recurring_indefinitely.sh	Fri Apr 22 16:22:58 2016 +0200
    61.3 @@ -39,7 +39,9 @@
    61.4  
    61.5  "$FREEBUSY_SCRIPT" "$USER" $FREEBUSY_ARGS $ARGS 2>> $ERROR
    61.6  
    61.7 -   grep -q 'event14@example.com' "$STORE/$USER/freebusy-providers" \
    61.8 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_providers" \
    61.9 +|  tee out3p.tmp \
   61.10 +|  grep -q 'event14@example.com' \
   61.11  && echo "Success" \
   61.12  || echo "Failed"
   61.13  
   61.14 @@ -57,7 +59,10 @@
   61.15  && echo "Success" \
   61.16  || echo "Failed"
   61.17  
   61.18 -   ! grep -q 'event14@example.com' "$STORE/$USER/freebusy-providers" \
   61.19 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_providers" \
   61.20 +>  out4p.tmp
   61.21 +
   61.22 +   ! grep -q 'event14@example.com' "out4p.tmp" \
   61.23  && echo "Success" \
   61.24  || echo "Failed"
   61.25  
   61.26 @@ -83,6 +88,8 @@
   61.27  && echo "Success" \
   61.28  || echo "Failed"
   61.29  
   61.30 -   grep -q 'event14@example.com' "$STORE/$USER/freebusy-providers" \
   61.31 +   "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_providers" \
   61.32 +|  tee out6p.tmp \
   61.33 +|  grep -q 'event14@example.com' \
   61.34  && echo "Success" \
   61.35  || echo "Failed"
    62.1 --- a/tools/config.sh	Tue Apr 19 21:20:57 2016 +0200
    62.2 +++ b/tools/config.sh	Fri Apr 22 16:22:58 2016 +0200
    62.3 @@ -1,7 +1,16 @@
    62.4  #!/bin/sh
    62.5  
    62.6 +STORE_TYPE=file
    62.7  IMIP_AGENT_USER=imip-agent
    62.8  IMIP_AGENT_GROUP=lmtp
    62.9  INSTALL_DIR=/var/lib/imip-agent
   62.10  WEB_INSTALL_DIR=/var/www/imip-agent
   62.11  CONFIG_DIR=/etc/imip-agent
   62.12 +
   62.13 +# Store-specific settings.
   62.14 +
   62.15 +# For STORE_TYPE=postgresql...
   62.16 +
   62.17 +POSTGRESQL_DB=imip_agent
   62.18 +POSTGRESQL_USERS="imip-agent www-data"
   62.19 +AS_POSTGRES="sudo -u postgres"
    63.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    63.2 +++ b/tools/copy_store.py	Fri Apr 22 16:22:58 2016 +0200
    63.3 @@ -0,0 +1,177 @@
    63.4 +#!/usr/bin/env python
    63.5 +
    63.6 +"""
    63.7 +Copy store information into another store.
    63.8 +
    63.9 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
   63.10 +
   63.11 +This program is free software; you can redistribute it and/or modify it under
   63.12 +the terms of the GNU General Public License as published by the Free Software
   63.13 +Foundation; either version 3 of the License, or (at your option) any later
   63.14 +version.
   63.15 +
   63.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   63.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   63.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   63.19 +details.
   63.20 +
   63.21 +You should have received a copy of the GNU General Public License along with
   63.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   63.23 +"""
   63.24 +
   63.25 +from os.path import abspath, split
   63.26 +import sys
   63.27 +
   63.28 +# Find the modules.
   63.29 +
   63.30 +try:
   63.31 +    import imiptools
   63.32 +except ImportError:
   63.33 +    parent = abspath(split(split(__file__)[0])[0])
   63.34 +    if split(parent)[1] == "imip-agent":
   63.35 +        sys.path.append(parent)
   63.36 +
   63.37 +from imiptools import config
   63.38 +from imiptools.data import Object
   63.39 +from imiptools.stores import get_store, get_publisher, get_journal
   63.40 +
   63.41 +def copy_store(from_store, from_journal, to_store, to_journal):
   63.42 +
   63.43 +    """
   63.44 +    Copy stored information from the specified 'from_store' and 'from_journal'
   63.45 +    to the specified 'to_store' and 'to_journal' respectively.
   63.46 +    """
   63.47 +
   63.48 +    # For each user...
   63.49 +
   63.50 +    for user in from_store.get_users():
   63.51 +
   63.52 +        # Copy requests.
   63.53 +
   63.54 +        requests = from_store.get_requests(user)
   63.55 +        if requests:
   63.56 +            to_store.set_requests(user, requests)
   63.57 +
   63.58 +        # Copy events, both active and cancellations.
   63.59 +
   63.60 +        for dirname in (None, "cancellations"):
   63.61 +
   63.62 +            # Get event, recurrence information.
   63.63 +
   63.64 +            for uid, recurrenceid in from_store.get_all_events(user, dirname=dirname):
   63.65 +                d = from_store.get_event(user, uid, recurrenceid, dirname=dirname)
   63.66 +                if d:
   63.67 +                    to_store.set_event(user, uid, recurrenceid, Object(d).to_node())
   63.68 +                    if dirname == "cancellations":
   63.69 +                        to_store.cancel_event(user, uid, recurrenceid)
   63.70 +                else:
   63.71 +                    print >>sys.stderr, "Event for %s with UID %s and RECURRENCE-ID %s not found in %s" % (
   63.72 +                        (user, uid, recurrenceid or "null", dirname or "active events"))
   63.73 +
   63.74 +                # Copy counter-proposals.
   63.75 +
   63.76 +                if dirname is None:
   63.77 +                    for other in from_store.get_counters(user, uid, recurrenceid):
   63.78 +                        d = from_store.get_counter(user, other, uid, recurrenceid)
   63.79 +                        if d:
   63.80 +                            to_store.set_counter(user, other, Object(d).to_node(), uid, recurrenceid)
   63.81 +                        else:
   63.82 +                            print >>sys.stderr, "Counter-proposal for %s with UID %s and RECURRENCE-ID %s not found in %s" % (
   63.83 +                                (user, uid, recurrenceid or "null", dirname or "active events"))
   63.84 +
   63.85 +        # Copy free/busy information for the user.
   63.86 +
   63.87 +        freebusy = from_store.get_freebusy(user)
   63.88 +        if freebusy:
   63.89 +            to_store.set_freebusy(user, freebusy)
   63.90 +
   63.91 +        # Copy free/busy information for other users.
   63.92 +
   63.93 +        for other in from_store.get_freebusy_others(user):
   63.94 +            freebusy = from_store.get_freebusy_for_other(user, other)
   63.95 +            if freebusy:
   63.96 +                to_store.set_freebusy_for_other(user, freebusy, other)
   63.97 +
   63.98 +        # Copy free/busy offers.
   63.99 +
  63.100 +        offers = from_store.get_freebusy_offers(user)
  63.101 +        if offers:
  63.102 +            to_store.set_freebusy_offers(user, offers)
  63.103 +
  63.104 +    # For each quota group...
  63.105 +
  63.106 +    for quota in from_journal.get_quotas():
  63.107 +
  63.108 +        # Copy quota limits.
  63.109 +
  63.110 +        for user_group, limit in from_journal.get_limits(quota).items():
  63.111 +            to_journal.set_limit(quota, user_group, limit)
  63.112 +
  63.113 +        # Copy group mappings.
  63.114 +
  63.115 +        for store_user, user_group in from_journal.get_groups(quota).items():
  63.116 +            to_journal.set_group(quota, store_user, user_group)
  63.117 +
  63.118 +        # Copy journal details.
  63.119 +
  63.120 +        for group in from_journal.get_quota_users(quota):
  63.121 +            to_journal.set_entries(quota, group, from_journal.get_entries(quota, group))
  63.122 +
  63.123 +        # Copy individual free/busy details.
  63.124 +
  63.125 +        for store_user in from_journal.get_freebusy_users(quota):
  63.126 +            to_journal.set_freebusy(store_user, from_journal.get_freebusy(store_user))
  63.127 +
  63.128 +# Main program.
  63.129 +
  63.130 +if __name__ == "__main__":
  63.131 +
  63.132 +    # Interpret the command line arguments.
  63.133 +
  63.134 +    from_store_args = []
  63.135 +    to_store_args = []
  63.136 +    l = ignored = []
  63.137 +
  63.138 +    for arg in sys.argv[1:]:
  63.139 +        if arg in ("-t", "--to"):
  63.140 +            l = to_store_args
  63.141 +        elif arg in ("-f", "--from"):
  63.142 +            l = from_store_args
  63.143 +        else:
  63.144 +            l.append(arg)
  63.145 +
  63.146 +    if len(from_store_args) not in (0, 3) or len(to_store_args) != 3:
  63.147 +        print >>sys.stderr, """\
  63.148 +Usage: %s \\
  63.149 +       [ ( -f | --from ) <store type> <store directory> <journal directory> ] \\
  63.150 +         ( -t | --to ) <store type> <store directory> <journal directory>
  63.151 +
  63.152 +Need details of a destination store indicated by the -t or --to option.
  63.153 +In addition, details of a source store may be indicated by the -f or --from
  63.154 +option; otherwise, the currently-configured store is used.
  63.155 +""" % split(sys.argv[0])[1]
  63.156 +        sys.exit(1)
  63.157 +
  63.158 +    # Override defaults if indicated.
  63.159 +
  63.160 +    getvalue = lambda value, pos=0, default=None: value and value[pos] or default
  63.161 +
  63.162 +    from_store_type = getvalue(from_store_args, 0, config.STORE_TYPE)
  63.163 +    from_store_dir = getvalue(from_store_args, 1)
  63.164 +    from_journal_dir = getvalue(from_store_args, 2)
  63.165 +
  63.166 +    to_store_type, to_store_dir, to_journal_dir = to_store_args
  63.167 +
  63.168 +    # Obtain store-related objects.
  63.169 +
  63.170 +    from_store = get_store(from_store_type, from_store_dir)
  63.171 +    from_journal = get_journal(from_store_type, from_journal_dir)
  63.172 +
  63.173 +    to_store = get_store(to_store_type, to_store_dir)
  63.174 +    to_journal = get_journal(to_store_type, to_journal_dir)
  63.175 +
  63.176 +    # Process the store.
  63.177 +
  63.178 +    copy_store(from_store, from_journal, to_store, to_journal)
  63.179 +
  63.180 +# vim: tabstop=4 expandtab shiftwidth=4
    64.1 --- a/tools/fix.sh	Tue Apr 19 21:20:57 2016 +0200
    64.2 +++ b/tools/fix.sh	Fri Apr 22 16:22:58 2016 +0200
    64.3 @@ -3,38 +3,45 @@
    64.4  DIRNAME=`dirname "$0"`
    64.5  
    64.6  if [ -e "$DIRNAME/config.sh" ]; then
    64.7 -    . "$DIRNAME/config.sh"
    64.8 +    CONFIG="$DIRNAME/config.sh"
    64.9 +    . "$CONFIG"
   64.10  else
   64.11 -    . /etc/imip-agent/config.sh
   64.12 +    CONFIG=/etc/imip-agent/config.sh
   64.13 +    . "$CONFIG"
   64.14  fi
   64.15  
   64.16  PROGNAME=`basename "$0"`
   64.17  
   64.18  if [ "$1" = "--help" ]; then
   64.19      cat 1>&2 <<EOF
   64.20 -Usage: $PROGNAME [ <stored data directory> [ <published data directory> [ <user> [ <group> ] ] ] ]
   64.21 +Usage: $PROGNAME
   64.22 +
   64.23 +Fix permissions for the stored and published data directories, operating on...
   64.24  
   64.25 -Fix permissions for the stored and published data directories, operating on the
   64.26 -given stored data and published data directories (or, respectively,
   64.27 -$INSTALL_DIR and $WEB_INSTALL_DIR if omitted).
   64.28 +  * $INSTALL_DIR
   64.29 +  * $WEB_INSTALL_DIR
   64.30  
   64.31 -Set ownership and membership using the given user and group (or, respectively,
   64.32 -$IMIP_AGENT_USER and $IMIP_AGENT_GROUP if omitted).
   64.33 +...respectively.
   64.34 +
   64.35 +Set ownership and membership to the user and group respectively given as
   64.36 +$IMIP_AGENT_USER and $IMIP_AGENT_GROUP.
   64.37  EOF
   64.38      exit 1
   64.39  fi
   64.40  
   64.41 -INSTALL_DIR=${1:-$INSTALL_DIR}
   64.42 -WEB_INSTALL_DIR=${2:-$WEB_INSTALL_DIR}
   64.43 -USER=${3:-$IMIP_AGENT_USER}
   64.44 -GROUP=${4:-$IMIP_AGENT_GROUP}
   64.45 +chown -R "$IMIP_AGENT_USER" "$INSTALL_DIR"
   64.46 +chgrp -R "$IMIP_AGENT_GROUP" "$INSTALL_DIR"
   64.47  
   64.48 -chown -R "$USER" "$INSTALL_DIR"
   64.49 -chgrp -R "$GROUP" "$INSTALL_DIR"
   64.50 -
   64.51 -for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static \
   64.52 -           "$INSTALL_DIR"/journal ; do
   64.53 -    chown -R "$USER" "$DIR"
   64.54 -    chgrp -R "$GROUP" "$DIR"
   64.55 +for DIR in "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static ; do
   64.56 +    chown -R "$IMIP_AGENT_USER" "$DIR"
   64.57 +    chgrp -R "$IMIP_AGENT_GROUP" "$DIR"
   64.58      chmod -R g+w "$DIR"
   64.59  done
   64.60 +
   64.61 +if [ "$STORE_TYPE" = "file" ]; then
   64.62 +    for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/journal ; do
   64.63 +        chown -R "$IMIP_AGENT_USER" "$DIR"
   64.64 +        chgrp -R "$IMIP_AGENT_GROUP" "$DIR"
   64.65 +        chmod -R g+w "$DIR"
   64.66 +    done
   64.67 +fi
    65.1 --- a/tools/init.sh	Tue Apr 19 21:20:57 2016 +0200
    65.2 +++ b/tools/init.sh	Fri Apr 22 16:22:58 2016 +0200
    65.3 @@ -1,49 +1,169 @@
    65.4  #!/bin/sh
    65.5  
    65.6  DIRNAME=`dirname "$0"`
    65.7 +CONFIG="$DIRNAME/config.sh"
    65.8  
    65.9 -if [ -e "$DIRNAME/config.sh" ]; then
   65.10 -    . "$DIRNAME/config.sh"
   65.11 +if [ -e "$CONFIG" ]; then
   65.12 +    . "$CONFIG"
   65.13  else
   65.14 -    . /etc/imip-agent/config.sh
   65.15 +    CONFIG=/etc/imip-agent/config.sh
   65.16 +    . "$CONFIG"
   65.17 +fi
   65.18 +
   65.19 +SCHEMA="$DIRNAME/../conf/postgresql/schema.sql"
   65.20 +
   65.21 +if [ ! -e "$SCHEMA" ]; then
   65.22 +    SCHEMA=/etc/imip-agent/postgresql/schema.sql
   65.23  fi
   65.24  
   65.25  PROGNAME=`basename "$0"`
   65.26  
   65.27  if [ "$1" = "--help" ]; then
   65.28      cat 1>&2 <<EOF
   65.29 -Usage: $PROGNAME [ <stored data directory> [ <published data directory> [ <user> [ <group> ] ] ] ]
   65.30 +Usage: $PROGNAME
   65.31 +
   65.32 +Initialise stored and published data directories at...
   65.33  
   65.34 -Initialise stored and published data directories either at any specified
   65.35 -locations or, respectively, at $INSTALL_DIR and $WEB_INSTALL_DIR.
   65.36 +  * $INSTALL_DIR
   65.37 +  * $WEB_INSTALL_DIR
   65.38  
   65.39 -Set permissions to the given user and group or, respectively, to $IMIP_AGENT_USER
   65.40 +...respectively.
   65.41 +
   65.42 +Set permissions to the user and group respectively given as $IMIP_AGENT_USER
   65.43  and $IMIP_AGENT_GROUP.
   65.44  
   65.45 -Within the stored data directory (using $INSTALL_DIR as an example), the
   65.46 -following directories are created:
   65.47 +Within the stored data directory, the following directories will be created
   65.48 +(with STORE_TYPE currently set as "$STORE_TYPE"):
   65.49  
   65.50 -  * $INSTALL_DIR/journal
   65.51    * $INSTALL_DIR/preferences
   65.52 -  * $INSTALL_DIR/store
   65.53 +EOF
   65.54  
   65.55 -Within the published data directory (using $WEB_INSTALL_DIR as an example), the
   65.56 -following directory is created:
   65.57 +    if [ "$STORE_TYPE" = "file" ]; then
   65.58 +        cat 1>&2 <<EOF
   65.59 +  * $INSTALL_DIR/journal (if STORE_TYPE is "file")
   65.60 +  * $INSTALL_DIR/store (if STORE_TYPE is "file")
   65.61 +EOF
   65.62 +    fi
   65.63 +
   65.64 +    cat 1>&2 <<EOF
   65.65 +
   65.66 +Within the published data directory the following directory will be created:
   65.67  
   65.68    * $WEB_INSTALL_DIR/static
   65.69  EOF
   65.70 +
   65.71 +    if [ "$STORE_TYPE" = "postgresql" ]; then
   65.72 +        cat 1>&2 <<EOF
   65.73 +
   65.74 +With STORE_TYPE set as "database", a database schema will be initialised for the
   65.75 +following database:
   65.76 +
   65.77 +  * $POSTGRESQL_DB
   65.78 +EOF
   65.79 +    fi
   65.80 +
   65.81 +    cat 1>&2 <<EOF
   65.82 +
   65.83 +See $CONFIG for the settings used as described above.
   65.84 +EOF
   65.85 +    exit 1
   65.86 +fi
   65.87 +
   65.88 +# Test for a privileged user.
   65.89 +
   65.90 +if [ `whoami` != 'root' ]; then
   65.91 +    cat 1>&2 <<EOF
   65.92 +You will need to become a privileged user using su or sudo to run this program
   65.93 +because it changes file ownership and may also switch users to run database
   65.94 +administration commands.
   65.95 +EOF
   65.96      exit 1
   65.97  fi
   65.98  
   65.99 -INSTALL_DIR=${1:-$INSTALL_DIR}
  65.100 -WEB_INSTALL_DIR=${2:-$WEB_INSTALL_DIR}
  65.101 -USER=${3:-$IMIP_AGENT_USER}
  65.102 -GROUP=${4:-$IMIP_AGENT_GROUP}
  65.103 +# Create necessary directories regardless of store type.
  65.104 +
  65.105 +echo "Creating preferences and static Web directories..." 1>&2
  65.106  
  65.107 -for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static \
  65.108 -           "$INSTALL_DIR"/journal ; do
  65.109 +for DIR in "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static ; do
  65.110      mkdir -p "$DIR"
  65.111 -    chown "$USER" "$DIR"
  65.112 -    chgrp "$GROUP" "$DIR"
  65.113 +    chown "$IMIP_AGENT_USER" "$DIR"
  65.114 +    chgrp "$IMIP_AGENT_GROUP" "$DIR"
  65.115      chmod g+ws "$DIR"
  65.116  done
  65.117 +
  65.118 +# Initialise a file store.
  65.119 +
  65.120 +if [ "$STORE_TYPE" = "file" ]; then
  65.121 +
  65.122 +    echo "Creating store and journal directories..." 1>&2
  65.123 +
  65.124 +    for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/journal ; do
  65.125 +        mkdir -p "$DIR"
  65.126 +        chown "$IMIP_AGENT_USER" "$DIR"
  65.127 +        chgrp "$IMIP_AGENT_GROUP" "$DIR"
  65.128 +        chmod g+ws "$DIR"
  65.129 +    done
  65.130 +
  65.131 +# Initialise a PostgreSQL store.
  65.132 +
  65.133 +elif [ "$STORE_TYPE" = "postgresql" ]; then
  65.134 +
  65.135 +    # Check for the database.
  65.136 +
  65.137 +    echo "Checking for the database ${POSTGRESQL_DB}..." 1>&2
  65.138 +
  65.139 +    if $AS_POSTGRES psql -tA -c 'select datname from pg_database' postgres | grep -q ^"$POSTGRESQL_DB"$ ; then
  65.140 +        cat 1>&2 <<EOF
  65.141 +Database $POSTGRESQL_DB already exists.
  65.142 +EOF
  65.143 +        exit 1
  65.144 +    fi
  65.145 +
  65.146 +    # Attempt to create the database.
  65.147 +
  65.148 +    echo "Creating database ${POSTGRESQL_DB}..." 1>&2
  65.149 +
  65.150 +    if ! $AS_POSTGRES createdb "$POSTGRESQL_DB" ; then
  65.151 +        cat 1>&2 <<EOF
  65.152 +Could not create database $POSTGRESQL_DB using createdb.
  65.153 +EOF
  65.154 +        exit 1
  65.155 +    fi
  65.156 +
  65.157 +    # Attempt to initialise the schema.
  65.158 +
  65.159 +    echo "Initialising the schema for database ${POSTGRESQL_DB}..." 1>&2
  65.160 +
  65.161 +    if ! $AS_POSTGRES psql -q -f "$SCHEMA" "$POSTGRESQL_DB" ; then
  65.162 +        cat 1>&2 <<EOF
  65.163 +Could not initialise schema in database $POSTGRESQL_DB using psql.
  65.164 +EOF
  65.165 +        exit 1
  65.166 +    fi
  65.167 +
  65.168 +    # For each user needing to connect, attempt to create a role and grant it
  65.169 +    # privileges on the tables.
  65.170 +
  65.171 +    for USER in $POSTGRESQL_USERS ; do
  65.172 +
  65.173 +        echo "Creating a database user for ${USER}..." 1>&2
  65.174 +
  65.175 +        if ! $AS_POSTGRES createuser -D -R -S "$USER" ; then
  65.176 +            cat 1>&2 <<EOF
  65.177 +Could not create database user $USER using createuser.
  65.178 +EOF
  65.179 +        fi
  65.180 +
  65.181 +        echo "Granting privileges to database user for ${USER}..." 1>&2
  65.182 +
  65.183 +        if ! $AS_POSTGRES psql -Atc '\dt' "$POSTGRESQL_DB" \
  65.184 +           | cut -d '|' -f 2 \
  65.185 +           | xargs -I{} $AS_POSTGRES psql -q -c "grant all privileges on table {} to \"$USER\"" "$POSTGRESQL_DB" ; then
  65.186 +
  65.187 +            cat 1>&2 <<EOF
  65.188 +Could not grant permissions for schema in database $POSTGRESQL_DB to $USER
  65.189 +using psql.
  65.190 +EOF
  65.191 +        fi
  65.192 +    done
  65.193 +fi
    66.1 --- a/tools/init_user.sh	Tue Apr 19 21:20:57 2016 +0200
    66.2 +++ b/tools/init_user.sh	Fri Apr 22 16:22:58 2016 +0200
    66.3 @@ -3,36 +3,76 @@
    66.4  DIRNAME=`dirname "$0"`
    66.5  
    66.6  if [ -e "$DIRNAME/config.sh" ]; then
    66.7 -    . "$DIRNAME/config.sh"
    66.8 +    CONFIG="$DIRNAME/config.sh"
    66.9 +    . "$CONFIG"
   66.10  else
   66.11 -    . /etc/imip-agent/config.sh
   66.12 +    CONFIG=/etc/imip-agent/config.sh
   66.13 +    . "$CONFIG"
   66.14  fi
   66.15  
   66.16  PROGNAME=`basename "$0"`
   66.17  
   66.18  if [ "$1" = "--help" ] || [ ! "$1" ]; then
   66.19      cat 1>&2 <<EOF
   66.20 -Usage: $PROGNAME <calendar user> [ <stored data directory> [ <published data directory> [ <user> ] ] ]
   66.21 +Usage: $PROGNAME <calendar user>
   66.22  
   66.23  Initialise a given calendar user within an existing installation, creating
   66.24 -resources within the given stored data and published data directories or,
   66.25 -respectively, within $INSTALL_DIR and $WEB_INSTALL_DIR.
   66.26 +resources within the given stored data and published data directories...
   66.27 +
   66.28 +  * $INSTALL_DIR
   66.29 +  * $WEB_INSTALL_DIR
   66.30 +
   66.31 +...respectively.
   66.32  
   66.33 -The resources will be defined as having the given system user as owner or,
   66.34 -if the user is omitted, the $IMIP_AGENT_USER as owner.
   66.35 +The resources will be defined as having $IMIP_AGENT_USER as owner.
   66.36 +
   66.37 +See $CONFIG for the settings used as described above.
   66.38 +
   66.39 +Example:
   66.40 +
   66.41 +$PROGNAME mailto:vincent.vole@example.com
   66.42  EOF
   66.43      exit 1
   66.44  fi
   66.45  
   66.46  CALENDAR_USER=$1
   66.47 -INSTALL_DIR=${2:-$INSTALL_DIR}
   66.48 -WEB_INSTALL_DIR=${3:-$WEB_INSTALL_DIR}
   66.49 -USER=${4:-$IMIP_AGENT_USER}
   66.50 +
   66.51 +if [ ! "$CALENDAR_USER" ]; then
   66.52 +    cat 1>&2 <<EOF
   66.53 +Need a calendar user to initialise.
   66.54 +EOF
   66.55 +    exit 1
   66.56 +fi
   66.57 +
   66.58 +# Test for a privileged user.
   66.59  
   66.60 -for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static \
   66.61 -           "$INSTALL_DIR"/journal ; do
   66.62 +if [ `whoami` != 'root' ]; then
   66.63 +    cat 1>&2 <<EOF
   66.64 +You will need to become a privileged user using su or sudo to run this program
   66.65 +because it changes file ownership.
   66.66 +EOF
   66.67 +    exit 1
   66.68 +fi
   66.69 +
   66.70 +# Initialise the directories.
   66.71 +
   66.72 +echo "Creating preferences and static Web directories..." 1>&2
   66.73 +
   66.74 +for DIR in "$INSTALL_DIR"/preferences "$WEB_INSTALL_DIR"/static ; do
   66.75      mkdir -p "$DIR/$CALENDAR_USER"
   66.76 -    chown "$USER" "$DIR/$CALENDAR_USER"
   66.77 +    chown "$IMIP_AGENT_USER" "$DIR/$CALENDAR_USER"
   66.78      chmod g+ws "$DIR/$CALENDAR_USER"
   66.79      # Group privileges should already be set.
   66.80  done
   66.81 +
   66.82 +if [ "$STORE_TYPE" = "file" ]; then
   66.83 +
   66.84 +    echo "Creating store and journal directories..." 1>&2
   66.85 +
   66.86 +    for DIR in "$INSTALL_DIR"/store "$INSTALL_DIR"/journal ; do
   66.87 +        mkdir -p "$DIR/$CALENDAR_USER"
   66.88 +        chown "$IMIP_AGENT_USER" "$DIR/$CALENDAR_USER"
   66.89 +        chmod g+ws "$DIR/$CALENDAR_USER"
   66.90 +        # Group privileges should already be set.
   66.91 +    done
   66.92 +fi
    67.1 --- a/tools/install.sh	Tue Apr 19 21:20:57 2016 +0200
    67.2 +++ b/tools/install.sh	Fri Apr 22 16:22:58 2016 +0200
    67.3 @@ -28,6 +28,7 @@
    67.4  
    67.5  for DIR in "$INSTALL_DIR/imiptools" \
    67.6             "$INSTALL_DIR/imiptools/stores" \
    67.7 +           "$INSTALL_DIR/imiptools/stores/database" \
    67.8             "$INSTALL_DIR/imiptools/handlers" \
    67.9             "$INSTALL_DIR/imiptools/handlers/scheduling" ; do
   67.10      if [ ! -e "$DIR" ]; then
   67.11 @@ -45,6 +46,7 @@
   67.12  
   67.13  cp imiptools/*.py "$INSTALL_DIR/imiptools/"
   67.14  cp imiptools/stores/*.py "$INSTALL_DIR/imiptools/stores/"
   67.15 +cp imiptools/stores/database/*.py "$INSTALL_DIR/imiptools/stores/database/"
   67.16  cp imiptools/handlers/*.py "$INSTALL_DIR/imiptools/handlers/"
   67.17  cp imiptools/handlers/scheduling/*.py "$INSTALL_DIR/imiptools/handlers/scheduling/"
   67.18  
   67.19 @@ -85,9 +87,23 @@
   67.20  
   67.21  ln -s "$CONFIG_DIR/config.py" "$INSTALL_DIR/imiptools/config.py"
   67.22  
   67.23 +# Copy related configuration files.
   67.24 +
   67.25 +if [ ! -e "$CONFIG_DIR/postgresql" ]; then
   67.26 +    mkdir -p "$CONFIG_DIR/postgresql"
   67.27 +fi
   67.28 +
   67.29 +if [ -e "$CONFIG_DIR/postgresql/schema.sql" ]; then
   67.30 +    if ! cmp "conf/postgresql/schema.sql" "$CONFIG_DIR/postgresql/schema.sql" > /dev/null 2>&1 ; then
   67.31 +        cp "conf/postgresql/schema.sql" "$CONFIG_DIR/postgresql/schema.sql.new"
   67.32 +    fi
   67.33 +else
   67.34 +    cp "conf/postgresql/schema.sql" "$CONFIG_DIR/postgresql/schema.sql"
   67.35 +fi
   67.36 +
   67.37  # Tools
   67.38  
   67.39 -TOOLS="fix.sh init.sh init_user.sh make_freebusy.py update_quotas.py update_scheduling_modules.py"
   67.40 +TOOLS="copy_store.py fix.sh init.sh init_user.sh make_freebusy.py set_quota_limit.py update_quotas.py update_scheduling_modules.py"
   67.41  
   67.42  if [ ! -e "$INSTALL_DIR/tools" ]; then
   67.43      mkdir -p "$INSTALL_DIR/tools"
    68.1 --- a/tools/make_freebusy.py	Tue Apr 19 21:20:57 2016 +0200
    68.2 +++ b/tools/make_freebusy.py	Fri Apr 22 16:22:58 2016 +0200
    68.3 @@ -21,7 +21,7 @@
    68.4  this program.  If not, see <http://www.gnu.org/licenses/>.
    68.5  """
    68.6  
    68.7 -from os.path import split
    68.8 +from os.path import abspath, split
    68.9  import sys
   68.10  
   68.11  # Find the modules.
   68.12 @@ -29,16 +29,17 @@
   68.13  try:
   68.14      import imiptools
   68.15  except ImportError:
   68.16 -    parent = split(split(__file__)[0])[0]
   68.17 +    parent = abspath(split(split(__file__)[0])[0])
   68.18      if split(parent)[1] == "imip-agent":
   68.19          sys.path.append(parent)
   68.20  
   68.21  from codecs import getwriter
   68.22 +from imiptools import config
   68.23  from imiptools.client import Client
   68.24  from imiptools.data import get_window_end, Object
   68.25  from imiptools.dates import get_default_timezone, to_utc_datetime
   68.26 -from imiptools.period import insert_period
   68.27 -from imiptools.stores.file import FileStore, FilePublisher, FileJournal
   68.28 +from imiptools.period import FreeBusyCollection
   68.29 +from imiptools.stores import get_store, get_publisher, get_journal
   68.30  
   68.31  def make_freebusy(client, participant, store_and_publish, include_needs_action,
   68.32      reset_updated_list, verbose):
   68.33 @@ -86,7 +87,7 @@
   68.34  
   68.35      if not all_events:
   68.36          all_events = store.get_all_events(user)
   68.37 -        fb = []
   68.38 +        fb = FreeBusyCollection()
   68.39  
   68.40      # With providers of additional periods, append to the existing collection.
   68.41  
   68.42 @@ -115,7 +116,7 @@
   68.43          if obj.get_participation(partstat, include_needs_action):
   68.44              for p in obj.get_active_periods(recurrenceids, tzid, window_end):
   68.45                  fbp = obj.get_freebusy_period(p, partstat == "ORG")
   68.46 -                insert_period(fb, fbp)
   68.47 +                fb.insert_period(fbp)
   68.48  
   68.49      # Store and publish the free/busy collection.
   68.50  
   68.51 @@ -148,6 +149,7 @@
   68.52  
   68.53      participants = []
   68.54      args = []
   68.55 +    store_type = []
   68.56      store_dir = []
   68.57      publishing_dir = []
   68.58      journal_dir = []
   68.59 @@ -163,6 +165,8 @@
   68.60          if arg in ("-n", "-s", "-v", "-r"):
   68.61              args.append(arg)
   68.62              l = ignored
   68.63 +        elif arg == "-T":
   68.64 +            l = store_type
   68.65          elif arg == "-S":
   68.66              l = store_dir
   68.67          elif arg == "-P":
   68.68 @@ -178,20 +182,26 @@
   68.69          user = participants[0]
   68.70      except IndexError:
   68.71          print >>sys.stderr, """\
   68.72 -Usage: %s <user> [ <other user> ] <options>
   68.73 +Usage: %s <user> [ <other user> ] [ <options> ]
   68.74  
   68.75  Need a user and an optional participant (if different from the user),
   68.76  along with the -s option if updating the store and the published details.
   68.77 -Specify -n to include objects with PARTSTAT of NEEDS-ACTION.
   68.78 -Specify -r to inspect all objects, not just those expected to provide details.
   68.79 -Specify -v for additional messages on standard error.
   68.80 +
   68.81 +Specific options:
   68.82 +
   68.83 +-s  Update the store and published details (write details to standard output
   68.84 +    otherwise)
   68.85 +-n  Include objects with PARTSTAT of NEEDS-ACTION
   68.86 +-r  Inspect all objects, not just those expected to provide details
   68.87 +-v  Show additional messages on standard error
   68.88  
   68.89  General options:
   68.90  
   68.91 --j  indicate the journal directory location
   68.92 --p  indicate the preferences directory location
   68.93 --P  indicate the publishing directory location
   68.94 --S  indicate the store directory location
   68.95 +-j  Indicates the journal directory location
   68.96 +-p  Indicates the preferences directory location
   68.97 +-P  Indicates the publishing directory location
   68.98 +-S  Indicates the store directory location
   68.99 +-T  Indicates the store type (the configured value if omitted)
  68.100  """ % split(sys.argv[0])[1]
  68.101          sys.exit(1)
  68.102  
  68.103 @@ -205,16 +215,19 @@
  68.104  
  68.105      # Override defaults if indicated.
  68.106  
  68.107 -    store_dir = store_dir and store_dir[0] or None
  68.108 -    publishing_dir = publishing_dir and publishing_dir[0] or None
  68.109 -    journal_dir = journal_dir and journal_dir[0] or None
  68.110 -    preferences_dir = preferences_dir and preferences_dir[0] or None
  68.111 +    getvalue = lambda value, default=None: value and value[0] or default
  68.112  
  68.113 -    # Obtain store-related objects.
  68.114 +    store_type = getvalue(store_type, config.STORE_TYPE)
  68.115 +    store_dir = getvalue(store_dir)
  68.116 +    publishing_dir = getvalue(publishing_dir)
  68.117 +    journal_dir = getvalue(journal_dir)
  68.118 +    preferences_dir = getvalue(preferences_dir)
  68.119  
  68.120 -    store = FileStore(store_dir)
  68.121 -    publisher = FilePublisher(publishing_dir)
  68.122 -    journal = FileJournal(journal_dir)
  68.123 +    # Obtain store-related objects or delegate this to the Client initialiser.
  68.124 +
  68.125 +    store = get_store(store_type, store_dir)
  68.126 +    publisher = get_publisher(publishing_dir)
  68.127 +    journal = get_journal(store_type, journal_dir)
  68.128  
  68.129      # Obtain a list of users for processing.
  68.130  
    69.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    69.2 +++ b/tools/sendmail.py	Fri Apr 22 16:22:58 2016 +0200
    69.3 @@ -0,0 +1,9 @@
    69.4 +#!/usr/bin/env python
    69.5 +
    69.6 +import smtplib
    69.7 +import sys
    69.8 +
    69.9 +sender, recipients = sys.argv[1], sys.argv[2:]
   69.10 +
   69.11 +s = smtplib.SMTP("localhost")
   69.12 +print s.sendmail(sender, recipients, sys.stdin.read())
    70.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    70.2 +++ b/tools/set_quota_limit.py	Fri Apr 22 16:22:58 2016 +0200
    70.3 @@ -0,0 +1,85 @@
    70.4 +#!/usr/bin/env python
    70.5 +
    70.6 +"""
    70.7 +Set a quota limit for a user group.
    70.8 +
    70.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
   70.10 +
   70.11 +This program is free software; you can redistribute it and/or modify it under
   70.12 +the terms of the GNU General Public License as published by the Free Software
   70.13 +Foundation; either version 3 of the License, or (at your option) any later
   70.14 +version.
   70.15 +
   70.16 +This program is distributed in the hope that it will be useful, but WITHOUT
   70.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   70.18 +FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   70.19 +details.
   70.20 +
   70.21 +You should have received a copy of the GNU General Public License along with
   70.22 +this program.  If not, see <http://www.gnu.org/licenses/>.
   70.23 +"""
   70.24 +
   70.25 +from os.path import abspath, split
   70.26 +import sys
   70.27 +
   70.28 +# Find the modules.
   70.29 +
   70.30 +try:
   70.31 +    import imiptools
   70.32 +except ImportError:
   70.33 +    parent = abspath(split(split(__file__)[0])[0])
   70.34 +    if split(parent)[1] == "imip-agent":
   70.35 +        sys.path.append(parent)
   70.36 +
   70.37 +from imiptools import config
   70.38 +from imiptools.stores import get_journal
   70.39 +
   70.40 +# Main program.
   70.41 +
   70.42 +if __name__ == "__main__":
   70.43 +
   70.44 +    # Interpret the command line arguments.
   70.45 +
   70.46 +    args = []
   70.47 +    store_type = []
   70.48 +    journal_dir = []
   70.49 +
   70.50 +    # Collect quota details first, switching to other arguments when encountering
   70.51 +    # switches.
   70.52 +
   70.53 +    l = args
   70.54 +
   70.55 +    for arg in sys.argv[1:]:
   70.56 +        if arg == "-T":
   70.57 +            l = store_type
   70.58 +        elif arg == "-j":
   70.59 +            l = journal_dir
   70.60 +        else:
   70.61 +            l.append(arg)
   70.62 +
   70.63 +    try:
   70.64 +        quota, group, limit = args
   70.65 +    except ValueError:
   70.66 +        print >>sys.stderr, """\
   70.67 +Usage: %s <quota> <group> <limit> [ <options> ]
   70.68 +
   70.69 +General options:
   70.70 +
   70.71 +-j  Indicates the journal directory location
   70.72 +-T  Indicates the store type (the configured value if omitted)
   70.73 +""" % split(sys.argv[0])[1]
   70.74 +        sys.exit(1)
   70.75 +
   70.76 +    # Override defaults if indicated.
   70.77 +
   70.78 +    getvalue = lambda value, default=None: value and value[0] or default
   70.79 +
   70.80 +    store_type = getvalue(store_type, config.STORE_TYPE)
   70.81 +    journal_dir = getvalue(journal_dir)
   70.82 +
   70.83 +    # Obtain store-related objects.
   70.84 +
   70.85 +    journal = get_journal(store_type, journal_dir)
   70.86 +    journal.set_limit(quota, group, limit)
   70.87 +
   70.88 +# vim: tabstop=4 expandtab shiftwidth=4
    71.1 --- a/tools/update_quotas.py	Tue Apr 19 21:20:57 2016 +0200
    71.2 +++ b/tools/update_quotas.py	Fri Apr 22 16:22:58 2016 +0200
    71.3 @@ -19,7 +19,7 @@
    71.4  this program.  If not, see <http://www.gnu.org/licenses/>.
    71.5  """
    71.6  
    71.7 -from os.path import split
    71.8 +from os.path import abspath, split
    71.9  import sys
   71.10  
   71.11  # Find the modules.
   71.12 @@ -27,14 +27,15 @@
   71.13  try:
   71.14      import imiptools
   71.15  except ImportError:
   71.16 -    parent = split(split(__file__)[0])[0]
   71.17 +    parent = abspath(split(split(__file__)[0])[0])
   71.18      if split(parent)[1] == "imip-agent":
   71.19          sys.path.append(parent)
   71.20  
   71.21  from codecs import getwriter
   71.22 +from imiptools import config
   71.23  from imiptools.dates import get_datetime, get_default_timezone, get_time, \
   71.24                              to_utc_datetime
   71.25 -from imiptools.stores.file import FileJournal
   71.26 +from imiptools.stores import get_journal
   71.27  
   71.28  def remove_expired_entries(entries, expiry):
   71.29  
   71.30 @@ -110,6 +111,7 @@
   71.31  
   71.32      quotas = []
   71.33      args = []
   71.34 +    store_type = []
   71.35      journal_dir = []
   71.36      expiry = []
   71.37      ignored = []
   71.38 @@ -123,6 +125,8 @@
   71.39          if arg in ("-s", "-v"):
   71.40              args.append(arg)
   71.41              l = ignored
   71.42 +        elif arg == "-T":
   71.43 +            l = store_type
   71.44          elif arg == "-j":
   71.45              l = journal_dir
   71.46          elif arg == "-e":
   71.47 @@ -141,8 +145,9 @@
   71.48  
   71.49  General options:
   71.50  
   71.51 --e  indicate an expiry time for events (default is now)
   71.52 --j  indicate the journal directory location
   71.53 +-e  Indicates an expiry time for events (default is now)
   71.54 +-j  Indicates the journal directory location
   71.55 +-T  Indicates the store type (the configured value if omitted)
   71.56  """ % split(sys.argv[0])[1]
   71.57          sys.exit(1)
   71.58  
   71.59 @@ -153,8 +158,11 @@
   71.60  
   71.61      # Override defaults if indicated.
   71.62  
   71.63 -    journal_dir = journal_dir and journal_dir[0] or None
   71.64 -    expiry = expiry and expiry[0] or None
   71.65 +    getvalue = lambda value, default=None: value and value[0] or default
   71.66 +
   71.67 +    store_type = getvalue(store_type, config.STORE_TYPE)
   71.68 +    journal_dir = getvalue(journal_dir)
   71.69 +    expiry = getvalue(expiry)
   71.70  
   71.71      if expiry:
   71.72          expiry = to_utc_datetime(get_datetime(expiry), get_default_timezone())
   71.73 @@ -164,7 +172,7 @@
   71.74  
   71.75      # Obtain store-related objects.
   71.76  
   71.77 -    journal = FileJournal(journal_dir)
   71.78 +    journal = get_journal(store_type, journal_dir)
   71.79  
   71.80      # Obtain a list of users for processing.
   71.81