# HG changeset patch # User Paul Boddie # Date 1465148192 -7200 # Node ID 7153fc86ae7daf15ee6fe6270c8bb53c0dcd9fd8 # Parent a00216dc5cc81a6af901f1608237a11ba1609287 Support indefinitely-recurring events within quotas, recording events in journal storage so that the free/busy tool can expand the occupied periods. diff -r a00216dc5cc8 -r 7153fc86ae7d conf/cron/cron.daily/imip-agent --- a/conf/cron/cron.daily/imip-agent Sun Jun 05 19:34:36 2016 +0200 +++ b/conf/cron/cron.daily/imip-agent Sun Jun 05 19:36:32 2016 +0200 @@ -4,4 +4,5 @@ export PYTHONPATH="$INSTALL_DIR" "$INSTALL_DIR/tools/make_freebusy.py" all -n -s +"$INSTALL_DIR/tools/make_freebusy.py" all all -q -s "$INSTALL_DIR/tools/update_quotas.py" all -s diff -r a00216dc5cc8 -r 7153fc86ae7d docs/wiki/CronIntegration --- a/docs/wiki/CronIntegration Sun Jun 05 19:34:36 2016 +0200 +++ b/docs/wiki/CronIntegration Sun Jun 05 19:36:32 2016 +0200 @@ -4,6 +4,7 @@ the following: * Free/busy collections for all known users + * Free/busy collections for all known quotas * Quota records for all known quota groups This file should be copied to the appropriate destination. For example: @@ -30,7 +31,7 @@ Responsibility for generating free/busy expansions lies with the `tools/make_freebusy.py` program, which is a general tool that can also reset the free/busy records defined for a user or those made available to a -user. +user, as well as the free/busy records defined for a resource quota. == Quota Journals == diff -r a00216dc5cc8 -r 7153fc86ae7d docs/wiki/DatabaseStore --- a/docs/wiki/DatabaseStore Sun Jun 05 19:34:36 2016 +0200 +++ b/docs/wiki/DatabaseStore Sun Jun 05 19:36:32 2016 +0200 @@ -56,6 +56,19 @@ .. `freebusy` table in the data store; this may be the same table as the one employed .. by the data store to store received or deduced free/busy details == +`freebusy_providers` +|| Details of [[../EventRecurrences|recurring events]] for which new free/busy records +.. must be [[../CronIntegration|periodically generated]] because these events recur +.. indefinitely, selectable for each user (`store_user`) +== +`freebusy_provider_datetimes` +|| Date/time details associated with the `freebusy_providers` information +== +`quota_delegates` +|| Details of the identities (`store_user`) employing each quota (`quota`) to +.. consolidate their schedules, between which delegation may take place if their +.. schedules permit it +== `quota_limits` || A mapping from user identities or group identifiers to quota limits == diff -r a00216dc5cc8 -r 7153fc86ae7d docs/wiki/FilesystemUsage --- a/docs/wiki/FilesystemUsage Sun Jun 05 19:34:36 2016 +0200 +++ b/docs/wiki/FilesystemUsage Sun Jun 05 19:36:32 2016 +0200 @@ -34,15 +34,21 @@ == Journal Structure == Within the journal directory are a collection of subdirectories, each of which -represent a distinct quota group for one or more resources. When a user attempts -to reserve a resource in such a group, their ability to schedule that resource -will depend on how much they are using the other resources in that group. +represent a distinct quota group for one or more [[../Resources|resources]]. +When a user attempts to reserve a resource in such a group, their ability to +schedule that resource will depend on how much they are using the other +resources in that group. The directory for each quota group contains the following entries: {{{{#!table '''Entry''' || '''Purpose''' == +`delegates` +|| A file containing details of the identities employing this quota to consolidate +.. their schedules, between which delegation may take place if their schedules +.. permit it +== `freebusy-other` || A directory containing files, one per user (each containing period descriptions .. for reservations made by that user, in chronological order, structured @@ -55,6 +61,11 @@ freebusy-other/USER }}} == +`freebusy-providers` +|| A file containing details of [[../EventRecurrences|recurring events]] for which +.. new free/busy records must be [[../CronIntegration|periodically generated]] +.. because these events recur indefinitely +== `groups` || A mapping from user identities to group identifiers indicating the sharing .. of a quota across a number of users diff -r a00216dc5cc8 -r 7153fc86ae7d imiptools/handlers/scheduling/quota.py --- a/imiptools/handlers/scheduling/quota.py Sun Jun 05 19:34:36 2016 +0200 +++ b/imiptools/handlers/scheduling/quota.py Sun Jun 05 19:36:32 2016 +0200 @@ -20,7 +20,7 @@ """ from imiptools.dates import get_duration, to_utc_datetime -from imiptools.data import get_uri, uri_dict +from imiptools.data import get_uri, uri_dict, Object from imiptools.handlers.scheduling.common import get_scheduling_conflicts, \ standard_responses from imiptools.period import Endless @@ -52,20 +52,18 @@ if not limit: return "DECLINED", _("You have no quota allocation for the recipient.") - # Decline endless events even for unlimited quotas. - # NOTE: Such events could be supported in a similar way to those supported - # NOTE: for each user. + # Where the quota is unlimited, accept the invitation. + + if limit == "*": + return "ACCEPTED", _("The recipient has scheduled the requested period.") + + # Decline endless events for limited quotas. total = _get_duration(handler) if total == Endless(): return "DECLINED", _("The event period exceeds your quota allocation for the recipient.") - # Where the quota is unlimited, accept the invitation. - - if limit == "*": - return "ACCEPTED", _("The recipient has scheduled the requested period.") - # Decline events whose durations exceed the balance. balance = get_duration(limit) - _get_usage(entries) @@ -84,14 +82,6 @@ quota, group = _get_quota_and_group(handler, args) - total = _get_duration(handler) - expiry = _get_expiry_time(handler) - - # Ignore indefinitely recurring events. - - if total == Endless() or not expiry: - return - # Update the journal entries. journal = handler.get_journal() @@ -99,6 +89,10 @@ handler.update_freebusy(entries, handler.user, False) journal.set_entries(quota, group, entries) + # Store/update the object so that recurring events can be maintained. + + _update_object(handler, args) + def remove_from_quota(handler, args): """ @@ -108,21 +102,86 @@ quota, group = _get_quota_and_group(handler, args) - total = _get_duration(handler) - expiry = _get_expiry_time(handler) - - # Ignore indefinitely recurring events. - - if total == Endless() or not expiry: - return - # Update the journal entries. journal = handler.get_journal() entries = journal.get_entries_for_update(quota, group) - handler.remove_from_freebusy(entries) + + # Remove only the entries associated with this recipient. + + removed = handler.remove_from_freebusy(entries) + for p in removed: + if p.attendee != handler.user: + entries.insert_period(p) + journal.set_entries(quota, group, entries) + # Remove participation from the object to stop recurring event generation. + + _remove_object(handler, args) + +def _update_object(handler, args): + + "Update a stored version of the current object of the given 'handler'." + + quota, group = _get_quota_and_group(handler, args) + journal = handler.get_journal() + + # Where an existing version of the object exists, merge the recipient's + # attendance information. + + fragment = journal.get_event(quota, handler.uid, handler.recurrenceid) + obj = fragment and Object(fragment) + if not obj: + obj = handler.obj + + # Set attendance. + + attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) + attendee_map[handler.user]["PARTSTAT"] = "ACCEPTED" + obj["ATTENDEE"] = attendee_map.items() + + # Record the object so that recurrences can be generated. + + journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node()) + +def _remove_object(handler, args): + + "Remove a stored version of the current object of the given 'handler'." + + quota, group = _get_quota_and_group(handler, args) + journal = handler.get_journal() + + # Where an existing version of the object exists, remove the recipient's + # attendance information. + + fragment = journal.get_event(quota, handler.uid, handler.recurrenceid) + obj = fragment and Object(fragment) + if not obj: + return + + attendee_map = uri_dict(obj.get_value_map("ATTENDEE")) + delegates = journal.get_delegates(quota) + + # Determine whether any of the delegates are still involved. + + attendees = set(delegates).intersection(attendee_map.keys()) + if handler.user in attendees: + attendees.remove(handler.user) + + # Remove event details where no delegates will be involved. + + if not attendees: + journal.remove_event(quota, handler.uid, handler.recurrenceid) + return + + del attendee_map[handler.user] + obj["ATTENDEE"] = attendee_map.items() + + # Record the object so that recurrences can be generated. + + journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node()) + def _get_quota_and_group(handler, args): """ diff -r a00216dc5cc8 -r 7153fc86ae7d tests/common.sh --- a/tests/common.sh Sun Jun 05 19:34:36 2016 +0200 +++ b/tests/common.sh Sun Jun 05 19:36:32 2016 +0200 @@ -13,6 +13,7 @@ FREEBUSY_SCRIPT="$BASE_DIR/tools/make_freebusy.py" FREEBUSY_ARGS="-s -n" +FREEBUSY_QUOTA_ARGS="-s -q" LIST_SCRIPT="$THIS_DIR/list_table.py" LIST_ARGS="$STORE_TYPE $STORE $JOURNAL" diff -r a00216dc5cc8 -r 7153fc86ae7d tests/list_table.py --- a/tests/list_table.py Sun Jun 05 19:34:36 2016 +0200 +++ b/tests/list_table.py Sun Jun 05 19:36:32 2016 +0200 @@ -24,20 +24,23 @@ import sys def show_list(data): - for row in data: - print row or "" + if data: + for row in data: + print row or "" def show_object(data): if data: - print Object(data).to_string() + print Object(data).to_string(line_length=1000) def show_periods(data): - for row in data: - print "\t".join(row.as_tuple(strings_only=True)) + if data: + for row in data: + print "\t".join(row.as_tuple(strings_only=True)) def show_tuples(data): - for row in data: - print "\t".join([(column or "") for column in row]) + if data: + for row in data: + print "\t".join([(column or "") for column in row]) if __name__ == "__main__": try: @@ -83,6 +86,10 @@ data = store.get_freebusy_providers(user) show_tuples(data) + elif table == "journal_freebusy_providers": + data = journal.get_freebusy_providers(user) + show_tuples(data) + # Objects. elif table == "countered_object": @@ -96,6 +103,11 @@ data = store.get_event(user, uid) show_object(data) + elif table == "journal_object": + uid = args[0] + data = journal.get_event(user, uid) + show_object(data) + elif table == "recurrence": uid = args[0] recurrenceid = args[1] diff -r a00216dc5cc8 -r 7153fc86ae7d tests/templates/event-cancel-car-recurring.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-cancel-car-recurring.txt Sun Jun 05 19:36:32 2016 +0200 @@ -0,0 +1,37 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-car-fiat500@example.com +Subject: Cancellation! + +Cancel the event for one of the cars. + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="CANCEL" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:CANCEL +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-car-fiat500@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T160000 +DTEND;TZID=Europe/Oslo:20141126T170000 +RRULE:FREQ=DAILY +SUMMARY:Test drives +UID:event25@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r a00216dc5cc8 -r 7153fc86ae7d tests/test_resource_invitation_constraints_quota_recurring_limits.sh --- a/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Sun Jun 05 19:34:36 2016 +0200 +++ b/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Sun Jun 05 19:36:32 2016 +0200 @@ -15,9 +15,10 @@ # would exceed the organiser's quota applying collectively to both resources. # The result should be the first scheduling attempt being declined because it -# attempts to reserve an indefinite amount of time, with the second attempt -# succeeding but only for one resource because the quota is used up reserving -# one of the two resources requested. This reservation is then cancelled. +# attempts to reserve an indefinite amount of time that obviously exceeds the +# quota, with the second attempt succeeding but only for one resource because +# the quota is used up reserving one of the two resources requested. This +# reservation is then cancelled. # Another attempt is made by a user with a larger quota, and this succeeds in # reserving both resources. diff -r a00216dc5cc8 -r 7153fc86ae7d tests/test_resource_invitation_constraints_quota_recurring_unlimited.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_resource_invitation_constraints_quota_recurring_unlimited.sh Sun Jun 05 19:36:32 2016 +0200 @@ -0,0 +1,263 @@ +#!/bin/sh + +. "`dirname \"$0\"`/common.sh" + +USER1="mailto:resource-car-porsche911@example.com" +USER2="mailto:resource-car-fiat500@example.com" +SENDER1="mailto:paul.boddie@example.com" +SENDERADDRESS1="paul.boddie@example.com" +QUOTA=cars + +# Test quota enforcement on two resources, first checking whether the resources +# can schedule an incoming reservation, then checking whether the reservation +# would exceed the organiser's quota applying collectively to both resources. + +# The result should be the first scheduling attempt being declined because it +# attempts to reserve an indefinite amount of time that obviously exceeds the +# quota, with the second attempt succeeding once the quota has been removed. + +# The event is then cancelled for one resource and the effect evaluated. + +mkdir -p "$PREFS/$USER1" +echo 'Europe/Oslo' > "$PREFS/$USER1/TZID" +echo 'share' > "$PREFS/$USER1/freebusy_sharing" +cat > "$PREFS/$USER1/scheduling_function" < "$PREFS/$USER2/TZID" +echo 'share' > "$PREFS/$USER2/freebusy_sharing" +cat > "$PREFS/$USER2/scheduling_function" <> $ERROR \ +| "$SHOWMAIL" \ +> out0.tmp + + grep -q 'METHOD:REPLY' out0.tmp \ +&& ! grep -q '^FREEBUSY' out0.tmp \ +&& echo "Success" \ +|| echo "Failed" + +# Attempt to schedule an event. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER1" "freebusy" \ +> out0f.tmp + + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out0f.tmp" \ +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out0f.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR \ +| tee out1r.tmp \ +| "$SHOWMAIL" \ +> out1.tmp + + grep -q 'METHOD:REPLY' out1.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out1.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ +> out1f.tmp + + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \ +> out1f2.tmp + + ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f.tmp" \ +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1f.tmp" \ +&& ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f2.tmp" \ +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1f2.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is not confirmed). + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \ +> out1e.tmp + + ! grep -q "event25@example.com" "out1e.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Modify the quota and attempt to schedule the event again. + +cat <> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER1" "freebusy" \ +> out1s.tmp + + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1s.tmp" \ +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out1s.tmp" \ +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1s.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR \ +| tee out2r.tmp \ +| "$SHOWMAIL" \ +> out2.tmp + +# Since the email module used by showmail.py cannot stop after reading a single +# message, the second message is obtained. + + "$SHOWMAIL" 1 < out2r.tmp \ +>> out2.tmp + + grep -q 'METHOD:REPLY' out2.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out2.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out2.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ +> out2f.tmp + + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \ +> out2f2.tmp + + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \ +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f.tmp" \ +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out2f.tmp" \ +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f2.tmp" \ +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f2.tmp" \ +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out2f2.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is confirmed for both resources). + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \ +> out2e.tmp + + grep -q "event25@example.com" "out2e.tmp" \ +&& grep -q "$USER1" "out2e.tmp" \ +&& grep -q "$USER2" "out2e.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the journal's event store. + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_object" "event25@example.com" \ +> out2o.tmp + + grep -q "event25@example.com" "out2o.tmp" \ +&& grep -q "$USER1" "out2o.tmp" \ +&& grep -q "$USER2" "out2o.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Run the free/busy maintenance script and check that the event provides +# recurrences. + +"$FREEBUSY_SCRIPT" "$QUOTA" '*' $FREEBUSY_QUOTA_ARGS $ARGS 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_freebusy_providers" \ +| tee out2p.tmp \ +| grep -q 'event25@example.com' \ +&& echo "Success" \ +|| echo "Failed" + +# Cancel the event for one of the resources. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-car-recurring.txt" 2>> $ERROR +echo "Cancel..." + +# Check the quota (event is confirmed for one resource). + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \ +> out3e.tmp + + grep -q "event25@example.com" "out3e.tmp" \ +&& grep -q "$USER1" "out3e.tmp" \ +&& ! grep -q "$USER2" "out3e.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the journal's event store. + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_object" "event25@example.com" \ +> out3o.tmp + + grep -q "event25@example.com" "out3o.tmp" \ +&& grep -q "$USER1" "out3o.tmp" \ +&& ! grep -q "$USER2" "out3o.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Run the free/busy maintenance script and check that the event still provides +# recurrences. + +"$FREEBUSY_SCRIPT" "$QUOTA" '*' $FREEBUSY_QUOTA_ARGS $ARGS 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_freebusy_providers" \ +| tee out3p.tmp \ +| grep -q 'event25@example.com' \ +&& echo "Success" \ +|| echo "Failed" + +# Cancel the event completely. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-cars-recurring.txt" 2>> $ERROR +echo "Cancel..." + +# Check the quota (event is confirmed for no resources). + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \ +> out4e.tmp + + ! grep -q "event25@example.com" "out4e.tmp" \ +&& ! grep -q "$USER1" "out4e.tmp" \ +&& ! grep -q "$USER2" "out4e.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the journal's event store. + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_object" "event25@example.com" \ +> out4o.tmp + + ! grep -q "event25@example.com" "out4o.tmp" \ +&& ! grep -q "$USER1" "out4o.tmp" \ +&& ! grep -q "$USER2" "out4o.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Run the free/busy maintenance script and check that the event no longer +# provides recurrences. + +"$FREEBUSY_SCRIPT" "$QUOTA" '*' $FREEBUSY_QUOTA_ARGS $ARGS 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_freebusy_providers" \ +> out4p.tmp + + ! grep -q 'event25@example.com' "out4p.tmp" \ +&& echo "Success" \ +|| echo "Failed" diff -r a00216dc5cc8 -r 7153fc86ae7d tools/make_freebusy.py --- a/tools/make_freebusy.py Sun Jun 05 19:34:36 2016 +0200 +++ b/tools/make_freebusy.py Sun Jun 05 19:36:32 2016 +0200 @@ -38,17 +38,20 @@ from imiptools.client import Client from imiptools.data import get_window_end, Object from imiptools.dates import get_default_timezone, to_utc_datetime -from imiptools.period import FreeBusyCollection +from imiptools.period import FreeBusyCollection, FreeBusyGroupCollection, \ + FreeBusyGroupPeriod from imiptools.stores import get_store, get_publisher, get_journal -def make_freebusy(client, participant, store_and_publish, include_needs_action, - reset_updated_list, verbose): +def make_freebusy(client, participants, storage, store_and_publish, + include_needs_action, reset_updated_list, verbose): """ Using the given 'client' representing a user, make free/busy details for the - records of the user, generating details for 'participant' if not indicated + records of the user, generating details for 'participants' if not indicated as None; otherwise, generating free/busy details concerning the given user. + The 'storage' is the specific store or journal object used to access data. + If 'store_and_publish' is set, the stored details will be updated; otherwise, the details will be written to standard output. @@ -64,11 +67,10 @@ """ user = client.user - store = client.get_store() + journal = client.get_journal() publisher = client.get_publisher() preferences = client.get_preferences() - participant = participant or user tzid = preferences.get("TZID") or get_default_timezone() # Get the size of the free/busy window. @@ -79,67 +81,112 @@ window_size = 100 window_end = get_window_end(tzid, window_size) - # Get identifiers for uncancelled events either from a list of events - # providing free/busy periods at the end of the given time window, or from - # a list of all events. + providers = [] + + # Iterate over participants, with None being a special null participant + # value. + + for participant in participants or [None]: + + # Get identifiers for uncancelled events either from a list of events + # providing free/busy periods at the end of the given time window, or from + # a list of all events. - all_events = not reset_updated_list and store.get_freebusy_providers(user, window_end) + all_events = not reset_updated_list and storage.get_freebusy_providers(user, window_end) - if not all_events: - all_events = store.get_all_events(user) - fb = FreeBusyCollection() + if not all_events: + all_events = storage.get_all_events(user) + if storage is journal: + fb = FreeBusyGroupCollection() + else: + fb = FreeBusyCollection() + + # With providers of additional periods, append to the existing collection. - # With providers of additional periods, append to the existing collection. + else: + if participants is None: + fb = storage.get_freebusy_for_update(user) + else: + fb = storage.get_freebusy_for_other_for_update(user, participant) + + # Obtain event objects. - else: - if user == participant: - fb = store.get_freebusy(user) - else: - fb = store.get_freebusy_for_other(user, participant) + objs = [] + for uid, recurrenceid in all_events: + if verbose: + print >>sys.stderr, uid, recurrenceid + event = storage.get_event(user, uid, recurrenceid) + if event: + objs.append(Object(event)) - # Obtain event objects. + # Build a free/busy collection for the given user. + + for obj in objs: + recurrenceids = not obj.get_recurrenceid() and storage.get_recurrences(user, obj.get_uid()) + + # Obtain genuine attendees. - objs = [] - for uid, recurrenceid in all_events: - if verbose: - print >>sys.stderr, uid, recurrenceid - event = store.get_event(user, uid, recurrenceid) - if event: - objs.append(Object(event)) + if storage is journal: + attendees = storage.get_delegates(user) + else: + attendees = [participant] + + # Generate records for each attendee (applicable to consolidated + # journal data). - # Build a free/busy collection for the given user. + for attendee in attendees: + partstat = obj.get_participation_status(attendee) + + # Only include objects where the attendee actually participates. - for obj in objs: - partstat = obj.get_participation_status(participant) - recurrenceids = not obj.get_recurrenceid() and store.get_recurrences(user, obj.get_uid()) + if obj.get_participation(partstat, include_needs_action): + + # Add each active period to the collection. + + for p in obj.get_active_periods(recurrenceids, tzid, window_end): + + # Obtain a suitable period object. - if obj.get_participation(partstat, include_needs_action): - for p in obj.get_active_periods(recurrenceids, tzid, window_end): - fbp = obj.get_freebusy_period(p, partstat == "ORG") - fb.insert_period(fbp) + fbp = obj.get_freebusy_period(p, partstat == "ORG") + + if storage is journal: + fbp = FreeBusyGroupPeriod(*fbp.as_tuple(), attendee=attendee) - # Store and publish the free/busy collection. + fb.insert_period(fbp) + + # Store and publish the free/busy collection. + + if store_and_publish: - if store_and_publish: - if user == participant: - store.set_freebusy(user, fb) + # Set the user's own free/busy information. + + if participant is None: + storage.set_freebusy(user, fb) - if client.is_sharing() and client.is_publishing(): - publisher.set_freebusy(user, fb) + if client.is_sharing() and client.is_publishing(): + publisher.set_freebusy(user, fb) + + # Set free/busy information concerning another user. + + else: + storage.set_freebusy_for_other(user, fb, participant) # Update the list of objects providing periods on future occasions. - store.set_freebusy_providers(user, to_utc_datetime(window_end, tzid), - [obj for obj in objs if obj.possibly_active_from(window_end, tzid)]) - else: - store.set_freebusy_for_other(user, fb, participant) + if participant is None or storage is journal: + providers += [obj for obj in objs if obj.possibly_active_from(window_end, tzid)] + + # Alternatively, just write the collection to standard output. - # Alternatively, just write the collection to standard output. + else: + f = getwriter("utf-8")(sys.stdout) + for item in fb: + print >>f, "\t".join(item.as_tuple(strings_only=True)) - else: - f = getwriter("utf-8")(sys.stdout) - for item in fb: - print >>f, "\t".join(item.as_tuple(strings_only=True)) + # Update free/busy providers if storing. + + if store_and_publish: + storage.set_freebusy_providers(user, to_utc_datetime(window_end, tzid), providers) # Main program. @@ -162,7 +209,7 @@ l = participants for arg in sys.argv[1:]: - if arg in ("-n", "-s", "-v", "-r"): + if arg in ("-n", "-s", "-v", "-r", "-q"): args.append(arg) l = ignored elif arg == "-T": @@ -182,13 +229,14 @@ user = participants[0] except IndexError: print >>sys.stderr, """\ -Usage: %s [ ] [ ] +Usage: %s [ ... ] [ ] -Need a user and an optional participant (if different from the user), +Need a user and optional participants (if different from the user), along with the -s option if updating the store and the published details. Specific options: +-q Access quotas in the journal instead of users in the store -s Update the store and published details (write details to standard output otherwise) -n Include objects with PARTSTAT of NEEDS-ACTION @@ -207,7 +255,8 @@ # Define any other participant of interest plus options. - participant = participants[1:] and participants[1] or None + participants = participants[1:] + using_journal = "-q" in args store_and_publish = "-s" in args include_needs_action = "-n" in args reset_updated_list = "-r" in args @@ -229,20 +278,44 @@ publisher = get_publisher(publishing_dir) journal = get_journal(store_type, journal_dir) + # Determine which kind of object will be accessed. + + if using_journal: + storage = journal + else: + storage = store + # Obtain a list of users for processing. if user in ("*", "all"): - users = store.get_users() + users = storage.get_users() else: users = [user] + # Obtain a list of participants for processing. + + if participants and participants[0] in ("*", "all"): + participants = storage.get_freebusy_others(user) + + # Provide a participants list to iterate over even if no specific + # participant is involved. This updates a user's own records, but only for + # the general data store. + + elif not participants: + if not using_journal: + participants = None + else: + print >>sys.stderr, "Participants must be indicated when updating quota records." + sys.exit(1) + # Process the given users. for user in users: if verbose: print >>sys.stderr, user make_freebusy( - Client(user, None, store, publisher, journal, preferences_dir), participant, - store_and_publish, include_needs_action, reset_updated_list, verbose) + Client(user, None, store, publisher, journal, preferences_dir), + participants, storage, store_and_publish, include_needs_action, + reset_updated_list, verbose) # vim: tabstop=4 expandtab shiftwidth=4