1.1 --- a/conf/postgresql/schema.sql Thu May 12 23:05:48 2016 +0200 1.2 +++ b/conf/postgresql/schema.sql Thu May 12 23:15:18 2016 +0200 1.3 @@ -128,6 +128,12 @@ 1.4 create index quota_freebusy_start on quota_freebusy(quota, user_group, "start"); 1.5 create index quota_freebusy_end on quota_freebusy(quota, user_group, "end"); 1.6 1.7 +create table quota_delegates ( 1.8 + quota varchar not null, 1.9 + store_user varchar not null, 1.10 + primary key(quota, store_user) 1.11 +); 1.12 + 1.13 create table user_freebusy ( 1.14 quota varchar not null, 1.15 store_user varchar not null,
2.1 --- a/imiptools/client.py Thu May 12 23:05:48 2016 +0200 2.2 +++ b/imiptools/client.py Thu May 12 23:15:18 2016 +0200 2.3 @@ -523,6 +523,19 @@ 2.4 if attendee_map.has_key(attendee): 2.5 attendee_map[attendee] = attendee_attr 2.6 2.7 + # Check for delegated attendees. 2.8 + 2.9 + for attendee, attendee_attr in attendees.items(): 2.10 + 2.11 + # Identify delegates and check the delegation using the updated 2.12 + # attendee information. 2.13 + 2.14 + if not attendee_map.has_key(attendee) and \ 2.15 + attendee_attr.has_key("DELEGATED-FROM") and \ 2.16 + check_delegation(attendee_map, attendee, attendee_attr): 2.17 + 2.18 + attendee_map[attendee] = attendee_attr 2.19 + 2.20 # Set the new details and store the object. 2.21 2.22 obj["ATTENDEE"] = attendee_map.items() 2.23 @@ -861,7 +874,8 @@ 2.24 # organiser property attributes. 2.25 2.26 attr = self.get_attendance(user, obj) 2.27 - return as_organiser or attr is not None and not attr or attr and attr.get("PARTSTAT") not in ("DECLINED", "NEEDS-ACTION") 2.28 + return as_organiser or attr is not None and not attr or \ 2.29 + attr and attr.get("PARTSTAT") not in ("DECLINED", "DELEGATED", "NEEDS-ACTION") 2.30 2.31 def has_indicated_attendance(self, user=None, obj=None): 2.32
3.1 --- a/imiptools/data.py Thu May 12 23:05:48 2016 +0200 3.2 +++ b/imiptools/data.py Thu May 12 23:15:18 2016 +0200 3.3 @@ -968,6 +968,28 @@ 3.4 3.5 return is_same_sequence and ignore_dtstamp or not is_old_sequence 3.6 3.7 +def check_delegation(attendee_map, attendee, attendee_attr): 3.8 + 3.9 + """ 3.10 + Using the 'attendee_map', check the attributes for the given 'attendee' 3.11 + provided as 'attendee_attr', following the delegation chain back to the 3.12 + delegator and forward again to yield the delegate identity. Return 3.13 + whether this identity is the given 'attendee', providing the delegator 3.14 + identity; otherwise return None. 3.15 + """ 3.16 + 3.17 + # The recipient should have a reference to the delegator. 3.18 + 3.19 + delegated_from = attendee_attr and attendee_attr.get("DELEGATED-FROM") 3.20 + delegated_from = delegated_from and delegated_from[0] 3.21 + delegator = delegated_from and attendee_map.get(delegated_from) 3.22 + 3.23 + # The delegator should have a reference to the recipient. 3.24 + 3.25 + delegated_to = delegator and delegator.get("DELEGATED-TO") 3.26 + delegated_to = delegated_to and delegated_to[0] 3.27 + return delegated_to == attendee and delegated_from or None 3.28 + 3.29 def get_periods(obj, tzid, end=None, inclusive=False): 3.30 3.31 """
4.1 --- a/imiptools/handlers/__init__.py Thu May 12 23:05:48 2016 +0200 4.2 +++ b/imiptools/handlers/__init__.py Thu May 12 23:15:18 2016 +0200 4.3 @@ -22,8 +22,8 @@ 4.4 from email.mime.text import MIMEText 4.5 from imiptools.client import ClientForObject 4.6 from imiptools.config import MANAGER_PATH, MANAGER_URL, MANAGER_URL_SCHEME 4.7 -from imiptools.data import get_address, get_uri, get_sender_identities, \ 4.8 - uri_dict, uri_item 4.9 +from imiptools.data import check_delegation, get_address, get_uri, \ 4.10 + get_sender_identities, uri_dict, uri_item 4.11 from socket import gethostname 4.12 4.13 # References to the Web interface. 4.14 @@ -150,13 +150,25 @@ 4.15 else: 4.16 return mapping 4.17 4.18 + def is_delegation(self): 4.19 + 4.20 + """ 4.21 + Return whether delegation is occurring by returning any delegator. 4.22 + """ 4.23 + 4.24 + attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 4.25 + attendee_attr = attendee_map.get(self.user) 4.26 + return check_delegation(attendee_map, self.user, attendee_attr) 4.27 + 4.28 def require_organiser(self, from_organiser=True): 4.29 4.30 """ 4.31 - Return the organiser for the current object, filtered for the sender or 4.32 - recipient of interest. Return None if no identities are eligible. 4.33 + Return the normalised organiser for the current object, filtered for the 4.34 + sender or recipient of interest. Return None if no identities are 4.35 + eligible. 4.36 4.37 - The organiser identity is normalized. 4.38 + If the sender is not the organiser but is delegating to the recipient, 4.39 + the actual organiser is returned. 4.40 """ 4.41 4.42 organiser, organiser_attr = organiser_item = uri_item(self.obj.get_item("ORGANIZER")) 4.43 @@ -164,11 +176,16 @@ 4.44 if not organiser: 4.45 return None 4.46 4.47 - # Only provide details for an organiser who sent/receives the message. 4.48 + # Check the delegate status of the recipient. 4.49 + 4.50 + delegated = from_organiser and self.is_delegation() 4.51 + 4.52 + # Only provide details for an organiser who sent/receives the message or 4.53 + # is presiding over a delegation. 4.54 4.55 organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient 4.56 4.57 - if not organiser_filter_fn(dict([organiser_item])): 4.58 + if not delegated and not organiser_filter_fn(dict([organiser_item])): 4.59 return None 4.60 4.61 # Test against any previously-received organiser details.
5.1 --- a/imiptools/handlers/resource.py Thu May 12 23:05:48 2016 +0200 5.2 +++ b/imiptools/handlers/resource.py Thu May 12 23:15:18 2016 +0200 5.3 @@ -20,7 +20,7 @@ 5.4 """ 5.5 5.6 from email.mime.text import MIMEText 5.7 -from imiptools.data import get_address, to_part, uri_dict 5.8 +from imiptools.data import get_address, uri_dict 5.9 from imiptools.handlers import Handler 5.10 from imiptools.handlers.common import CommonFreebusy, CommonEvent 5.11 from imiptools.handlers.scheduling import apply_scheduling_functions, \ 5.12 @@ -90,6 +90,7 @@ 5.13 "Attempt to schedule the current object for the current user." 5.14 5.15 attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user] 5.16 + delegate = None 5.17 5.18 # Attempt to schedule the event. 5.19 5.20 @@ -123,6 +124,19 @@ 5.21 if scheduled == "ACCEPTED": 5.22 self.confirm_scheduling() 5.23 5.24 + # For delegated proposals, prepare a request to the delegate in 5.25 + # addition to the usual response. 5.26 + 5.27 + elif scheduled == "DELEGATED": 5.28 + method = "REPLY" 5.29 + attendee_attr = self.update_participation("DELEGATED") 5.30 + 5.31 + # The recipient will have indicated the delegate whose details 5.32 + # will have been added to the object. 5.33 + 5.34 + delegated_to = attendee_attr["DELEGATED-TO"] 5.35 + delegate = delegated_to and delegated_to[0] 5.36 + 5.37 # For countered proposals, record the offer in the resource's 5.38 # free/busy collection. 5.39 5.40 @@ -141,6 +155,8 @@ 5.41 finally: 5.42 self.finish_scheduling() 5.43 5.44 + # Determine the recipients of the outgoing messages. 5.45 + 5.46 recipients = map(get_address, self.obj.get_values("ORGANIZER")) 5.47 5.48 # Add any description of the scheduling decision. 5.49 @@ -151,9 +167,35 @@ 5.50 # DTSTAMP in the response, and return the object for sending. 5.51 5.52 self.update_sender(attendee_attr) 5.53 - self.obj["ATTENDEE"] = [(self.user, attendee_attr)] 5.54 + attendees = [(self.user, attendee_attr)] 5.55 + 5.56 + # Add the delegate if delegating (RFC 5546 being inconsistent here since 5.57 + # it provides an example reply to the organiser without the delegate). 5.58 + 5.59 + if delegate: 5.60 + delegate_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegate] 5.61 + attendees.append((delegate, delegate_attr)) 5.62 + 5.63 + # Reply to the delegator in addition to the organiser if replying to a 5.64 + # delegation request. 5.65 + 5.66 + delegator = self.is_delegation() 5.67 + if delegator: 5.68 + delegator_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegator] 5.69 + attendees.append((delegator, delegator_attr)) 5.70 + recipients.append(get_address(delegator)) 5.71 + 5.72 + # Prepare the response for the organiser plus any delegator. 5.73 + 5.74 + self.obj["ATTENDEE"] = attendees 5.75 self.update_dtstamp() 5.76 - self.add_result(method, recipients, to_part(method, [self.obj.to_node()])) 5.77 + self.add_result(method, recipients, self.object_to_part(method, self.obj)) 5.78 + 5.79 + # If delegating, send a request to the delegate. 5.80 + 5.81 + if delegate: 5.82 + method = "REQUEST" 5.83 + self.add_result(method, [get_address(delegate)], self.object_to_part(method, self.obj)) 5.84 5.85 def _cancel_for_attendee(self): 5.86
6.1 --- a/imiptools/handlers/scheduling/__init__.py Thu May 12 23:05:48 2016 +0200 6.2 +++ b/imiptools/handlers/scheduling/__init__.py Thu May 12 23:15:18 2016 +0200 6.3 @@ -59,8 +59,8 @@ 6.4 if not fn: 6.5 return "DECLINED", None 6.6 6.7 - # Keep evaluating scheduling functions, stopping only if one 6.8 - # declines or gives a null response. 6.9 + # Keep evaluating scheduling functions, stopping if one declines or 6.10 + # gives a null response, or if one delegates to another resource. 6.11 6.12 else: 6.13 result = fn(handler, args) 6.14 @@ -68,7 +68,7 @@ 6.15 6.16 # Return a negative result immediately. 6.17 6.18 - if result == "DECLINED": 6.19 + if result in ("DECLINED", "DELEGATED"): 6.20 return result, description 6.21 6.22 # Modify the eventual response from acceptance if a countering
7.1 --- a/imiptools/handlers/scheduling/quota.py Thu May 12 23:05:48 2016 +0200 7.2 +++ b/imiptools/handlers/scheduling/quota.py Thu May 12 23:15:18 2016 +0200 7.3 @@ -20,7 +20,7 @@ 7.4 """ 7.5 7.6 from imiptools.dates import get_duration, to_utc_datetime 7.7 -from imiptools.data import get_uri 7.8 +from imiptools.data import get_uri, uri_dict 7.9 from imiptools.period import Endless 7.10 from datetime import timedelta 7.11 7.12 @@ -250,6 +250,81 @@ 7.13 7.14 return quota, organiser 7.15 7.16 +# Delegation of reservations. 7.17 + 7.18 +def schedule_for_delegate(handler, args): 7.19 + 7.20 + """ 7.21 + Check the current object of the given 'handler' against the schedules 7.22 + managed by the quota, delegating to a specific recipient according to the 7.23 + given policy. 7.24 + """ 7.25 + 7.26 + _ = handler.get_translator() 7.27 + 7.28 + quota, group = _get_quota_and_group(handler, args) 7.29 + policy = args and (args[1:] or ["arbitrary"])[0] 7.30 + 7.31 + # Determine the status of the recipient. 7.32 + 7.33 + attendee_map = uri_dict(handler.obj.get_value_map("ATTENDEE")) 7.34 + attendee_attr = attendee_map[handler.user] 7.35 + 7.36 + # Prevent delegation by a delegate. 7.37 + 7.38 + if attendee_attr.get("DELEGATED-FROM"): 7.39 + delegates = set([handler.user]) 7.40 + 7.41 + # Obtain the delegate pool for the quota. 7.42 + 7.43 + else: 7.44 + delegates = handler.get_journal().get_delegates(quota) 7.45 + 7.46 + # Obtain the remaining delegates not already involved in the event. 7.47 + 7.48 + delegates = set(delegates).difference(attendee_map) 7.49 + delegates.add(handler.user) 7.50 + 7.51 + # Get the quota's schedule for the requested periods and identify 7.52 + # unavailable delegates. 7.53 + 7.54 + entries = handler.get_journal().get_entries(quota, group) 7.55 + unavailable = set() 7.56 + 7.57 + for period in handler.get_periods(handler.obj): 7.58 + overlapping = entries.get_overlapping(period) 7.59 + 7.60 + # Where scheduling cannot occur, find the busy potential delegates. 7.61 + 7.62 + if overlapping: 7.63 + for p in overlapping: 7.64 + unavailable.add(p.attendee) 7.65 + 7.66 + # Get the remaining, available delegates. 7.67 + 7.68 + available = delegates.difference(unavailable) 7.69 + 7.70 + # Apply the policy to choose an available delegate. 7.71 + # NOTE: Currently an arbitrary delegate is chosen if not the recipient. 7.72 + 7.73 + if available: 7.74 + delegate = handler.user in available and handler.user or list(available)[0] 7.75 + 7.76 + # Add attendee for delegate, obtaining the original attendee dictionary. 7.77 + # Modify this user's status to refer to the delegate. 7.78 + 7.79 + if delegate != handler.user: 7.80 + attendee_map = handler.obj.get_value_map("ATTENDEE") 7.81 + attendee_map[delegate] = {"DELEGATED-FROM" : [handler.user]} 7.82 + attendee_attr["DELEGATED-TO"] = [delegate] 7.83 + handler.obj["ATTENDEE"] = attendee_map.items() 7.84 + 7.85 + return "DELEGATED", _("The recipient has delegated the requested period.") 7.86 + else: 7.87 + return "ACCEPTED", _("The recipient has scheduled the requested period.") 7.88 + else: 7.89 + return "DECLINED", _("The requested period cannot be scheduled.") 7.90 + 7.91 # Locking and unlocking. 7.92 7.93 def lock_journal(handler, args): 7.94 @@ -275,6 +350,7 @@ 7.95 scheduling_functions = { 7.96 "check_quota" : check_quota, 7.97 "schedule_across_quota" : schedule_across_quota, 7.98 + "schedule_for_delegate" : schedule_for_delegate, 7.99 } 7.100 7.101 # Registries of locking and unlocking functions. 7.102 @@ -282,11 +358,13 @@ 7.103 locking_functions = { 7.104 "check_quota" : lock_journal, 7.105 "schedule_across_quota" : lock_journal, 7.106 + "schedule_for_delegate" : lock_journal, 7.107 } 7.108 7.109 unlocking_functions = { 7.110 "check_quota" : unlock_journal, 7.111 "schedule_across_quota" : unlock_journal, 7.112 + "schedule_for_delegate" : unlock_journal, 7.113 } 7.114 7.115 # Registries of listener functions.
8.1 --- a/imiptools/stores/database/common.py Thu May 12 23:05:48 2016 +0200 8.2 +++ b/imiptools/stores/database/common.py Thu May 12 23:15:18 2016 +0200 8.3 @@ -831,18 +831,62 @@ 8.4 8.5 def get_quota_users(self, quota): 8.6 8.7 - "Return a list of quota users." 8.8 + "Return a list of quota users for the 'quota'." 8.9 + 8.10 + columns = ["quota"] 8.11 + values = [quota] 8.12 + 8.13 + query, values = self.get_query( 8.14 + "select distinct user_group from (" \ 8.15 + "select user_group from quota_freebusy :condition " \ 8.16 + "union all select user_group from quota_delegates :condition" \ 8.17 + ") as users", 8.18 + columns, values) 8.19 + 8.20 + self.cursor.execute(query, values) 8.21 + return [r[0] for r in self.cursor.fetchall()] 8.22 + 8.23 + # Delegate information for the quota. 8.24 + 8.25 + def get_delegates(self, quota): 8.26 + 8.27 + "Return a list of delegates for 'quota'." 8.28 8.29 columns = ["quota"] 8.30 values = [quota] 8.31 8.32 query, values = self.get_query( 8.33 - "select distinct user_group from quota_freebusy :condition", 8.34 + "select distinct store_user from quota_delegates :condition", 8.35 columns, values) 8.36 8.37 self.cursor.execute(query, values) 8.38 return [r[0] for r in self.cursor.fetchall()] 8.39 8.40 + def set_delegates(self, quota, delegates): 8.41 + 8.42 + "For the given 'quota', set the list of 'delegates'." 8.43 + 8.44 + columns = ["quota"] 8.45 + values = [quota] 8.46 + 8.47 + query, values = self.get_query( 8.48 + "delete from quota_delegates :condition", 8.49 + columns, values) 8.50 + 8.51 + self.cursor.execute(query, values) 8.52 + 8.53 + for store_user in delegates: 8.54 + 8.55 + columns = ["quota", "store_user"] 8.56 + values = [quota, store_user] 8.57 + 8.58 + query, values = self.get_query( 8.59 + "insert into quota_delegates (:columns) values (:values)", 8.60 + columns, values) 8.61 + 8.62 + self.cursor.execute(query, values) 8.63 + return True 8.64 + 8.65 # Groups of users sharing quotas. 8.66 8.67 def get_groups(self, quota): 8.68 @@ -859,32 +903,27 @@ 8.69 self.cursor.execute(query, values) 8.70 return dict(self.cursor.fetchall()) 8.71 8.72 - def set_group(self, quota, store_user, user_group): 8.73 + def set_groups(self, quota, groups): 8.74 8.75 - """ 8.76 - For the given 'quota', set a mapping from 'store_user' to 'user_group'. 8.77 - """ 8.78 + "For the given 'quota', set 'groups' mapping users to groups." 8.79 8.80 - columns = ["quota", "store_user"] 8.81 - values = [quota, store_user] 8.82 - setcolumns = ["user_group"] 8.83 - setvalues = [user_group] 8.84 + columns = ["quota"] 8.85 + values = [quota] 8.86 8.87 query, values = self.get_query( 8.88 - "update user_groups :set :condition", 8.89 - columns, values, setcolumns, setvalues) 8.90 + "delete from user_groups :condition", 8.91 + columns, values) 8.92 8.93 self.cursor.execute(query, values) 8.94 8.95 - if self.cursor.rowcount > 0: 8.96 - return True 8.97 + for store_user, user_group in groups.items(): 8.98 8.99 - columns = ["quota", "store_user", "user_group"] 8.100 - values = [quota, store_user, user_group] 8.101 + columns = ["quota", "store_user", "user_group"] 8.102 + values = [quota, store_user, user_group] 8.103 8.104 - query, values = self.get_query( 8.105 - "insert into user_groups (:columns) values (:values)", 8.106 - columns, values) 8.107 + query, values = self.get_query( 8.108 + "insert into user_groups (:columns) values (:values)", 8.109 + columns, values) 8.110 8.111 self.cursor.execute(query, values) 8.112 return True 8.113 @@ -906,33 +945,30 @@ 8.114 self.cursor.execute(query, values) 8.115 return dict(self.cursor.fetchall()) 8.116 8.117 - def set_limit(self, quota, group, limit): 8.118 + def set_limits(self, quota, limits): 8.119 8.120 """ 8.121 - For the given 'quota', set for a user 'group' the given 'limit' on 8.122 - resource usage. 8.123 + For the given 'quota', set the given 'limits' on resource usage mapping 8.124 + groups to limits. 8.125 """ 8.126 8.127 - columns = ["quota", "user_group"] 8.128 - values = [quota, group] 8.129 - setcolumns = ["quota_limit"] 8.130 - setvalues = [limit] 8.131 + columns = ["quota"] 8.132 + values = [quota] 8.133 8.134 query, values = self.get_query( 8.135 - "update quota_limits :set :condition", 8.136 - columns, values, setcolumns, setvalues) 8.137 + "delete from quota_limits :condition", 8.138 + columns, values) 8.139 8.140 self.cursor.execute(query, values) 8.141 8.142 - if self.cursor.rowcount > 0: 8.143 - return True 8.144 + for user_group, limit in limits.items(): 8.145 8.146 - columns = ["quota", "user_group", "quota_limit"] 8.147 - values = [quota, group, limit] 8.148 + columns = ["quota", "user_group", "quota_limit"] 8.149 + values = [quota, user_group, limit] 8.150 8.151 - query, values = self.get_query( 8.152 - "insert into quota_limits (:columns) values (:values)", 8.153 - columns, values) 8.154 + query, values = self.get_query( 8.155 + "insert into quota_limits (:columns) values (:values)", 8.156 + columns, values) 8.157 8.158 self.cursor.execute(query, values) 8.159 return True
9.1 --- a/imiptools/stores/file.py Thu May 12 23:05:48 2016 +0200 9.2 +++ b/imiptools/stores/file.py Thu May 12 23:15:18 2016 +0200 9.3 @@ -796,13 +796,36 @@ 9.4 9.5 def get_quota_users(self, quota): 9.6 9.7 - "Return a list of quota users." 9.8 + "Return a list of quota users for 'quota'." 9.9 9.10 filename = self.get_object_in_store(quota, "journal") 9.11 if not filename or not isdir(filename): 9.12 return [] 9.13 9.14 - return listdir(filename) 9.15 + return list(set(self.get_delegates(quota)).union(listdir(filename))) 9.16 + 9.17 + # Delegate information for the quota. 9.18 + 9.19 + def get_delegates(self, quota): 9.20 + 9.21 + "Return a list of delegates for 'quota'." 9.22 + 9.23 + filename = self.get_object_in_store(quota, "delegates") 9.24 + if not filename or not isfile(filename): 9.25 + return [] 9.26 + 9.27 + return [value for (value,) in self._get_table_atomic(quota, filename)] 9.28 + 9.29 + def set_delegates(self, quota, delegates): 9.30 + 9.31 + "For the given 'quota', set the list of 'delegates'." 9.32 + 9.33 + filename = self.get_object_in_store(quota, "delegates") 9.34 + if not filename: 9.35 + return False 9.36 + 9.37 + self._set_table_atomic(quota, filename, [(value,) for value in delegates]) 9.38 + return True 9.39 9.40 # Groups of users sharing quotas. 9.41 9.42 @@ -816,19 +839,14 @@ 9.43 9.44 return dict(self._get_table_atomic(quota, filename, tab_separated=False)) 9.45 9.46 - def set_group(self, quota, store_user, user_group): 9.47 + def set_groups(self, quota, groups): 9.48 9.49 - """ 9.50 - For the given 'quota', set a mapping from 'store_user' to 'user_group'. 9.51 - """ 9.52 + "For the given 'quota', set 'groups' mapping users to groups." 9.53 9.54 filename = self.get_object_in_store(quota, "groups") 9.55 if not filename: 9.56 return False 9.57 9.58 - groups = self.get_groups(quota) or {} 9.59 - groups[store_user] = user_group 9.60 - 9.61 self._set_table_atomic(quota, filename, groups.items()) 9.62 return True 9.63 9.64 @@ -845,20 +863,17 @@ 9.65 9.66 return dict(self._get_table_atomic(quota, filename, tab_separated=False)) 9.67 9.68 - def set_limit(self, quota, group, limit): 9.69 + def set_limits(self, quota, limits): 9.70 9.71 """ 9.72 - For the given 'quota', set for a user 'group' the given 'limit' on 9.73 - resource usage. 9.74 + For the given 'quota', set the given 'limits' on resource usage mapping 9.75 + groups to limits. 9.76 """ 9.77 9.78 filename = self.get_object_in_store(quota, "limits") 9.79 if not filename: 9.80 return False 9.81 9.82 - limits = self.get_limits(quota) or {} 9.83 - limits[group] = limit 9.84 - 9.85 self._set_table_atomic(quota, filename, limits.items()) 9.86 return True 9.87
10.1 --- a/tests/common.sh Thu May 12 23:05:48 2016 +0200 10.2 +++ b/tests/common.sh Thu May 12 23:15:18 2016 +0200 10.3 @@ -21,6 +21,9 @@ 10.4 10.5 PERSON_SCRIPT="$BASE_DIR/imip_person.py" 10.6 10.7 +SET_DELEGATES="$BASE_DIR/tools/set_delegates.py" 10.8 +SET_DELEGATES_ARGS="-T $STORE_TYPE -j $JOURNAL" 10.9 + 10.10 SET_QUOTA_LIMIT="$BASE_DIR/tools/set_quota_limit.py" 10.11 SET_QUOTA_LIMIT_ARGS="-T $STORE_TYPE -j $JOURNAL" 10.12
11.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 11.2 +++ b/tests/templates/event-request-car-delegating.txt Thu May 12 23:15:18 2016 +0200 11.3 @@ -0,0 +1,34 @@ 11.4 +Content-Type: multipart/alternative; boundary="===============0047278175==" 11.5 +MIME-Version: 1.0 11.6 +From: paul.boddie@example.com 11.7 +To: resource-car-porsche911@example.com 11.8 +Subject: Invitation! 11.9 + 11.10 +--===============0047278175== 11.11 +Content-Type: text/plain; charset="us-ascii" 11.12 +MIME-Version: 1.0 11.13 +Content-Transfer-Encoding: 7bit 11.14 + 11.15 +This message contains an event. 11.16 +--===============0047278175== 11.17 +MIME-Version: 1.0 11.18 +Content-Transfer-Encoding: 7bit 11.19 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" 11.20 + 11.21 +BEGIN:VCALENDAR 11.22 +PRODID:-//imip-agent/test//EN 11.23 +METHOD:REQUEST 11.24 +VERSION:2.0 11.25 +BEGIN:VEVENT 11.26 +ORGANIZER:mailto:paul.boddie@example.com 11.27 +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com 11.28 +ATTENDEE;RSVP=TRUE:mailto:resource-car-porsche911@example.com 11.29 +DTSTAMP:20141125T004600Z 11.30 +DTSTART;TZID=Europe/Oslo:20141126T163000 11.31 +DTEND;TZID=Europe/Oslo:20141126T173000 11.32 +SUMMARY:Another test drive 11.33 +UID:event27@example.com 11.34 +END:VEVENT 11.35 +END:VCALENDAR 11.36 + 11.37 +--===============0047278175==--
12.1 --- a/tests/test_resource_invitation_constraints_quota.sh Thu May 12 23:05:48 2016 +0200 12.2 +++ b/tests/test_resource_invitation_constraints_quota.sh Thu May 12 23:15:18 2016 +0200 12.3 @@ -24,8 +24,12 @@ 12.4 check_quota $QUOTA 12.5 EOF 12.6 12.7 -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT1H' $SET_QUOTA_LIMIT_ARGS 12.8 -"$SET_QUOTA_LIMIT" "$OTHER_QUOTA" '*' 'PT1H' $SET_QUOTA_LIMIT_ARGS 12.9 +cat <<EOF | "$SET_QUOTA_LIMIT" "$QUOTA" $SET_QUOTA_LIMIT_ARGS 12.10 +* PT1H 12.11 +EOF 12.12 +cat <<EOF | "$SET_QUOTA_LIMIT" "$OTHER_QUOTA" $SET_QUOTA_LIMIT_ARGS 12.13 +* PT1H 12.14 +EOF 12.15 12.16 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car.txt" 2>> $ERROR \ 12.17 | "$SHOWMAIL" \ 12.18 @@ -113,7 +117,9 @@ 12.19 12.20 # Increase the quota. 12.21 12.22 -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT2H' $SET_QUOTA_LIMIT_ARGS 12.23 +cat <<EOF | "$SET_QUOTA_LIMIT" "$QUOTA" $SET_QUOTA_LIMIT_ARGS 12.24 +* PT2H 12.25 +EOF 12.26 12.27 # Attempt to schedule the event again. 12.28 12.29 @@ -315,7 +321,9 @@ 12.30 12.31 # Increase the quota. 12.32 12.33 -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT3H' $SET_QUOTA_LIMIT_ARGS 12.34 +cat <<EOF | "$SET_QUOTA_LIMIT" "$QUOTA" $SET_QUOTA_LIMIT_ARGS 12.35 +* PT3H 12.36 +EOF 12.37 12.38 # Attempt to schedule an event involving both resources. 12.39
13.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 13.2 +++ b/tests/test_resource_invitation_constraints_quota_delegation.sh Thu May 12 23:15:18 2016 +0200 13.3 @@ -0,0 +1,264 @@ 13.4 +#!/bin/sh 13.5 + 13.6 +. "`dirname \"$0\"`/common.sh" 13.7 + 13.8 +USER1="mailto:resource-car-porsche911@example.com" 13.9 +USER2="mailto:resource-car-fiat500@example.com" 13.10 +SENDER="mailto:paul.boddie@example.com" 13.11 +USER1ADDRESS="resource-car-porsche911@example.com" 13.12 +USER2ADDRESS="resource-car-fiat500@example.com" 13.13 +SENDERADDRESS="paul.boddie@example.com" 13.14 +QUOTA=cars 13.15 +OTHER_QUOTA=rooms 13.16 + 13.17 +mkdir -p "$PREFS/$USER1" 13.18 +echo 'Europe/Oslo' > "$PREFS/$USER1/TZID" 13.19 +echo 'share' > "$PREFS/$USER1/freebusy_sharing" 13.20 +cat > "$PREFS/$USER1/scheduling_function" <<EOF 13.21 +schedule_for_delegate $QUOTA 13.22 +schedule_in_freebusy 13.23 +check_quota $QUOTA 13.24 +EOF 13.25 + 13.26 +mkdir -p "$PREFS/$USER2" 13.27 +echo 'Europe/Oslo' > "$PREFS/$USER2/TZID" 13.28 +echo 'share' > "$PREFS/$USER2/freebusy_sharing" 13.29 +cat > "$PREFS/$USER2/scheduling_function" <<EOF 13.30 +schedule_for_delegate $QUOTA 13.31 +schedule_in_freebusy 13.32 +check_quota $QUOTA 13.33 +EOF 13.34 + 13.35 +cat <<EOF | "$SET_QUOTA_LIMIT" "$QUOTA" $SET_QUOTA_LIMIT_ARGS 13.36 +* PT2H 13.37 +EOF 13.38 +cat <<EOF | "$SET_QUOTA_LIMIT" "$OTHER_QUOTA" $SET_QUOTA_LIMIT_ARGS 13.39 +* PT2H 13.40 +EOF 13.41 + 13.42 +# Allow cars to delegate to each other. 13.43 + 13.44 +cat <<EOF | "$SET_DELEGATES" "$QUOTA" $SET_DELEGATES_ARGS 13.45 +mailto:resource-car-porsche911@example.com 13.46 +mailto:resource-car-fiat500@example.com 13.47 +EOF 13.48 + 13.49 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car.txt" 2>> $ERROR \ 13.50 +| "$SHOWMAIL" \ 13.51 +> out0.tmp 13.52 + 13.53 + grep -q 'METHOD:REPLY' out0.tmp \ 13.54 +&& ! grep -q '^FREEBUSY' out0.tmp \ 13.55 +&& echo "Success" \ 13.56 +|| echo "Failed" 13.57 + 13.58 +# Attempt to schedule an event. 13.59 + 13.60 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR 13.61 + 13.62 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \ 13.63 +| tee out0s.tmp \ 13.64 +| grep -q "^20141126T150000Z${TAB}20141126T160000Z" \ 13.65 +&& echo "Success" \ 13.66 +|| echo "Failed" 13.67 + 13.68 +# Present the request to the resource. 13.69 + 13.70 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car.txt" 2>> $ERROR \ 13.71 +| tee out1r.tmp \ 13.72 +| "$SHOWMAIL" \ 13.73 +> out1.tmp 13.74 + 13.75 + grep -q 'METHOD:REPLY' out1.tmp \ 13.76 +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out1.tmp \ 13.77 +&& echo "Success" \ 13.78 +|| echo "Failed" 13.79 + 13.80 + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ 13.81 +| tee out1f.tmp \ 13.82 +| grep -q "^20141126T150000Z${TAB}20141126T160000Z" \ 13.83 +&& echo "Success" \ 13.84 +|| echo "Failed" 13.85 + 13.86 +# Check the quota (event is confirmed). 13.87 + 13.88 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \ 13.89 +| tee out1e.tmp \ 13.90 +| grep -q "event21@example.com" \ 13.91 +&& echo "Success" \ 13.92 +|| echo "Failed" 13.93 + 13.94 +# Attempt to schedule another event. 13.95 + 13.96 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-delegating.txt" 2>> $ERROR 13.97 + 13.98 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \ 13.99 +| tee out1s.tmp \ 13.100 +| grep -q "^20141126T153000Z${TAB}20141126T163000Z" \ 13.101 +&& echo "Success" \ 13.102 +|| echo "Failed" 13.103 + 13.104 +# Present the request to the resource. 13.105 + 13.106 + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-car-delegating.txt" 2>> $ERROR \ 13.107 +> out2r.tmp 13.108 + 13.109 + "$SHOWMAIL" < out2r.tmp \ 13.110 +> out2p0.tmp 13.111 + 13.112 + "$SHOWMAIL" 1 < out2r.tmp \ 13.113 +> out2p1.tmp 13.114 + 13.115 +if grep -q "To: $SENDERADDRESS" out2p0.tmp ; then 13.116 + ORGFN=out2p0.tmp ; DELFN=out2p1.tmp 13.117 +else 13.118 + ORGFN=out2p1.tmp ; DELFN=out2p0.tmp 13.119 +fi 13.120 + 13.121 +# One of the responses will be a request sent to the delegate. 13.122 + 13.123 + grep -q "To: $USER2ADDRESS" "$DELFN" \ 13.124 +&& grep -q 'METHOD:REQUEST' "$DELFN" \ 13.125 +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$DELFN" \ 13.126 +&& grep -q 'ATTENDEE.*:'"$USER2" "$DELFN" \ 13.127 +&& echo "Success" \ 13.128 +|| echo "Failed" 13.129 + 13.130 +# The other will be a reply to the organiser. 13.131 + 13.132 + grep -q "To: $SENDERADDRESS" "$ORGFN" \ 13.133 +&& grep -q 'METHOD:REPLY' "$ORGFN" \ 13.134 +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$ORGFN" \ 13.135 +&& grep -q 'ATTENDEE.*:'"$USER2" "$ORGFN" \ 13.136 +&& echo "Success" \ 13.137 +|| echo "Failed" 13.138 + 13.139 +# Neither the delegator or the delegate will have changed their schedules. 13.140 + 13.141 + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ 13.142 +> out2f1.tmp 13.143 + 13.144 + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out2f1.tmp" \ 13.145 +&& echo "Success" \ 13.146 +|| echo "Failed" 13.147 + 13.148 + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \ 13.149 +> out2f2.tmp 13.150 + 13.151 + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out2f2.tmp" \ 13.152 +&& echo "Success" \ 13.153 +|| echo "Failed" 13.154 + 13.155 +# Check the quota (event is not confirmed). 13.156 + 13.157 + "$LIST_SCRIPT" $LIST_ARGS "$QUOTA" "entries" "$SENDER" \ 13.158 +> out2e.tmp 13.159 + 13.160 + grep -q "event21@example.com" "out2e.tmp" \ 13.161 +&& ! grep -q "event27@example.com" "out2e.tmp" \ 13.162 +&& echo "Success" \ 13.163 +|| echo "Failed" 13.164 + 13.165 +# Present the reply to the organiser. 13.166 + 13.167 + "$PERSON_SCRIPT" $ARGS < "$ORGFN" 2>> "$ERROR" \ 13.168 +| tee out3r.tmp \ 13.169 +| "$SHOWMAIL" \ 13.170 +> out3.tmp 13.171 + 13.172 +# Check the free/busy status of the attendees at the organiser. 13.173 +# Currently, neither are attending. 13.174 + 13.175 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER1" \ 13.176 +> out3s0.tmp \ 13.177 + 13.178 + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" out3s0.tmp \ 13.179 +&& echo "Success" \ 13.180 +|| echo "Failed" 13.181 + 13.182 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER2" \ 13.183 +> out3s1.tmp \ 13.184 + 13.185 + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" out3s1.tmp \ 13.186 +&& echo "Success" \ 13.187 +|| echo "Failed" 13.188 + 13.189 +# Present the request to the delegate. 13.190 + 13.191 + "$RESOURCE_SCRIPT" $ARGS < "$DELFN" 2>> "$ERROR" \ 13.192 +> out4r.tmp 13.193 + 13.194 + "$SHOWMAIL" < out4r.tmp \ 13.195 +> out4p0.tmp 13.196 + 13.197 + "$SHOWMAIL" 1 < out4r.tmp \ 13.198 +> out4p1.tmp 13.199 + 13.200 +if grep -q "To: $SENDERADDRESS" out4p0.tmp ; then 13.201 + ORGFN=out4p0.tmp ; DELFN=out4p1.tmp 13.202 +else 13.203 + ORGFN=out4p1.tmp ; DELFN=out4p0.tmp 13.204 +fi 13.205 + 13.206 +# One of the responses will be a reply sent to the organiser. 13.207 + 13.208 + grep -q "To: $SENDERADDRESS" "$ORGFN" \ 13.209 +&& grep -q 'METHOD:REPLY' "$ORGFN" \ 13.210 +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$ORGFN" \ 13.211 +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED.*:'"$USER2" "$ORGFN" \ 13.212 +&& echo "Success" \ 13.213 +|| echo "Failed" 13.214 + 13.215 +# The other will be a reply to the delegator. 13.216 + 13.217 + grep -q "To: $USER1ADDRESS" "$DELFN" \ 13.218 +&& grep -q 'METHOD:REPLY' "$DELFN" \ 13.219 +&& grep -q 'ATTENDEE.*;PARTSTAT=DELEGATED.*:'"$USER1" "$DELFN" \ 13.220 +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED.*:'"$USER2" "$DELFN" \ 13.221 +&& echo "Success" \ 13.222 +|| echo "Failed" 13.223 + 13.224 +# The delegate should now have a changed schedule. 13.225 + 13.226 + "$LIST_SCRIPT" $LIST_ARGS "$USER1" "freebusy" \ 13.227 +> out4f1.tmp 13.228 + 13.229 + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out4f1.tmp" \ 13.230 +&& echo "Success" \ 13.231 +|| echo "Failed" 13.232 + 13.233 + "$LIST_SCRIPT" $LIST_ARGS "$USER2" "freebusy" \ 13.234 +> out4f2.tmp 13.235 + 13.236 + grep -q "^20141126T153000Z${TAB}20141126T163000Z" "out4f2.tmp" \ 13.237 +&& echo "Success" \ 13.238 +|| echo "Failed" 13.239 + 13.240 +# Present the reply to the organiser. 13.241 + 13.242 + "$PERSON_SCRIPT" $ARGS < "$ORGFN" 2>> "$ERROR" \ 13.243 +| tee out5r.tmp \ 13.244 +| "$SHOWMAIL" \ 13.245 +> out5.tmp 13.246 + 13.247 +# Check the free/busy status of the attendees at the organiser. 13.248 +# Now, the delegate is attending. 13.249 + 13.250 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER1" \ 13.251 +> out5s0.tmp \ 13.252 + 13.253 + ! grep -q "^20141126T153000Z${TAB}20141126T163000Z" out5s0.tmp \ 13.254 +&& echo "Success" \ 13.255 +|| echo "Failed" 13.256 + 13.257 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER2" \ 13.258 +> out5s1.tmp \ 13.259 + 13.260 + grep -q "^20141126T153000Z${TAB}20141126T163000Z" out5s1.tmp \ 13.261 +&& echo "Success" \ 13.262 +|| echo "Failed" 13.263 + 13.264 +# Present the reply to the delegator. 13.265 + 13.266 + "$RESOURCE_SCRIPT" $ARGS < "$DELFN" 2>> "$ERROR" \ 13.267 +> out6r.tmp
14.1 --- a/tests/test_resource_invitation_constraints_quota_recurring.sh Thu May 12 23:05:48 2016 +0200 14.2 +++ b/tests/test_resource_invitation_constraints_quota_recurring.sh Thu May 12 23:15:18 2016 +0200 14.3 @@ -16,7 +16,9 @@ 14.4 14.5 # Employ a user-specific quota (no argument with the functions above). 14.6 14.7 -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT10H' $SET_QUOTA_LIMIT_ARGS 14.8 +cat <<EOF | "$SET_QUOTA_LIMIT" "$QUOTA" $SET_QUOTA_LIMIT_ARGS 14.9 +* PT10H 14.10 +EOF 14.11 14.12 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car-all.txt" 2>> $ERROR \ 14.13 | "$SHOWMAIL" \
15.1 --- a/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Thu May 12 23:05:48 2016 +0200 15.2 +++ b/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Thu May 12 23:15:18 2016 +0200 15.3 @@ -26,8 +26,10 @@ 15.4 check_quota $QUOTA 15.5 EOF 15.6 15.7 -"$SET_QUOTA_LIMIT" "$QUOTA" 'mailto:vincent.vole@example.com' 'PT10H' $SET_QUOTA_LIMIT_ARGS 15.8 -"$SET_QUOTA_LIMIT" "$QUOTA" '*' 'PT5H' $SET_QUOTA_LIMIT_ARGS 15.9 +cat <<EOF | "$SET_QUOTA_LIMIT" "$QUOTA" $SET_QUOTA_LIMIT_ARGS 15.10 +mailto:vincent.vole@example.com PT10H 15.11 +* PT5H 15.12 +EOF 15.13 15.14 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-car-all.txt" 2>> $ERROR \ 15.15 | "$SHOWMAIL" \
16.1 --- a/tools/copy_store.py Thu May 12 23:05:48 2016 +0200 16.2 +++ b/tools/copy_store.py Thu May 12 23:15:18 2016 +0200 16.3 @@ -104,13 +104,15 @@ 16.4 16.5 # Copy quota limits. 16.6 16.7 - for user_group, limit in from_journal.get_limits(quota).items(): 16.8 - to_journal.set_limit(quota, user_group, limit) 16.9 + to_journal.set_limits(quota, from_journal.get_limits(quota)) 16.10 16.11 # Copy group mappings. 16.12 16.13 - for store_user, user_group in from_journal.get_groups(quota).items(): 16.14 - to_journal.set_group(quota, store_user, user_group) 16.15 + to_journal.set_groups(quota, from_journal.get_groups(quota)) 16.16 + 16.17 + # Copy delegates. 16.18 + 16.19 + to_journal.set_delegates(quota, from_journal.get_delegates(quota)) 16.20 16.21 # Copy journal details. 16.22
17.1 --- a/tools/install.sh Thu May 12 23:05:48 2016 +0200 17.2 +++ b/tools/install.sh Thu May 12 23:15:18 2016 +0200 17.3 @@ -103,7 +103,7 @@ 17.4 17.5 # Tools 17.6 17.7 -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" 17.8 +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" 17.9 17.10 if [ ! -e "$INSTALL_DIR/tools" ]; then 17.11 mkdir -p "$INSTALL_DIR/tools"
18.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 18.2 +++ b/tools/set_delegates.py Thu May 12 23:15:18 2016 +0200 18.3 @@ -0,0 +1,89 @@ 18.4 +#!/usr/bin/env python 18.5 + 18.6 +""" 18.7 +Set delegates for a particular quota. 18.8 + 18.9 +Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk> 18.10 + 18.11 +This program is free software; you can redistribute it and/or modify it under 18.12 +the terms of the GNU General Public License as published by the Free Software 18.13 +Foundation; either version 3 of the License, or (at your option) any later 18.14 +version. 18.15 + 18.16 +This program is distributed in the hope that it will be useful, but WITHOUT 18.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 18.19 +details. 18.20 + 18.21 +You should have received a copy of the GNU General Public License along with 18.22 +this program. If not, see <http://www.gnu.org/licenses/>. 18.23 +""" 18.24 + 18.25 +from codecs import getreader 18.26 +from os.path import abspath, split 18.27 +import sys 18.28 + 18.29 +# Find the modules. 18.30 + 18.31 +try: 18.32 + import imiptools 18.33 +except ImportError: 18.34 + parent = abspath(split(split(__file__)[0])[0]) 18.35 + if split(parent)[1] == "imip-agent": 18.36 + sys.path.append(parent) 18.37 + 18.38 +from imiptools import config 18.39 +from imiptools.stores import get_journal 18.40 +from imiptools.text import get_table_from_stream 18.41 + 18.42 +# Main program. 18.43 + 18.44 +if __name__ == "__main__": 18.45 + 18.46 + # Interpret the command line arguments. 18.47 + 18.48 + args = [] 18.49 + store_type = [] 18.50 + journal_dir = [] 18.51 + 18.52 + # Collect quota details first, switching to other arguments when encountering 18.53 + # switches. 18.54 + 18.55 + l = args 18.56 + 18.57 + for arg in sys.argv[1:]: 18.58 + if arg == "-T": 18.59 + l = store_type 18.60 + elif arg == "-j": 18.61 + l = journal_dir 18.62 + else: 18.63 + l.append(arg) 18.64 + 18.65 + try: 18.66 + quota, = args 18.67 + except ValueError: 18.68 + print >>sys.stderr, """\ 18.69 +Usage: %s <quota> <delegate>... [ <options> ] 18.70 + 18.71 +General options: 18.72 + 18.73 +-j Indicates the journal directory location 18.74 +-T Indicates the store type (the configured value if omitted) 18.75 +""" % split(sys.argv[0])[1] 18.76 + sys.exit(1) 18.77 + 18.78 + # Override defaults if indicated. 18.79 + 18.80 + getvalue = lambda value, default=None: value and value[0] or default 18.81 + 18.82 + store_type = getvalue(store_type, config.STORE_TYPE) 18.83 + journal_dir = getvalue(journal_dir) 18.84 + 18.85 + # Obtain store-related objects. 18.86 + 18.87 + journal = get_journal(store_type, journal_dir) 18.88 + f = getreader("utf-8")(sys.stdin) 18.89 + delegates = get_table_from_stream(f, tab_separated=False) 18.90 + journal.set_delegates(quota, [value for (value,) in delegates]) 18.91 + 18.92 +# vim: tabstop=4 expandtab shiftwidth=4
19.1 --- a/tools/set_quota_limit.py Thu May 12 23:05:48 2016 +0200 19.2 +++ b/tools/set_quota_limit.py Thu May 12 23:15:18 2016 +0200 19.3 @@ -1,7 +1,7 @@ 19.4 #!/usr/bin/env python 19.5 19.6 """ 19.7 -Set a quota limit for a user group. 19.8 +Set quota limits for a collection of user groups. 19.9 19.10 Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk> 19.11 19.12 @@ -19,6 +19,7 @@ 19.13 this program. If not, see <http://www.gnu.org/licenses/>. 19.14 """ 19.15 19.16 +from codecs import getreader 19.17 from os.path import abspath, split 19.18 import sys 19.19 19.20 @@ -33,6 +34,7 @@ 19.21 19.22 from imiptools import config 19.23 from imiptools.stores import get_journal 19.24 +from imiptools.text import get_table_from_stream 19.25 19.26 # Main program. 19.27 19.28 @@ -58,10 +60,21 @@ 19.29 l.append(arg) 19.30 19.31 try: 19.32 - quota, group, limit = args 19.33 + quota, = args 19.34 except ValueError: 19.35 print >>sys.stderr, """\ 19.36 -Usage: %s <quota> <group> <limit> [ <options> ] 19.37 +Usage: %s <quota> [ <options> ] 19.38 + 19.39 +Read from standard input a list of group-to-limit mappings of the following 19.40 +form: 19.41 + 19.42 +<user or group> <limit> 19.43 + 19.44 +For example: 19.45 + 19.46 +* PT1H 19.47 + 19.48 +The values may be separated using any whitespace characters. 19.49 19.50 General options: 19.51 19.52 @@ -80,6 +93,8 @@ 19.53 # Obtain store-related objects. 19.54 19.55 journal = get_journal(store_type, journal_dir) 19.56 - journal.set_limit(quota, group, limit) 19.57 + f = getreader("utf-8")(sys.stdin) 19.58 + limits = dict(get_table_from_stream(f, tab_separated=False)) 19.59 + journal.set_limits(quota, limits) 19.60 19.61 # vim: tabstop=4 expandtab shiftwidth=4