# HG changeset patch # User Paul Boddie # Date 1463087718 -7200 # Node ID 6bc9f39224a9c51c94f60a7ba04a95052184c811 # Parent 7e03dc656fc1d6678d7db040034805b9f9a31e43 Added initial support for delegating attendance, introducing a quota scheduling function that appoints delegates within a quota group when a recipient cannot itself attend. Changed the journal methods setting user-group mappings and quota limits to operate using entire collections rather than with individual items. diff -r 7e03dc656fc1 -r 6bc9f39224a9 conf/postgresql/schema.sql --- a/conf/postgresql/schema.sql Thu May 12 23:05:48 2016 +0200 +++ b/conf/postgresql/schema.sql Thu May 12 23:15:18 2016 +0200 @@ -128,6 +128,12 @@ create index quota_freebusy_start on quota_freebusy(quota, user_group, "start"); create index quota_freebusy_end on quota_freebusy(quota, user_group, "end"); +create table quota_delegates ( + quota varchar not null, + store_user varchar not null, + primary key(quota, store_user) +); + create table user_freebusy ( quota varchar not null, store_user varchar not null, diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/client.py --- a/imiptools/client.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/client.py Thu May 12 23:15:18 2016 +0200 @@ -523,6 +523,19 @@ if attendee_map.has_key(attendee): attendee_map[attendee] = attendee_attr + # Check for delegated attendees. + + for attendee, attendee_attr in attendees.items(): + + # Identify delegates and check the delegation using the updated + # attendee information. + + if not attendee_map.has_key(attendee) and \ + attendee_attr.has_key("DELEGATED-FROM") and \ + check_delegation(attendee_map, attendee, attendee_attr): + + attendee_map[attendee] = attendee_attr + # Set the new details and store the object. obj["ATTENDEE"] = attendee_map.items() @@ -861,7 +874,8 @@ # organiser property attributes. attr = self.get_attendance(user, obj) - return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") not in ("DECLINED", "NEEDS-ACTION") + return as_organiser or attr is not None and not attr or \ + attr and attr.get("PARTSTAT") not in ("DECLINED", "DELEGATED", "NEEDS-ACTION") def has_indicated_attendance(self, user=None, obj=None): diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/data.py --- a/imiptools/data.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/data.py Thu May 12 23:15:18 2016 +0200 @@ -968,6 +968,28 @@ return is_same_sequence and ignore_dtstamp or not is_old_sequence +def check_delegation(attendee_map, attendee, attendee_attr): + + """ + Using the 'attendee_map', check the attributes for the given 'attendee' + provided as 'attendee_attr', following the delegation chain back to the + delegator and forward again to yield the delegate identity. Return + whether this identity is the given 'attendee', providing the delegator + identity; otherwise return None. + """ + + # The recipient should have a reference to the delegator. + + delegated_from = attendee_attr and attendee_attr.get("DELEGATED-FROM") + delegated_from = delegated_from and delegated_from[0] + delegator = delegated_from and attendee_map.get(delegated_from) + + # The delegator should have a reference to the recipient. + + delegated_to = delegator and delegator.get("DELEGATED-TO") + delegated_to = delegated_to and delegated_to[0] + return delegated_to == attendee and delegated_from or None + def get_periods(obj, tzid, end=None, inclusive=False): """ diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/handlers/__init__.py --- a/imiptools/handlers/__init__.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/handlers/__init__.py Thu May 12 23:15:18 2016 +0200 @@ -22,8 +22,8 @@ from email.mime.text import MIMEText from imiptools.client import ClientForObject from imiptools.config import MANAGER_PATH, MANAGER_URL, MANAGER_URL_SCHEME -from imiptools.data import get_address, get_uri, get_sender_identities, \ - uri_dict, uri_item +from imiptools.data import check_delegation, get_address, get_uri, \ + get_sender_identities, uri_dict, uri_item from socket import gethostname # References to the Web interface. @@ -150,13 +150,25 @@ else: return mapping + def is_delegation(self): + + """ + Return whether delegation is occurring by returning any delegator. + """ + + attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) + attendee_attr = attendee_map.get(self.user) + return check_delegation(attendee_map, self.user, attendee_attr) + def require_organiser(self, from_organiser=True): """ - Return the organiser for the current object, filtered for the sender or - recipient of interest. Return None if no identities are eligible. + Return the normalised organiser for the current object, filtered for the + sender or recipient of interest. Return None if no identities are + eligible. - The organiser identity is normalized. + If the sender is not the organiser but is delegating to the recipient, + the actual organiser is returned. """ organiser, organiser_attr = organiser_item = uri_item(self.obj.get_item("ORGANIZER")) @@ -164,11 +176,16 @@ if not organiser: return None - # Only provide details for an organiser who sent/receives the message. + # Check the delegate status of the recipient. + + delegated = from_organiser and self.is_delegation() + + # Only provide details for an organiser who sent/receives the message or + # is presiding over a delegation. organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient - if not organiser_filter_fn(dict([organiser_item])): + if not delegated and not organiser_filter_fn(dict([organiser_item])): return None # Test against any previously-received organiser details. diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/handlers/resource.py Thu May 12 23:15:18 2016 +0200 @@ -20,7 +20,7 @@ """ from email.mime.text import MIMEText -from imiptools.data import get_address, to_part, uri_dict +from imiptools.data import get_address, uri_dict from imiptools.handlers import Handler from imiptools.handlers.common import CommonFreebusy, CommonEvent from imiptools.handlers.scheduling import apply_scheduling_functions, \ @@ -90,6 +90,7 @@ "Attempt to schedule the current object for the current user." attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user] + delegate = None # Attempt to schedule the event. @@ -123,6 +124,19 @@ if scheduled == "ACCEPTED": self.confirm_scheduling() + # For delegated proposals, prepare a request to the delegate in + # addition to the usual response. + + elif scheduled == "DELEGATED": + method = "REPLY" + attendee_attr = self.update_participation("DELEGATED") + + # The recipient will have indicated the delegate whose details + # will have been added to the object. + + delegated_to = attendee_attr["DELEGATED-TO"] + delegate = delegated_to and delegated_to[0] + # For countered proposals, record the offer in the resource's # free/busy collection. @@ -141,6 +155,8 @@ finally: self.finish_scheduling() + # Determine the recipients of the outgoing messages. + recipients = map(get_address, self.obj.get_values("ORGANIZER")) # Add any description of the scheduling decision. @@ -151,9 +167,35 @@ # DTSTAMP in the response, and return the object for sending. self.update_sender(attendee_attr) - self.obj["ATTENDEE"] = [(self.user, attendee_attr)] + attendees = [(self.user, attendee_attr)] + + # Add the delegate if delegating (RFC 5546 being inconsistent here since + # it provides an example reply to the organiser without the delegate). + + if delegate: + delegate_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegate] + attendees.append((delegate, delegate_attr)) + + # Reply to the delegator in addition to the organiser if replying to a + # delegation request. + + delegator = self.is_delegation() + if delegator: + delegator_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegator] + attendees.append((delegator, delegator_attr)) + recipients.append(get_address(delegator)) + + # Prepare the response for the organiser plus any delegator. + + self.obj["ATTENDEE"] = attendees self.update_dtstamp() - self.add_result(method, recipients, to_part(method, [self.obj.to_node()])) + self.add_result(method, recipients, self.object_to_part(method, self.obj)) + + # If delegating, send a request to the delegate. + + if delegate: + method = "REQUEST" + self.add_result(method, [get_address(delegate)], self.object_to_part(method, self.obj)) def _cancel_for_attendee(self): diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/handlers/scheduling/__init__.py --- a/imiptools/handlers/scheduling/__init__.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/handlers/scheduling/__init__.py Thu May 12 23:15:18 2016 +0200 @@ -59,8 +59,8 @@ if not fn: return "DECLINED", None - # Keep evaluating scheduling functions, stopping only if one - # declines or gives a null response. + # Keep evaluating scheduling functions, stopping if one declines or + # gives a null response, or if one delegates to another resource. else: result = fn(handler, args) @@ -68,7 +68,7 @@ # Return a negative result immediately. - if result == "DECLINED": + if result in ("DECLINED", "DELEGATED"): return result, description # Modify the eventual response from acceptance if a countering diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/handlers/scheduling/quota.py --- a/imiptools/handlers/scheduling/quota.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/handlers/scheduling/quota.py Thu May 12 23:15:18 2016 +0200 @@ -20,7 +20,7 @@ """ from imiptools.dates import get_duration, to_utc_datetime -from imiptools.data import get_uri +from imiptools.data import get_uri, uri_dict from imiptools.period import Endless from datetime import timedelta @@ -250,6 +250,81 @@ return quota, organiser +# Delegation of reservations. + +def schedule_for_delegate(handler, args): + + """ + Check the current object of the given 'handler' against the schedules + managed by the quota, delegating to a specific recipient according to the + given policy. + """ + + _ = handler.get_translator() + + quota, group = _get_quota_and_group(handler, args) + policy = args and (args[1:] or ["arbitrary"])[0] + + # Determine the status of the recipient. + + attendee_map = uri_dict(handler.obj.get_value_map("ATTENDEE")) + attendee_attr = attendee_map[handler.user] + + # Prevent delegation by a delegate. + + if attendee_attr.get("DELEGATED-FROM"): + delegates = set([handler.user]) + + # Obtain the delegate pool for the quota. + + else: + delegates = handler.get_journal().get_delegates(quota) + + # Obtain the remaining delegates not already involved in the event. + + delegates = set(delegates).difference(attendee_map) + delegates.add(handler.user) + + # Get the quota's schedule for the requested periods and identify + # unavailable delegates. + + entries = handler.get_journal().get_entries(quota, group) + unavailable = set() + + for period in handler.get_periods(handler.obj): + overlapping = entries.get_overlapping(period) + + # Where scheduling cannot occur, find the busy potential delegates. + + if overlapping: + for p in overlapping: + unavailable.add(p.attendee) + + # Get the remaining, available delegates. + + available = delegates.difference(unavailable) + + # Apply the policy to choose an available delegate. + # NOTE: Currently an arbitrary delegate is chosen if not the recipient. + + if available: + delegate = handler.user in available and handler.user or list(available)[0] + + # Add attendee for delegate, obtaining the original attendee dictionary. + # Modify this user's status to refer to the delegate. + + if delegate != handler.user: + attendee_map = handler.obj.get_value_map("ATTENDEE") + attendee_map[delegate] = {"DELEGATED-FROM" : [handler.user]} + attendee_attr["DELEGATED-TO"] = [delegate] + handler.obj["ATTENDEE"] = attendee_map.items() + + return "DELEGATED", _("The recipient has delegated the requested period.") + else: + return "ACCEPTED", _("The recipient has scheduled the requested period.") + else: + return "DECLINED", _("The requested period cannot be scheduled.") + # Locking and unlocking. def lock_journal(handler, args): @@ -275,6 +350,7 @@ scheduling_functions = { "check_quota" : check_quota, "schedule_across_quota" : schedule_across_quota, + "schedule_for_delegate" : schedule_for_delegate, } # Registries of locking and unlocking functions. @@ -282,11 +358,13 @@ locking_functions = { "check_quota" : lock_journal, "schedule_across_quota" : lock_journal, + "schedule_for_delegate" : lock_journal, } unlocking_functions = { "check_quota" : unlock_journal, "schedule_across_quota" : unlock_journal, + "schedule_for_delegate" : unlock_journal, } # Registries of listener functions. diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/stores/database/common.py --- a/imiptools/stores/database/common.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/stores/database/common.py Thu May 12 23:15:18 2016 +0200 @@ -831,18 +831,62 @@ def get_quota_users(self, quota): - "Return a list of quota users." + "Return a list of quota users for the 'quota'." + + columns = ["quota"] + values = [quota] + + query, values = self.get_query( + "select distinct user_group from (" \ + "select user_group from quota_freebusy :condition " \ + "union all select user_group from quota_delegates :condition" \ + ") as users", + columns, values) + + self.cursor.execute(query, values) + return [r[0] for r in self.cursor.fetchall()] + + # Delegate information for the quota. + + def get_delegates(self, quota): + + "Return a list of delegates for 'quota'." columns = ["quota"] values = [quota] query, values = self.get_query( - "select distinct user_group from quota_freebusy :condition", + "select distinct store_user from quota_delegates :condition", columns, values) self.cursor.execute(query, values) return [r[0] for r in self.cursor.fetchall()] + def set_delegates(self, quota, delegates): + + "For the given 'quota', set the list of 'delegates'." + + columns = ["quota"] + values = [quota] + + query, values = self.get_query( + "delete from quota_delegates :condition", + columns, values) + + self.cursor.execute(query, values) + + for store_user in delegates: + + columns = ["quota", "store_user"] + values = [quota, store_user] + + query, values = self.get_query( + "insert into quota_delegates (:columns) values (:values)", + columns, values) + + self.cursor.execute(query, values) + return True + # Groups of users sharing quotas. def get_groups(self, quota): @@ -859,32 +903,27 @@ self.cursor.execute(query, values) return dict(self.cursor.fetchall()) - def set_group(self, quota, store_user, user_group): + def set_groups(self, quota, groups): - """ - For the given 'quota', set a mapping from 'store_user' to 'user_group'. - """ + "For the given 'quota', set 'groups' mapping users to groups." - columns = ["quota", "store_user"] - values = [quota, store_user] - setcolumns = ["user_group"] - setvalues = [user_group] + columns = ["quota"] + values = [quota] query, values = self.get_query( - "update user_groups :set :condition", - columns, values, setcolumns, setvalues) + "delete from user_groups :condition", + columns, values) self.cursor.execute(query, values) - if self.cursor.rowcount > 0: - return True + for store_user, user_group in groups.items(): - columns = ["quota", "store_user", "user_group"] - values = [quota, store_user, user_group] + columns = ["quota", "store_user", "user_group"] + values = [quota, store_user, user_group] - query, values = self.get_query( - "insert into user_groups (:columns) values (:values)", - columns, values) + query, values = self.get_query( + "insert into user_groups (:columns) values (:values)", + columns, values) self.cursor.execute(query, values) return True @@ -906,33 +945,30 @@ self.cursor.execute(query, values) return dict(self.cursor.fetchall()) - def set_limit(self, quota, group, limit): + def set_limits(self, quota, limits): """ - For the given 'quota', set for a user 'group' the given 'limit' on - resource usage. + For the given 'quota', set the given 'limits' on resource usage mapping + groups to limits. """ - columns = ["quota", "user_group"] - values = [quota, group] - setcolumns = ["quota_limit"] - setvalues = [limit] + columns = ["quota"] + values = [quota] query, values = self.get_query( - "update quota_limits :set :condition", - columns, values, setcolumns, setvalues) + "delete from quota_limits :condition", + columns, values) self.cursor.execute(query, values) - if self.cursor.rowcount > 0: - return True + for user_group, limit in limits.items(): - columns = ["quota", "user_group", "quota_limit"] - values = [quota, group, limit] + columns = ["quota", "user_group", "quota_limit"] + values = [quota, user_group, limit] - query, values = self.get_query( - "insert into quota_limits (:columns) values (:values)", - columns, values) + query, values = self.get_query( + "insert into quota_limits (:columns) values (:values)", + columns, values) self.cursor.execute(query, values) return True diff -r 7e03dc656fc1 -r 6bc9f39224a9 imiptools/stores/file.py --- a/imiptools/stores/file.py Thu May 12 23:05:48 2016 +0200 +++ b/imiptools/stores/file.py Thu May 12 23:15:18 2016 +0200 @@ -796,13 +796,36 @@ def get_quota_users(self, quota): - "Return a list of quota users." + "Return a list of quota users for 'quota'." filename = self.get_object_in_store(quota, "journal") if not filename or not isdir(filename): return [] - return listdir(filename) + return list(set(self.get_delegates(quota)).union(listdir(filename))) + + # Delegate information for the quota. + + def get_delegates(self, quota): + + "Return a list of delegates for 'quota'." + + filename = self.get_object_in_store(quota, "delegates") + if not filename or not isfile(filename): + return [] + + return [value for (value,) in self._get_table_atomic(quota, filename)] + + def set_delegates(self, quota, delegates): + + "For the given 'quota', set the list of 'delegates'." + + filename = self.get_object_in_store(quota, "delegates") + if not filename: + return False + + self._set_table_atomic(quota, filename, [(value,) for value in delegates]) + return True # Groups of users sharing quotas. @@ -816,19 +839,14 @@ return dict(self._get_table_atomic(quota, filename, tab_separated=False)) - def set_group(self, quota, store_user, user_group): + def set_groups(self, quota, groups): - """ - For the given 'quota', set a mapping from 'store_user' to 'user_group'. - """ + "For the given 'quota', set 'groups' mapping users to groups." filename = self.get_object_in_store(quota, "groups") if not filename: return False - groups = self.get_groups(quota) or {} - groups[store_user] = user_group - self._set_table_atomic(quota, filename, groups.items()) return True @@ -845,20 +863,17 @@ return dict(self._get_table_atomic(quota, filename, tab_separated=False)) - def set_limit(self, quota, group, limit): + def set_limits(self, quota, limits): """ - For the given 'quota', set for a user 'group' the given 'limit' on - resource usage. + For the given 'quota', set the given 'limits' on resource usage mapping + groups to limits. """ filename = self.get_object_in_store(quota, "limits") if not filename: return False - limits = self.get_limits(quota) or {} - limits[group] = limit - self._set_table_atomic(quota, filename, limits.items()) return True diff -r 7e03dc656fc1 -r 6bc9f39224a9 tests/common.sh --- a/tests/common.sh Thu May 12 23:05:48 2016 +0200 +++ b/tests/common.sh Thu May 12 23:15:18 2016 +0200 @@ -21,6 +21,9 @@ PERSON_SCRIPT="$BASE_DIR/imip_person.py" +SET_DELEGATES="$BASE_DIR/tools/set_delegates.py" +SET_DELEGATES_ARGS="-T $STORE_TYPE -j $JOURNAL" + SET_QUOTA_LIMIT="$BASE_DIR/tools/set_quota_limit.py" SET_QUOTA_LIMIT_ARGS="-T $STORE_TYPE -j $JOURNAL" diff -r 7e03dc656fc1 -r 6bc9f39224a9 tests/templates/event-request-car-delegating.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-car-delegating.txt Thu May 12 23:15:18 2016 +0200 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-car-porsche911@example.com +Subject: Invitation! + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-car-porsche911@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T163000 +DTEND;TZID=Europe/Oslo:20141126T173000 +SUMMARY:Another test drive +UID:event27@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r 7e03dc656fc1 -r 6bc9f39224a9 tests/test_resource_invitation_constraints_quota.sh --- a/tests/test_resource_invitation_constraints_quota.sh Thu May 12 23:05:48 2016 +0200 +++ b/tests/test_resource_invitation_constraints_quota.sh Thu May 12 23:15:18 2016 +0200 @@ -24,8 +24,12 @@ check_quota $QUOTA EOF -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT1H' $SET_QUOTA_LIMIT_ARGS -"$SET_QUOTA_LIMIT" "$OTHER_QUOTA" '*' 'PT1H' $SET_QUOTA_LIMIT_ARGS +cat <> $ERROR \ | "$SHOWMAIL" \ @@ -113,7 +117,9 @@ # Increase the quota. -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT2H' $SET_QUOTA_LIMIT_ARGS +cat < "$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-car.txt" 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \ +| tee out0s.tmp \ +| grep -q "^20141126T150000Z${TAB}20141126T160000Z" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR \ +| tee out1r.tmp \ +| "$SHOWMAIL" \ +> out1.tmp + + grep -q 'METHOD:REPLY' out1.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out1.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ +| tee out1f.tmp \ +| grep -q "^20141126T150000Z${TAB}20141126T160000Z" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is confirmed). + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \ +| tee out1e.tmp \ +| grep -q "event21@example.com" \ +&& echo "Success" \ +|| echo "Failed" + +# Attempt to schedule another event. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-delegating.txt" 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \ +| tee out1s.tmp \ +| grep -q "^20141126T153000Z${TAB}20141126T163000Z" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-delegating.txt" 2>> $ERROR \ +> out2r.tmp + + "$SHOWMAIL" < out2r.tmp \ +> out2p0.tmp + + "$SHOWMAIL" 1 < out2r.tmp \ +> out2p1.tmp + +if grep -q "To: $SENDERADDRESS" out2p0.tmp ; then + ORGFN=out2p0.tmp ; DELFN=out2p1.tmp +else + ORGFN=out2p1.tmp ; DELFN=out2p0.tmp +fi + +# One of the responses will be a request sent to the delegate. + + grep -q "To: $USER2ADDRESS" "$DELFN" \ +&& grep -q 'METHOD:REQUEST' "$DELFN" \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$DELFN" \ +&& grep -q 'ATTENDEE.*:'"$USER2" "$DELFN" \ +&& echo "Success" \ +|| echo "Failed" + +# The other will be a reply to the organiser. + + grep -q "To: $SENDERADDRESS" "$ORGFN" \ +&& grep -q 'METHOD:REPLY' "$ORGFN" \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$ORGFN" \ +&& grep -q 'ATTENDEE.*:'"$USER2" "$ORGFN" \ +&& echo "Success" \ +|| echo "Failed" + +# Neither the delegator or the delegate will have changed their schedules. + + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ +> out2f1.tmp + + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out2f1.tmp" \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \ +> out2f2.tmp + + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out2f2.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Check the quota (event is not confirmed). + + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \ +> out2e.tmp + + grep -q "event21@example.com" "out2e.tmp" \ +&& ! grep -q "event27@example.com" "out2e.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the reply to the organiser. + + "$PERSON_SCRIPT" $ARGS < "$ORGFN" 2>> "$ERROR" \ +| tee out3r.tmp \ +| "$SHOWMAIL" \ +> out3.tmp + +# Check the free/busy status of the attendees at the organiser. +# Currently, neither are attending. + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER1" \ +> out3s0.tmp \ + + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" out3s0.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER2" \ +> out3s1.tmp \ + + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" out3s1.tmp \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the delegate. + + "$RESOURCE_SCRIPT" $ARGS < "$DELFN" 2>> "$ERROR" \ +> out4r.tmp + + "$SHOWMAIL" < out4r.tmp \ +> out4p0.tmp + + "$SHOWMAIL" 1 < out4r.tmp \ +> out4p1.tmp + +if grep -q "To: $SENDERADDRESS" out4p0.tmp ; then + ORGFN=out4p0.tmp ; DELFN=out4p1.tmp +else + ORGFN=out4p1.tmp ; DELFN=out4p0.tmp +fi + +# One of the responses will be a reply sent to the organiser. + + grep -q "To: $SENDERADDRESS" "$ORGFN" \ +&& grep -q 'METHOD:REPLY' "$ORGFN" \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$ORGFN" \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED.*:'"$USER2" "$ORGFN" \ +&& echo "Success" \ +|| echo "Failed" + +# The other will be a reply to the delegator. + + grep -q "To: $USER1ADDRESS" "$DELFN" \ +&& grep -q 'METHOD:REPLY' "$DELFN" \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$DELFN" \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED.*:'"$USER2" "$DELFN" \ +&& echo "Success" \ +|| echo "Failed" + +# The delegate should now have a changed schedule. + + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ +> out4f1.tmp + + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out4f1.tmp" \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \ +> out4f2.tmp + + grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out4f2.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the reply to the organiser. + + "$PERSON_SCRIPT" $ARGS < "$ORGFN" 2>> "$ERROR" \ +| tee out5r.tmp \ +| "$SHOWMAIL" \ +> out5.tmp + +# Check the free/busy status of the attendees at the organiser. +# Now, the delegate is attending. + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER1" \ +> out5s0.tmp \ + + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" out5s0.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER2" \ +> out5s1.tmp \ + + grep -q "^20141126T153000Z${TAB}20141126T163000Z" out5s1.tmp \ +&& echo "Success" \ +|| echo "Failed" + +# Present the reply to the delegator. + + "$RESOURCE_SCRIPT" $ARGS < "$DELFN" 2>> "$ERROR" \ +> out6r.tmp diff -r 7e03dc656fc1 -r 6bc9f39224a9 tests/test_resource_invitation_constraints_quota_recurring.sh --- a/tests/test_resource_invitation_constraints_quota_recurring.sh Thu May 12 23:05:48 2016 +0200 +++ b/tests/test_resource_invitation_constraints_quota_recurring.sh Thu May 12 23:15:18 2016 +0200 @@ -16,7 +16,9 @@ # Employ a user-specific quota (no argument with the functions above). -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT10H' $SET_QUOTA_LIMIT_ARGS +cat <> $ERROR \ | "$SHOWMAIL" \ diff -r 7e03dc656fc1 -r 6bc9f39224a9 tests/test_resource_invitation_constraints_quota_recurring_limits.sh --- a/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Thu May 12 23:05:48 2016 +0200 +++ b/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Thu May 12 23:15:18 2016 +0200 @@ -26,8 +26,10 @@ check_quota $QUOTA EOF -"$SET_QUOTA_LIMIT" "$QUOTA" 'mailto:vincent.vole@example.com' 'PT10H' $SET_QUOTA_LIMIT_ARGS -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT5H' $SET_QUOTA_LIMIT_ARGS +cat <> $ERROR \ | "$SHOWMAIL" \ diff -r 7e03dc656fc1 -r 6bc9f39224a9 tools/copy_store.py --- a/tools/copy_store.py Thu May 12 23:05:48 2016 +0200 +++ b/tools/copy_store.py Thu May 12 23:15:18 2016 +0200 @@ -104,13 +104,15 @@ # Copy quota limits. - for user_group, limit in from_journal.get_limits(quota).items(): - to_journal.set_limit(quota, user_group, limit) + to_journal.set_limits(quota, from_journal.get_limits(quota)) # Copy group mappings. - for store_user, user_group in from_journal.get_groups(quota).items(): - to_journal.set_group(quota, store_user, user_group) + to_journal.set_groups(quota, from_journal.get_groups(quota)) + + # Copy delegates. + + to_journal.set_delegates(quota, from_journal.get_delegates(quota)) # Copy journal details. diff -r 7e03dc656fc1 -r 6bc9f39224a9 tools/install.sh --- a/tools/install.sh Thu May 12 23:05:48 2016 +0200 +++ b/tools/install.sh Thu May 12 23:15:18 2016 +0200 @@ -103,7 +103,7 @@ # Tools -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" +TOOLS="copy_store.py fix.sh init.sh init_user.sh make_freebusy.py set_delegates.py set_quota_limit.py update_quotas.py update_scheduling_modules.py" if [ ! -e "$INSTALL_DIR/tools" ]; then mkdir -p "$INSTALL_DIR/tools" diff -r 7e03dc656fc1 -r 6bc9f39224a9 tools/set_delegates.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/set_delegates.py Thu May 12 23:15:18 2016 +0200 @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +""" +Set delegates for a particular quota. + +Copyright (C) 2016 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from codecs import getreader +from os.path import abspath, split +import sys + +# Find the modules. + +try: + import imiptools +except ImportError: + parent = abspath(split(split(__file__)[0])[0]) + if split(parent)[1] == "imip-agent": + sys.path.append(parent) + +from imiptools import config +from imiptools.stores import get_journal +from imiptools.text import get_table_from_stream + +# Main program. + +if __name__ == "__main__": + + # Interpret the command line arguments. + + args = [] + store_type = [] + journal_dir = [] + + # Collect quota details first, switching to other arguments when encountering + # switches. + + l = args + + for arg in sys.argv[1:]: + if arg == "-T": + l = store_type + elif arg == "-j": + l = journal_dir + else: + l.append(arg) + + try: + quota, = args + except ValueError: + print >>sys.stderr, """\ +Usage: %s ... [ ] + +General options: + +-j Indicates the journal directory location +-T Indicates the store type (the configured value if omitted) +""" % split(sys.argv[0])[1] + sys.exit(1) + + # Override defaults if indicated. + + getvalue = lambda value, default=None: value and value[0] or default + + store_type = getvalue(store_type, config.STORE_TYPE) + journal_dir = getvalue(journal_dir) + + # Obtain store-related objects. + + journal = get_journal(store_type, journal_dir) + f = getreader("utf-8")(sys.stdin) + delegates = get_table_from_stream(f, tab_separated=False) + journal.set_delegates(quota, [value for (value,) in delegates]) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 7e03dc656fc1 -r 6bc9f39224a9 tools/set_quota_limit.py --- a/tools/set_quota_limit.py Thu May 12 23:05:48 2016 +0200 +++ b/tools/set_quota_limit.py Thu May 12 23:15:18 2016 +0200 @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Set a quota limit for a user group. +Set quota limits for a collection of user groups. Copyright (C) 2016 Paul Boddie @@ -19,6 +19,7 @@ this program. If not, see . """ +from codecs import getreader from os.path import abspath, split import sys @@ -33,6 +34,7 @@ from imiptools import config from imiptools.stores import get_journal +from imiptools.text import get_table_from_stream # Main program. @@ -58,10 +60,21 @@ l.append(arg) try: - quota, group, limit = args + quota, = args except ValueError: print >>sys.stderr, """\ -Usage: %s [ ] +Usage: %s [ ] + +Read from standard input a list of group-to-limit mappings of the following +form: + + + +For example: + +* PT1H + +The values may be separated using any whitespace characters. General options: @@ -80,6 +93,8 @@ # Obtain store-related objects. journal = get_journal(store_type, journal_dir) - journal.set_limit(quota, group, limit) + f = getreader("utf-8")(sys.stdin) + limits = dict(get_table_from_stream(f, tab_separated=False)) + journal.set_limits(quota, limits) # vim: tabstop=4 expandtab shiftwidth=4