1.1 --- a/conf/cron/cron.daily/imip-agent Sun Jun 05 19:34:36 2016 +0200
1.2 +++ b/conf/cron/cron.daily/imip-agent Sun Jun 05 19:36:32 2016 +0200
1.3 @@ -4,4 +4,5 @@
1.4 export PYTHONPATH="$INSTALL_DIR"
1.5
1.6 "$INSTALL_DIR/tools/make_freebusy.py" all -n -s
1.7 +"$INSTALL_DIR/tools/make_freebusy.py" all all -q -s
1.8 "$INSTALL_DIR/tools/update_quotas.py" all -s
2.1 --- a/docs/wiki/CronIntegration Sun Jun 05 19:34:36 2016 +0200
2.2 +++ b/docs/wiki/CronIntegration Sun Jun 05 19:36:32 2016 +0200
2.3 @@ -4,6 +4,7 @@
2.4 the following:
2.5
2.6 * Free/busy collections for all known users
2.7 + * Free/busy collections for all known quotas
2.8 * Quota records for all known quota groups
2.9
2.10 This file should be copied to the appropriate destination. For example:
2.11 @@ -30,7 +31,7 @@
2.12 Responsibility for generating free/busy expansions lies with the
2.13 `tools/make_freebusy.py` program, which is a general tool that can also
2.14 reset the free/busy records defined for a user or those made available to a
2.15 -user.
2.16 +user, as well as the free/busy records defined for a resource quota.
2.17
2.18 == Quota Journals ==
2.19
3.1 --- a/docs/wiki/DatabaseStore Sun Jun 05 19:34:36 2016 +0200
3.2 +++ b/docs/wiki/DatabaseStore Sun Jun 05 19:36:32 2016 +0200
3.3 @@ -56,6 +56,19 @@
3.4 .. `freebusy` table in the data store; this may be the same table as the one employed
3.5 .. by the data store to store received or deduced free/busy details
3.6 ==
3.7 +`freebusy_providers`
3.8 +|| Details of [[../EventRecurrences|recurring events]] for which new free/busy records
3.9 +.. must be [[../CronIntegration|periodically generated]] because these events recur
3.10 +.. indefinitely, selectable for each user (`store_user`)
3.11 +==
3.12 +`freebusy_provider_datetimes`
3.13 +|| Date/time details associated with the `freebusy_providers` information
3.14 +==
3.15 +`quota_delegates`
3.16 +|| Details of the identities (`store_user`) employing each quota (`quota`) to
3.17 +.. consolidate their schedules, between which delegation may take place if their
3.18 +.. schedules permit it
3.19 +==
3.20 `quota_limits`
3.21 || A mapping from user identities or group identifiers to quota limits
3.22 ==
4.1 --- a/docs/wiki/FilesystemUsage Sun Jun 05 19:34:36 2016 +0200
4.2 +++ b/docs/wiki/FilesystemUsage Sun Jun 05 19:36:32 2016 +0200
4.3 @@ -34,15 +34,21 @@
4.4 == Journal Structure ==
4.5
4.6 Within the journal directory are a collection of subdirectories, each of which
4.7 -represent a distinct quota group for one or more resources. When a user attempts
4.8 -to reserve a resource in such a group, their ability to schedule that resource
4.9 -will depend on how much they are using the other resources in that group.
4.10 +represent a distinct quota group for one or more [[../Resources|resources]].
4.11 +When a user attempts to reserve a resource in such a group, their ability to
4.12 +schedule that resource will depend on how much they are using the other
4.13 +resources in that group.
4.14
4.15 The directory for each quota group contains the following entries:
4.16
4.17 {{{{#!table
4.18 '''Entry''' || '''Purpose'''
4.19 ==
4.20 +`delegates`
4.21 +|| A file containing details of the identities employing this quota to consolidate
4.22 +.. their schedules, between which delegation may take place if their schedules
4.23 +.. permit it
4.24 +==
4.25 `freebusy-other`
4.26 || A directory containing files, one per user (each containing period descriptions
4.27 .. for reservations made by that user, in chronological order, structured
4.28 @@ -55,6 +61,11 @@
4.29 freebusy-other/USER
4.30 }}}
4.31 ==
4.32 +`freebusy-providers`
4.33 +|| A file containing details of [[../EventRecurrences|recurring events]] for which
4.34 +.. new free/busy records must be [[../CronIntegration|periodically generated]]
4.35 +.. because these events recur indefinitely
4.36 +==
4.37 `groups`
4.38 || A mapping from user identities to group identifiers indicating the sharing
4.39 .. of a quota across a number of users
5.1 --- a/imiptools/handlers/scheduling/quota.py Sun Jun 05 19:34:36 2016 +0200
5.2 +++ b/imiptools/handlers/scheduling/quota.py Sun Jun 05 19:36:32 2016 +0200
5.3 @@ -20,7 +20,7 @@
5.4 """
5.5
5.6 from imiptools.dates import get_duration, to_utc_datetime
5.7 -from imiptools.data import get_uri, uri_dict
5.8 +from imiptools.data import get_uri, uri_dict, Object
5.9 from imiptools.handlers.scheduling.common import get_scheduling_conflicts, \
5.10 standard_responses
5.11 from imiptools.period import Endless
5.12 @@ -52,20 +52,18 @@
5.13 if not limit:
5.14 return "DECLINED", _("You have no quota allocation for the recipient.")
5.15
5.16 - # Decline endless events even for unlimited quotas.
5.17 - # NOTE: Such events could be supported in a similar way to those supported
5.18 - # NOTE: for each user.
5.19 + # Where the quota is unlimited, accept the invitation.
5.20 +
5.21 + if limit == "*":
5.22 + return "ACCEPTED", _("The recipient has scheduled the requested period.")
5.23 +
5.24 + # Decline endless events for limited quotas.
5.25
5.26 total = _get_duration(handler)
5.27
5.28 if total == Endless():
5.29 return "DECLINED", _("The event period exceeds your quota allocation for the recipient.")
5.30
5.31 - # Where the quota is unlimited, accept the invitation.
5.32 -
5.33 - if limit == "*":
5.34 - return "ACCEPTED", _("The recipient has scheduled the requested period.")
5.35 -
5.36 # Decline events whose durations exceed the balance.
5.37
5.38 balance = get_duration(limit) - _get_usage(entries)
5.39 @@ -84,14 +82,6 @@
5.40
5.41 quota, group = _get_quota_and_group(handler, args)
5.42
5.43 - total = _get_duration(handler)
5.44 - expiry = _get_expiry_time(handler)
5.45 -
5.46 - # Ignore indefinitely recurring events.
5.47 -
5.48 - if total == Endless() or not expiry:
5.49 - return
5.50 -
5.51 # Update the journal entries.
5.52
5.53 journal = handler.get_journal()
5.54 @@ -99,6 +89,10 @@
5.55 handler.update_freebusy(entries, handler.user, False)
5.56 journal.set_entries(quota, group, entries)
5.57
5.58 + # Store/update the object so that recurring events can be maintained.
5.59 +
5.60 + _update_object(handler, args)
5.61 +
5.62 def remove_from_quota(handler, args):
5.63
5.64 """
5.65 @@ -108,21 +102,86 @@
5.66
5.67 quota, group = _get_quota_and_group(handler, args)
5.68
5.69 - total = _get_duration(handler)
5.70 - expiry = _get_expiry_time(handler)
5.71 -
5.72 - # Ignore indefinitely recurring events.
5.73 -
5.74 - if total == Endless() or not expiry:
5.75 - return
5.76 -
5.77 # Update the journal entries.
5.78
5.79 journal = handler.get_journal()
5.80 entries = journal.get_entries_for_update(quota, group)
5.81 - handler.remove_from_freebusy(entries)
5.82 +
5.83 + # Remove only the entries associated with this recipient.
5.84 +
5.85 + removed = handler.remove_from_freebusy(entries)
5.86 + for p in removed:
5.87 + if p.attendee != handler.user:
5.88 + entries.insert_period(p)
5.89 +
5.90 journal.set_entries(quota, group, entries)
5.91
5.92 + # Remove participation from the object to stop recurring event generation.
5.93 +
5.94 + _remove_object(handler, args)
5.95 +
5.96 +def _update_object(handler, args):
5.97 +
5.98 + "Update a stored version of the current object of the given 'handler'."
5.99 +
5.100 + quota, group = _get_quota_and_group(handler, args)
5.101 + journal = handler.get_journal()
5.102 +
5.103 + # Where an existing version of the object exists, merge the recipient's
5.104 + # attendance information.
5.105 +
5.106 + fragment = journal.get_event(quota, handler.uid, handler.recurrenceid)
5.107 + obj = fragment and Object(fragment)
5.108 + if not obj:
5.109 + obj = handler.obj
5.110 +
5.111 + # Set attendance.
5.112 +
5.113 + attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
5.114 + attendee_map[handler.user]["PARTSTAT"] = "ACCEPTED"
5.115 + obj["ATTENDEE"] = attendee_map.items()
5.116 +
5.117 + # Record the object so that recurrences can be generated.
5.118 +
5.119 + journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node())
5.120 +
5.121 +def _remove_object(handler, args):
5.122 +
5.123 + "Remove a stored version of the current object of the given 'handler'."
5.124 +
5.125 + quota, group = _get_quota_and_group(handler, args)
5.126 + journal = handler.get_journal()
5.127 +
5.128 + # Where an existing version of the object exists, remove the recipient's
5.129 + # attendance information.
5.130 +
5.131 + fragment = journal.get_event(quota, handler.uid, handler.recurrenceid)
5.132 + obj = fragment and Object(fragment)
5.133 + if not obj:
5.134 + return
5.135 +
5.136 + attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
5.137 + delegates = journal.get_delegates(quota)
5.138 +
5.139 + # Determine whether any of the delegates are still involved.
5.140 +
5.141 + attendees = set(delegates).intersection(attendee_map.keys())
5.142 + if handler.user in attendees:
5.143 + attendees.remove(handler.user)
5.144 +
5.145 + # Remove event details where no delegates will be involved.
5.146 +
5.147 + if not attendees:
5.148 + journal.remove_event(quota, handler.uid, handler.recurrenceid)
5.149 + return
5.150 +
5.151 + del attendee_map[handler.user]
5.152 + obj["ATTENDEE"] = attendee_map.items()
5.153 +
5.154 + # Record the object so that recurrences can be generated.
5.155 +
5.156 + journal.set_event(quota, handler.uid, handler.recurrenceid, obj.to_node())
5.157 +
5.158 def _get_quota_and_group(handler, args):
5.159
5.160 """
6.1 --- a/tests/common.sh Sun Jun 05 19:34:36 2016 +0200
6.2 +++ b/tests/common.sh Sun Jun 05 19:36:32 2016 +0200
6.3 @@ -13,6 +13,7 @@
6.4
6.5 FREEBUSY_SCRIPT="$BASE_DIR/tools/make_freebusy.py"
6.6 FREEBUSY_ARGS="-s -n"
6.7 +FREEBUSY_QUOTA_ARGS="-s -q"
6.8
6.9 LIST_SCRIPT="$THIS_DIR/list_table.py"
6.10 LIST_ARGS="$STORE_TYPE $STORE $JOURNAL"
7.1 --- a/tests/list_table.py Sun Jun 05 19:34:36 2016 +0200
7.2 +++ b/tests/list_table.py Sun Jun 05 19:36:32 2016 +0200
7.3 @@ -24,20 +24,23 @@
7.4 import sys
7.5
7.6 def show_list(data):
7.7 - for row in data:
7.8 - print row or ""
7.9 + if data:
7.10 + for row in data:
7.11 + print row or ""
7.12
7.13 def show_object(data):
7.14 if data:
7.15 - print Object(data).to_string()
7.16 + print Object(data).to_string(line_length=1000)
7.17
7.18 def show_periods(data):
7.19 - for row in data:
7.20 - print "\t".join(row.as_tuple(strings_only=True))
7.21 + if data:
7.22 + for row in data:
7.23 + print "\t".join(row.as_tuple(strings_only=True))
7.24
7.25 def show_tuples(data):
7.26 - for row in data:
7.27 - print "\t".join([(column or "") for column in row])
7.28 + if data:
7.29 + for row in data:
7.30 + print "\t".join([(column or "") for column in row])
7.31
7.32 if __name__ == "__main__":
7.33 try:
7.34 @@ -83,6 +86,10 @@
7.35 data = store.get_freebusy_providers(user)
7.36 show_tuples(data)
7.37
7.38 + elif table == "journal_freebusy_providers":
7.39 + data = journal.get_freebusy_providers(user)
7.40 + show_tuples(data)
7.41 +
7.42 # Objects.
7.43
7.44 elif table == "countered_object":
7.45 @@ -96,6 +103,11 @@
7.46 data = store.get_event(user, uid)
7.47 show_object(data)
7.48
7.49 + elif table == "journal_object":
7.50 + uid = args[0]
7.51 + data = journal.get_event(user, uid)
7.52 + show_object(data)
7.53 +
7.54 elif table == "recurrence":
7.55 uid = args[0]
7.56 recurrenceid = args[1]
8.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
8.2 +++ b/tests/templates/event-cancel-car-recurring.txt Sun Jun 05 19:36:32 2016 +0200
8.3 @@ -0,0 +1,37 @@
8.4 +Content-Type: multipart/alternative; boundary="===============0047278175=="
8.5 +MIME-Version: 1.0
8.6 +From: paul.boddie@example.com
8.7 +To: resource-car-fiat500@example.com
8.8 +Subject: Cancellation!
8.9 +
8.10 +Cancel the event for one of the cars.
8.11 +
8.12 +--===============0047278175==
8.13 +Content-Type: text/plain; charset="us-ascii"
8.14 +MIME-Version: 1.0
8.15 +Content-Transfer-Encoding: 7bit
8.16 +
8.17 +This message contains an event.
8.18 +--===============0047278175==
8.19 +MIME-Version: 1.0
8.20 +Content-Transfer-Encoding: 7bit
8.21 +Content-Type: text/calendar; charset="us-ascii"; method="CANCEL"
8.22 +
8.23 +BEGIN:VCALENDAR
8.24 +PRODID:-//imip-agent/test//EN
8.25 +METHOD:CANCEL
8.26 +VERSION:2.0
8.27 +BEGIN:VEVENT
8.28 +ORGANIZER:mailto:paul.boddie@example.com
8.29 +ATTENDEE:mailto:paul.boddie@example.com
8.30 +ATTENDEE;RSVP=TRUE:mailto:resource-car-fiat500@example.com
8.31 +DTSTAMP:20141125T004600Z
8.32 +DTSTART;TZID=Europe/Oslo:20141126T160000
8.33 +DTEND;TZID=Europe/Oslo:20141126T170000
8.34 +RRULE:FREQ=DAILY
8.35 +SUMMARY:Test drives
8.36 +UID:event25@example.com
8.37 +END:VEVENT
8.38 +END:VCALENDAR
8.39 +
8.40 +--===============0047278175==--
9.1 --- a/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Sun Jun 05 19:34:36 2016 +0200
9.2 +++ b/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Sun Jun 05 19:36:32 2016 +0200
9.3 @@ -15,9 +15,10 @@
9.4 # would exceed the organiser's quota applying collectively to both resources.
9.5
9.6 # The result should be the first scheduling attempt being declined because it
9.7 -# attempts to reserve an indefinite amount of time, with the second attempt
9.8 -# succeeding but only for one resource because the quota is used up reserving
9.9 -# one of the two resources requested. This reservation is then cancelled.
9.10 +# attempts to reserve an indefinite amount of time that obviously exceeds the
9.11 +# quota, with the second attempt succeeding but only for one resource because
9.12 +# the quota is used up reserving one of the two resources requested. This
9.13 +# reservation is then cancelled.
9.14
9.15 # Another attempt is made by a user with a larger quota, and this succeeds in
9.16 # reserving both resources.
10.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
10.2 +++ b/tests/test_resource_invitation_constraints_quota_recurring_unlimited.sh Sun Jun 05 19:36:32 2016 +0200
10.3 @@ -0,0 +1,263 @@
10.4 +#!/bin/sh
10.5 +
10.6 +. "`dirname \"$0\"`/common.sh"
10.7 +
10.8 +USER1="mailto:resource-car-porsche911@example.com"
10.9 +USER2="mailto:resource-car-fiat500@example.com"
10.10 +SENDER1="mailto:paul.boddie@example.com"
10.11 +SENDERADDRESS1="paul.boddie@example.com"
10.12 +QUOTA=cars
10.13 +
10.14 +# Test quota enforcement on two resources, first checking whether the resources
10.15 +# can schedule an incoming reservation, then checking whether the reservation
10.16 +# would exceed the organiser's quota applying collectively to both resources.
10.17 +
10.18 +# The result should be the first scheduling attempt being declined because it
10.19 +# attempts to reserve an indefinite amount of time that obviously exceeds the
10.20 +# quota, with the second attempt succeeding once the quota has been removed.
10.21 +
10.22 +# The event is then cancelled for one resource and the effect evaluated.
10.23 +
10.24 +mkdir -p "$PREFS/$USER1"
10.25 +echo 'Europe/Oslo' > "$PREFS/$USER1/TZID"
10.26 +echo 'share' > "$PREFS/$USER1/freebusy_sharing"
10.27 +cat > "$PREFS/$USER1/scheduling_function" <<EOF
10.28 +schedule_in_freebusy
10.29 +check_quota $QUOTA
10.30 +EOF
10.31 +
10.32 +mkdir -p "$PREFS/$USER2"
10.33 +echo 'Europe/Oslo' > "$PREFS/$USER2/TZID"
10.34 +echo 'share' > "$PREFS/$USER2/freebusy_sharing"
10.35 +cat > "$PREFS/$USER2/scheduling_function" <<EOF
10.36 +schedule_in_freebusy
10.37 +check_quota $QUOTA
10.38 +EOF
10.39 +
10.40 +cat <<EOF | "$SET_QUOTA_LIMITS" "$QUOTA" $SET_QUOTA_LIMITS_ARGS
10.41 +* PT10H
10.42 +EOF
10.43 +
10.44 +# Allow cars to delegate to each other. This defines the possible attendees for
10.45 +# recurring events.
10.46 +
10.47 +cat <<EOF | "$SET_DELEGATES" "$QUOTA" $SET_DELEGATES_ARGS
10.48 +mailto:resource-car-porsche911@example.com
10.49 +mailto:resource-car-fiat500@example.com
10.50 +EOF
10.51 +
10.52 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car-all.txt" 2>> $ERROR \
10.53 +| "$SHOWMAIL" \
10.54 +> out0.tmp
10.55 +
10.56 + grep -q 'METHOD:REPLY' out0.tmp \
10.57 +&& ! grep -q '^FREEBUSY' out0.tmp \
10.58 +&& echo "Success" \
10.59 +|| echo "Failed"
10.60 +
10.61 +# Attempt to schedule an event.
10.62 +
10.63 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR
10.64 +
10.65 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER1" "freebusy" \
10.66 +> out0f.tmp
10.67 +
10.68 + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out0f.tmp" \
10.69 +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out0f.tmp" \
10.70 +&& echo "Success" \
10.71 +|| echo "Failed"
10.72 +
10.73 +# Present the request to the resource.
10.74 +
10.75 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR \
10.76 +| tee out1r.tmp \
10.77 +| "$SHOWMAIL" \
10.78 +> out1.tmp
10.79 +
10.80 + grep -q 'METHOD:REPLY' out1.tmp \
10.81 +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out1.tmp \
10.82 +&& echo "Success" \
10.83 +|| echo "Failed"
10.84 +
10.85 + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
10.86 +> out1f.tmp
10.87 +
10.88 + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
10.89 +> out1f2.tmp
10.90 +
10.91 + ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f.tmp" \
10.92 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1f.tmp" \
10.93 +&& ! grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1f2.tmp" \
10.94 +&& ! grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1f2.tmp" \
10.95 +&& echo "Success" \
10.96 +|| echo "Failed"
10.97 +
10.98 +# Check the quota (event is not confirmed).
10.99 +
10.100 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \
10.101 +> out1e.tmp
10.102 +
10.103 + ! grep -q "event25@example.com" "out1e.tmp" \
10.104 +&& echo "Success" \
10.105 +|| echo "Failed"
10.106 +
10.107 +# Modify the quota and attempt to schedule the event again.
10.108 +
10.109 +cat <<EOF | "$SET_QUOTA_LIMITS" "$QUOTA" $SET_QUOTA_LIMITS_ARGS
10.110 +* *
10.111 +EOF
10.112 +
10.113 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR
10.114 +
10.115 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER1" "freebusy" \
10.116 +> out1s.tmp
10.117 +
10.118 + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out1s.tmp" \
10.119 +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out1s.tmp" \
10.120 +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out1s.tmp" \
10.121 +&& echo "Success" \
10.122 +|| echo "Failed"
10.123 +
10.124 +# Present the request to the resource.
10.125 +
10.126 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-cars-recurring.txt" 2>> $ERROR \
10.127 +| tee out2r.tmp \
10.128 +| "$SHOWMAIL" \
10.129 +> out2.tmp
10.130 +
10.131 +# Since the email module used by showmail.py cannot stop after reading a single
10.132 +# message, the second message is obtained.
10.133 +
10.134 + "$SHOWMAIL" 1 < out2r.tmp \
10.135 +>> out2.tmp
10.136 +
10.137 + grep -q 'METHOD:REPLY' out2.tmp \
10.138 +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out2.tmp \
10.139 +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out2.tmp \
10.140 +&& echo "Success" \
10.141 +|| echo "Failed"
10.142 +
10.143 + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \
10.144 +> out2f.tmp
10.145 +
10.146 + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \
10.147 +> out2f2.tmp
10.148 +
10.149 + grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f.tmp" \
10.150 +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f.tmp" \
10.151 +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out2f.tmp" \
10.152 +&& grep -q "^20141126T150000Z${TAB}20141126T160000Z" "out2f2.tmp" \
10.153 +&& grep -q "^20141130T150000Z${TAB}20141130T160000Z" "out2f2.tmp" \
10.154 +&& grep -q "^20141206T150000Z${TAB}20141206T160000Z" "out2f2.tmp" \
10.155 +&& echo "Success" \
10.156 +|| echo "Failed"
10.157 +
10.158 +# Check the quota (event is confirmed for both resources).
10.159 +
10.160 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \
10.161 +> out2e.tmp
10.162 +
10.163 + grep -q "event25@example.com" "out2e.tmp" \
10.164 +&& grep -q "$USER1" "out2e.tmp" \
10.165 +&& grep -q "$USER2" "out2e.tmp" \
10.166 +&& echo "Success" \
10.167 +|| echo "Failed"
10.168 +
10.169 +# Check the journal's event store.
10.170 +
10.171 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_object" "event25@example.com" \
10.172 +> out2o.tmp
10.173 +
10.174 + grep -q "event25@example.com" "out2o.tmp" \
10.175 +&& grep -q "$USER1" "out2o.tmp" \
10.176 +&& grep -q "$USER2" "out2o.tmp" \
10.177 +&& echo "Success" \
10.178 +|| echo "Failed"
10.179 +
10.180 +# Run the free/busy maintenance script and check that the event provides
10.181 +# recurrences.
10.182 +
10.183 +"$FREEBUSY_SCRIPT" "$QUOTA" '*' $FREEBUSY_QUOTA_ARGS $ARGS 2>> $ERROR
10.184 +
10.185 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_freebusy_providers" \
10.186 +| tee out2p.tmp \
10.187 +| grep -q 'event25@example.com' \
10.188 +&& echo "Success" \
10.189 +|| echo "Failed"
10.190 +
10.191 +# Cancel the event for one of the resources.
10.192 +
10.193 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-car-recurring.txt" 2>> $ERROR
10.194 +echo "Cancel..."
10.195 +
10.196 +# Check the quota (event is confirmed for one resource).
10.197 +
10.198 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \
10.199 +> out3e.tmp
10.200 +
10.201 + grep -q "event25@example.com" "out3e.tmp" \
10.202 +&& grep -q "$USER1" "out3e.tmp" \
10.203 +&& ! grep -q "$USER2" "out3e.tmp" \
10.204 +&& echo "Success" \
10.205 +|| echo "Failed"
10.206 +
10.207 +# Check the journal's event store.
10.208 +
10.209 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_object" "event25@example.com" \
10.210 +> out3o.tmp
10.211 +
10.212 + grep -q "event25@example.com" "out3o.tmp" \
10.213 +&& grep -q "$USER1" "out3o.tmp" \
10.214 +&& ! grep -q "$USER2" "out3o.tmp" \
10.215 +&& echo "Success" \
10.216 +|| echo "Failed"
10.217 +
10.218 +# Run the free/busy maintenance script and check that the event still provides
10.219 +# recurrences.
10.220 +
10.221 +"$FREEBUSY_SCRIPT" "$QUOTA" '*' $FREEBUSY_QUOTA_ARGS $ARGS 2>> $ERROR
10.222 +
10.223 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_freebusy_providers" \
10.224 +| tee out3p.tmp \
10.225 +| grep -q 'event25@example.com' \
10.226 +&& echo "Success" \
10.227 +|| echo "Failed"
10.228 +
10.229 +# Cancel the event completely.
10.230 +
10.231 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-cars-recurring.txt" 2>> $ERROR
10.232 +echo "Cancel..."
10.233 +
10.234 +# Check the quota (event is confirmed for no resources).
10.235 +
10.236 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER1" \
10.237 +> out4e.tmp
10.238 +
10.239 + ! grep -q "event25@example.com" "out4e.tmp" \
10.240 +&& ! grep -q "$USER1" "out4e.tmp" \
10.241 +&& ! grep -q "$USER2" "out4e.tmp" \
10.242 +&& echo "Success" \
10.243 +|| echo "Failed"
10.244 +
10.245 +# Check the journal's event store.
10.246 +
10.247 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_object" "event25@example.com" \
10.248 +> out4o.tmp
10.249 +
10.250 + ! grep -q "event25@example.com" "out4o.tmp" \
10.251 +&& ! grep -q "$USER1" "out4o.tmp" \
10.252 +&& ! grep -q "$USER2" "out4o.tmp" \
10.253 +&& echo "Success" \
10.254 +|| echo "Failed"
10.255 +
10.256 +# Run the free/busy maintenance script and check that the event no longer
10.257 +# provides recurrences.
10.258 +
10.259 +"$FREEBUSY_SCRIPT" "$QUOTA" '*' $FREEBUSY_QUOTA_ARGS $ARGS 2>> $ERROR
10.260 +
10.261 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "journal_freebusy_providers" \
10.262 +> out4p.tmp
10.263 +
10.264 + ! grep -q 'event25@example.com' "out4p.tmp" \
10.265 +&& echo "Success" \
10.266 +|| echo "Failed"
11.1 --- a/tools/make_freebusy.py Sun Jun 05 19:34:36 2016 +0200
11.2 +++ b/tools/make_freebusy.py Sun Jun 05 19:36:32 2016 +0200
11.3 @@ -38,17 +38,20 @@
11.4 from imiptools.client import Client
11.5 from imiptools.data import get_window_end, Object
11.6 from imiptools.dates import get_default_timezone, to_utc_datetime
11.7 -from imiptools.period import FreeBusyCollection
11.8 +from imiptools.period import FreeBusyCollection, FreeBusyGroupCollection, \
11.9 + FreeBusyGroupPeriod
11.10 from imiptools.stores import get_store, get_publisher, get_journal
11.11
11.12 -def make_freebusy(client, participant, store_and_publish, include_needs_action,
11.13 - reset_updated_list, verbose):
11.14 +def make_freebusy(client, participants, storage, store_and_publish,
11.15 + include_needs_action, reset_updated_list, verbose):
11.16
11.17 """
11.18 Using the given 'client' representing a user, make free/busy details for the
11.19 - records of the user, generating details for 'participant' if not indicated
11.20 + records of the user, generating details for 'participants' if not indicated
11.21 as None; otherwise, generating free/busy details concerning the given user.
11.22
11.23 + The 'storage' is the specific store or journal object used to access data.
11.24 +
11.25 If 'store_and_publish' is set, the stored details will be updated;
11.26 otherwise, the details will be written to standard output.
11.27
11.28 @@ -64,11 +67,10 @@
11.29 """
11.30
11.31 user = client.user
11.32 - store = client.get_store()
11.33 + journal = client.get_journal()
11.34 publisher = client.get_publisher()
11.35 preferences = client.get_preferences()
11.36
11.37 - participant = participant or user
11.38 tzid = preferences.get("TZID") or get_default_timezone()
11.39
11.40 # Get the size of the free/busy window.
11.41 @@ -79,67 +81,112 @@
11.42 window_size = 100
11.43 window_end = get_window_end(tzid, window_size)
11.44
11.45 - # Get identifiers for uncancelled events either from a list of events
11.46 - # providing free/busy periods at the end of the given time window, or from
11.47 - # a list of all events.
11.48 + providers = []
11.49 +
11.50 + # Iterate over participants, with None being a special null participant
11.51 + # value.
11.52 +
11.53 + for participant in participants or [None]:
11.54 +
11.55 + # Get identifiers for uncancelled events either from a list of events
11.56 + # providing free/busy periods at the end of the given time window, or from
11.57 + # a list of all events.
11.58
11.59 - all_events = not reset_updated_list and store.get_freebusy_providers(user, window_end)
11.60 + all_events = not reset_updated_list and storage.get_freebusy_providers(user, window_end)
11.61
11.62 - if not all_events:
11.63 - all_events = store.get_all_events(user)
11.64 - fb = FreeBusyCollection()
11.65 + if not all_events:
11.66 + all_events = storage.get_all_events(user)
11.67 + if storage is journal:
11.68 + fb = FreeBusyGroupCollection()
11.69 + else:
11.70 + fb = FreeBusyCollection()
11.71 +
11.72 + # With providers of additional periods, append to the existing collection.
11.73
11.74 - # With providers of additional periods, append to the existing collection.
11.75 + else:
11.76 + if participants is None:
11.77 + fb = storage.get_freebusy_for_update(user)
11.78 + else:
11.79 + fb = storage.get_freebusy_for_other_for_update(user, participant)
11.80 +
11.81 + # Obtain event objects.
11.82
11.83 - else:
11.84 - if user == participant:
11.85 - fb = store.get_freebusy(user)
11.86 - else:
11.87 - fb = store.get_freebusy_for_other(user, participant)
11.88 + objs = []
11.89 + for uid, recurrenceid in all_events:
11.90 + if verbose:
11.91 + print >>sys.stderr, uid, recurrenceid
11.92 + event = storage.get_event(user, uid, recurrenceid)
11.93 + if event:
11.94 + objs.append(Object(event))
11.95
11.96 - # Obtain event objects.
11.97 + # Build a free/busy collection for the given user.
11.98 +
11.99 + for obj in objs:
11.100 + recurrenceids = not obj.get_recurrenceid() and storage.get_recurrences(user, obj.get_uid())
11.101 +
11.102 + # Obtain genuine attendees.
11.103
11.104 - objs = []
11.105 - for uid, recurrenceid in all_events:
11.106 - if verbose:
11.107 - print >>sys.stderr, uid, recurrenceid
11.108 - event = store.get_event(user, uid, recurrenceid)
11.109 - if event:
11.110 - objs.append(Object(event))
11.111 + if storage is journal:
11.112 + attendees = storage.get_delegates(user)
11.113 + else:
11.114 + attendees = [participant]
11.115 +
11.116 + # Generate records for each attendee (applicable to consolidated
11.117 + # journal data).
11.118
11.119 - # Build a free/busy collection for the given user.
11.120 + for attendee in attendees:
11.121 + partstat = obj.get_participation_status(attendee)
11.122 +
11.123 + # Only include objects where the attendee actually participates.
11.124
11.125 - for obj in objs:
11.126 - partstat = obj.get_participation_status(participant)
11.127 - recurrenceids = not obj.get_recurrenceid() and store.get_recurrences(user, obj.get_uid())
11.128 + if obj.get_participation(partstat, include_needs_action):
11.129 +
11.130 + # Add each active period to the collection.
11.131 +
11.132 + for p in obj.get_active_periods(recurrenceids, tzid, window_end):
11.133 +
11.134 + # Obtain a suitable period object.
11.135
11.136 - if obj.get_participation(partstat, include_needs_action):
11.137 - for p in obj.get_active_periods(recurrenceids, tzid, window_end):
11.138 - fbp = obj.get_freebusy_period(p, partstat == "ORG")
11.139 - fb.insert_period(fbp)
11.140 + fbp = obj.get_freebusy_period(p, partstat == "ORG")
11.141 +
11.142 + if storage is journal:
11.143 + fbp = FreeBusyGroupPeriod(*fbp.as_tuple(), attendee=attendee)
11.144
11.145 - # Store and publish the free/busy collection.
11.146 + fb.insert_period(fbp)
11.147 +
11.148 + # Store and publish the free/busy collection.
11.149 +
11.150 + if store_and_publish:
11.151
11.152 - if store_and_publish:
11.153 - if user == participant:
11.154 - store.set_freebusy(user, fb)
11.155 + # Set the user's own free/busy information.
11.156 +
11.157 + if participant is None:
11.158 + storage.set_freebusy(user, fb)
11.159
11.160 - if client.is_sharing() and client.is_publishing():
11.161 - publisher.set_freebusy(user, fb)
11.162 + if client.is_sharing() and client.is_publishing():
11.163 + publisher.set_freebusy(user, fb)
11.164 +
11.165 + # Set free/busy information concerning another user.
11.166 +
11.167 + else:
11.168 + storage.set_freebusy_for_other(user, fb, participant)
11.169
11.170 # Update the list of objects providing periods on future occasions.
11.171
11.172 - store.set_freebusy_providers(user, to_utc_datetime(window_end, tzid),
11.173 - [obj for obj in objs if obj.possibly_active_from(window_end, tzid)])
11.174 - else:
11.175 - store.set_freebusy_for_other(user, fb, participant)
11.176 + if participant is None or storage is journal:
11.177 + providers += [obj for obj in objs if obj.possibly_active_from(window_end, tzid)]
11.178 +
11.179 + # Alternatively, just write the collection to standard output.
11.180
11.181 - # Alternatively, just write the collection to standard output.
11.182 + else:
11.183 + f = getwriter("utf-8")(sys.stdout)
11.184 + for item in fb:
11.185 + print >>f, "\t".join(item.as_tuple(strings_only=True))
11.186
11.187 - else:
11.188 - f = getwriter("utf-8")(sys.stdout)
11.189 - for item in fb:
11.190 - print >>f, "\t".join(item.as_tuple(strings_only=True))
11.191 + # Update free/busy providers if storing.
11.192 +
11.193 + if store_and_publish:
11.194 + storage.set_freebusy_providers(user, to_utc_datetime(window_end, tzid), providers)
11.195
11.196 # Main program.
11.197
11.198 @@ -162,7 +209,7 @@
11.199 l = participants
11.200
11.201 for arg in sys.argv[1:]:
11.202 - if arg in ("-n", "-s", "-v", "-r"):
11.203 + if arg in ("-n", "-s", "-v", "-r", "-q"):
11.204 args.append(arg)
11.205 l = ignored
11.206 elif arg == "-T":
11.207 @@ -182,13 +229,14 @@
11.208 user = participants[0]
11.209 except IndexError:
11.210 print >>sys.stderr, """\
11.211 -Usage: %s <user> [ <other user> ] [ <options> ]
11.212 +Usage: %s <user> [ <other user> ... ] [ <options> ]
11.213
11.214 -Need a user and an optional participant (if different from the user),
11.215 +Need a user and optional participants (if different from the user),
11.216 along with the -s option if updating the store and the published details.
11.217
11.218 Specific options:
11.219
11.220 +-q Access quotas in the journal instead of users in the store
11.221 -s Update the store and published details (write details to standard output
11.222 otherwise)
11.223 -n Include objects with PARTSTAT of NEEDS-ACTION
11.224 @@ -207,7 +255,8 @@
11.225
11.226 # Define any other participant of interest plus options.
11.227
11.228 - participant = participants[1:] and participants[1] or None
11.229 + participants = participants[1:]
11.230 + using_journal = "-q" in args
11.231 store_and_publish = "-s" in args
11.232 include_needs_action = "-n" in args
11.233 reset_updated_list = "-r" in args
11.234 @@ -229,20 +278,44 @@
11.235 publisher = get_publisher(publishing_dir)
11.236 journal = get_journal(store_type, journal_dir)
11.237
11.238 + # Determine which kind of object will be accessed.
11.239 +
11.240 + if using_journal:
11.241 + storage = journal
11.242 + else:
11.243 + storage = store
11.244 +
11.245 # Obtain a list of users for processing.
11.246
11.247 if user in ("*", "all"):
11.248 - users = store.get_users()
11.249 + users = storage.get_users()
11.250 else:
11.251 users = [user]
11.252
11.253 + # Obtain a list of participants for processing.
11.254 +
11.255 + if participants and participants[0] in ("*", "all"):
11.256 + participants = storage.get_freebusy_others(user)
11.257 +
11.258 + # Provide a participants list to iterate over even if no specific
11.259 + # participant is involved. This updates a user's own records, but only for
11.260 + # the general data store.
11.261 +
11.262 + elif not participants:
11.263 + if not using_journal:
11.264 + participants = None
11.265 + else:
11.266 + print >>sys.stderr, "Participants must be indicated when updating quota records."
11.267 + sys.exit(1)
11.268 +
11.269 # Process the given users.
11.270
11.271 for user in users:
11.272 if verbose:
11.273 print >>sys.stderr, user
11.274 make_freebusy(
11.275 - Client(user, None, store, publisher, journal, preferences_dir), participant,
11.276 - store_and_publish, include_needs_action, reset_updated_list, verbose)
11.277 + Client(user, None, store, publisher, journal, preferences_dir),
11.278 + participants, storage, store_and_publish, include_needs_action,
11.279 + reset_updated_list, verbose)
11.280
11.281 # vim: tabstop=4 expandtab shiftwidth=4