# HG changeset patch # User Paul Boddie # Date 1461350031 -7200 # Node ID 967f8a54ef75279d62bba7fad861935387516e01 # Parent 5f8c76782d8344c84297bbd2e48abd2e58b2d307 Allow scheduling methods to return result descriptions for inclusion in message parts. Improved the showmail tool to handle the multipart messages now produced by resource-scheduling operations. Fixed various aspects of the tests. diff -r 5f8c76782d83 -r 967f8a54ef75 imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Fri Apr 22 20:30:51 2016 +0200 +++ b/imiptools/handlers/resource.py Fri Apr 22 20:33:51 2016 +0200 @@ -19,6 +19,7 @@ this program. If not, see . """ +from email.mime.text import MIMEText from imiptools.data import get_address, to_part, uri_dict from imiptools.handlers import Handler from imiptools.handlers.common import CommonFreebusy, CommonEvent @@ -92,7 +93,7 @@ # Attempt to schedule the event. - scheduled = self.schedule() + scheduled, description = self.schedule() try: # Update the participation of the resource in the object. @@ -140,13 +141,19 @@ finally: self.finish_scheduling() + recipients = map(get_address, self.obj.get_values("ORGANIZER")) + + # Add any description of the scheduling decision. + + self.add_result(None, recipients, MIMEText(description)) + # Make a version of the object with just this attendee, update the # DTSTAMP in the response, and return the object for sending. self.update_sender(attendee_attr) self.obj["ATTENDEE"] = [(self.user, attendee_attr)] self.update_dtstamp() - self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()])) + self.add_result(method, recipients, to_part(method, [self.obj.to_node()])) def _cancel_for_attendee(self): diff -r 5f8c76782d83 -r 967f8a54ef75 imiptools/handlers/scheduling/__init__.py --- a/imiptools/handlers/scheduling/__init__.py Fri Apr 22 20:30:51 2016 +0200 +++ b/imiptools/handlers/scheduling/__init__.py Fri Apr 22 20:33:51 2016 +0200 @@ -34,6 +34,9 @@ Apply the scheduling functions for the current object of the given 'handler'. This function starts a transaction that should be finalised using the 'finish_scheduling' function. + + Return a tuple containing the scheduling decision and any accompanying + description. """ # First, lock the resources to be used. @@ -47,24 +50,26 @@ # Then, invoke the scheduling functions. response = "ACCEPTED" + description = None for fn, args in schedulers: # NOTE: Should signal an error for incorrectly configured resources. if not fn: - return "DECLINED" + return "DECLINED", None # Keep evaluating scheduling functions, stopping only if one # declines or gives a null response. else: result = fn(handler, args) + result, description = result or ("DECLINED", None) # Return a negative result immediately. - if not result or result == "DECLINED": - return result + if result == "DECLINED": + return result, description # Modify the eventual response from acceptance if a countering # result is obtained. @@ -72,7 +77,7 @@ elif response == "ACCEPTED": response = result - return response + return response, description def confirm_scheduling(handler): diff -r 5f8c76782d83 -r 967f8a54ef75 imiptools/handlers/scheduling/access.py --- a/imiptools/handlers/scheduling/access.py Fri Apr 22 20:30:51 2016 +0200 +++ b/imiptools/handlers/scheduling/access.py Fri Apr 22 20:33:51 2016 +0200 @@ -108,7 +108,7 @@ if match: response = result - return response + return standard_responses(handler, response) def same_domain_only(handler, args): @@ -123,7 +123,24 @@ organiser_domain = organiser.rsplit("@", 1)[-1] user_domain = user.rsplit("@", 1)[-1] - return organiser_domain == user_domain and "ACCEPTED" or "DECLINED" + response = organiser_domain == user_domain and "ACCEPTED" or "DECLINED" + return standard_responses(handler, response) + +def standard_responses(handler, response): + + """ + Using 'handler' to translate descriptions, return a tuple containing the + 'response' and a suitable description. + """ + + _ = handler.get_translator() + + if response == "ACCEPTED": + return response, _("The recipient has scheduled the requested period.") + elif response == "DECLINED": + return response, _("The recipient has refused to schedule the requested period.") + else: + return response, None # Registry of scheduling functions. diff -r 5f8c76782d83 -r 967f8a54ef75 imiptools/handlers/scheduling/freebusy.py --- a/imiptools/handlers/scheduling/freebusy.py Fri Apr 22 20:30:51 2016 +0200 +++ b/imiptools/handlers/scheduling/freebusy.py Fri Apr 22 20:33:51 2016 +0200 @@ -34,6 +34,8 @@ free/busy records will be used. """ + _ = handler.get_translator() + # If newer than any old version, discard old details from the # free/busy record and check for suitability. @@ -48,7 +50,7 @@ scheduled = handler.can_schedule(freebusy, periods) scheduled = scheduled and handler.can_schedule(offers, periods) - return scheduled and "ACCEPTED" or "DECLINED" + return standard_responses(handler, scheduled and "ACCEPTED" or "DECLINED") def schedule_corrected_in_freebusy(handler, args): @@ -58,6 +60,8 @@ returning an indication of the kind of response to be returned. """ + _ = handler.get_translator() + obj = handler.obj.copy() # Check any constraints on the request. @@ -73,17 +77,19 @@ # With a valid request, determine whether the event can be scheduled. scheduled = schedule_in_freebusy(handler, args) + response, description = scheduled or ("DECLINED", None) # Restore the original object if it was corrected but could not be # scheduled. - if scheduled == "DECLINED" and corrected: + if response == "DECLINED" and corrected: handler.set_object(obj) # Where the corrected object can be scheduled, issue a counter # request. - return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED" + response = response == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED" + return standard_responses(handler, response) def schedule_next_available_in_freebusy(handler, args): @@ -95,9 +101,12 @@ of response to be returned. """ - scheduled = schedule_corrected_in_freebusy(handler, args) + _ = handler.get_translator() - if scheduled in ("ACCEPTED", "COUNTER"): + scheduled = schedule_corrected_in_freebusy(handler, args) + response, description = scheduled or ("DECLINED", None) + + if response in ("ACCEPTED", "COUNTER"): return scheduled # There should already be free/busy information for the user. @@ -191,7 +200,7 @@ # Where no period can be found, decline the invitation. else: - return "DECLINED" + return "DECLINED", _("The recipient is unavailable in the requested period.") # Use the found period to set the start of the next window to search. @@ -205,11 +214,29 @@ # Check one last time, reverting the change if not scheduled. scheduled = schedule_in_freebusy(handler, args, busy) + response, description = scheduled or ("DECLINED", None) - if scheduled == "DECLINED": + if response == "DECLINED": handler.set_object(obj) - return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED" + response = response == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED" + return standard_responses(handler, response) + +def standard_responses(handler, response): + + """ + Using 'handler' to translate descriptions, return a tuple containing the + 'response' and a suitable description. + """ + + _ = handler.get_translator() + + if response == "ACCEPTED": + return response, _("The recipient has scheduled the requested period.") + elif response == "COUNTER": + return response, _("The recipient has suggested a different period.") + else: + return response, _("The recipient is unavailable in the requested period.") # Registry of scheduling functions. diff -r 5f8c76782d83 -r 967f8a54ef75 imiptools/handlers/scheduling/quota.py --- a/imiptools/handlers/scheduling/quota.py Fri Apr 22 20:30:51 2016 +0200 +++ b/imiptools/handlers/scheduling/quota.py Fri Apr 22 20:33:51 2016 +0200 @@ -33,6 +33,8 @@ quota. """ + _ = handler.get_translator() + quota, group = _get_quota_and_group(handler, args) # Obtain the journal entries and check the balance. @@ -46,21 +48,21 @@ limit = limits.get(group) or limits.get("*") if not limit: - return "DECLINED" + return "DECLINED", _("You have no quota allocation for the recipient.") # Decline events whose durations exceed the balance. total = _get_duration(handler) if total == Endless(): - return "DECLINED" + return "DECLINED", _("The event period exceeds your quota allocation for the recipient.") balance = get_duration(limit) - _get_usage(entries) if total > balance: - return "DECLINED" + return "DECLINED", _("The event period exceeds your quota allocation for the recipient.") else: - return "ACCEPTED" + return "ACCEPTED", _("The recipient has scheduled the requested period.") def add_to_quota(handler, args): @@ -188,6 +190,8 @@ managed by the quota. """ + _ = handler.get_translator() + quota, organiser = _get_quota_and_identity(handler, args) # If newer than any old version, discard old details from the @@ -197,7 +201,10 @@ freebusy = handler.get_journal().get_freebusy(quota, organiser) scheduled = handler.can_schedule(freebusy, periods) - return scheduled and "ACCEPTED" or "DECLINED" + if scheduled: + return "ACCEPTED", _("The recipient has scheduled the requested period.") + else: + return "DECLINED", _("The requested period cannot be scheduled.") def add_to_quota_freebusy(handler, args): diff -r 5f8c76782d83 -r 967f8a54ef75 tests/test_resource_invitation_constraints.sh --- a/tests/test_resource_invitation_constraints.sh Fri Apr 22 20:30:51 2016 +0200 +++ b/tests/test_resource_invitation_constraints.sh Fri Apr 22 20:33:51 2016 +0200 @@ -104,6 +104,7 @@ # Present the request to the resource. "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-rival.txt" 2>> $ERROR \ +| tee out3r.tmp \ | "$SHOWMAIL" \ > out3.tmp @@ -114,7 +115,7 @@ # Present the response to the organiser. - "$PERSON_SCRIPT" $ARGS < out3.tmp 2>> $ERROR \ + "$PERSON_SCRIPT" $ARGS < out3r.tmp 2>> $ERROR \ | tee out4r.tmp \ | "$SHOWMAIL" \ > out4.tmp diff -r 5f8c76782d83 -r 967f8a54ef75 tests/test_resource_invitation_constraints_alternative.sh --- a/tests/test_resource_invitation_constraints_alternative.sh Fri Apr 22 20:30:51 2016 +0200 +++ b/tests/test_resource_invitation_constraints_alternative.sh Fri Apr 22 20:33:51 2016 +0200 @@ -35,6 +35,7 @@ # Present the request to the resource. "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-bad.txt" 2>> $ERROR \ +| tee out1r.tmp \ | "$SHOWMAIL" \ > out1.tmp @@ -58,7 +59,7 @@ # Present the response to the organiser. - "$PERSON_SCRIPT" $ARGS < out1.tmp 2>> $ERROR \ + "$PERSON_SCRIPT" $ARGS < out1r.tmp 2>> $ERROR \ | tee out2r.tmp \ | "$SHOWMAIL" \ > out2.tmp @@ -99,6 +100,7 @@ "$OUTGOING_SCRIPT" $ARGS < out3.tmp 2>> $ERROR "$RESOURCE_SCRIPT" $ARGS < out3.tmp 2>> $ERROR \ +| tee out4r.tmp \ | "$SHOWMAIL" \ > out4.tmp @@ -131,6 +133,7 @@ # Present the request to the resource. "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-rival.txt" 2>> $ERROR \ +| tee out5r.tmp \ | "$SHOWMAIL" \ > out5.tmp @@ -154,16 +157,17 @@ # Present the response to the organiser. - "$PERSON_SCRIPT" $ARGS < out5.tmp 2>> $ERROR \ + "$PERSON_SCRIPT" $ARGS < out5r.tmp 2>> $ERROR \ +| tee out6r.tmp \ | "$SHOWMAIL" \ > out6.tmp "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "freebusy" \ -> out6r.tmp +> out6R.tmp - ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out6r.tmp" \ -&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out6r.tmp" \ -&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out6r.tmp" \ + ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out6R.tmp" \ +&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out6R.tmp" \ +&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out6R.tmp" \ && echo "Success" \ || echo "Failed" diff -r 5f8c76782d83 -r 967f8a54ef75 tests/test_resource_invitation_constraints_quota.sh --- a/tests/test_resource_invitation_constraints_quota.sh Fri Apr 22 20:30:51 2016 +0200 +++ b/tests/test_resource_invitation_constraints_quota.sh Fri Apr 22 20:33:51 2016 +0200 @@ -335,11 +335,7 @@ # Since the email module used by showmail.py cannot stop after reading a single # message, the second message is obtained. - grep -n '^From ' out8r.tmp \ -| tail -n 1 \ -| cut -d ':' -f 1 \ -| xargs -I{} tail -n +'{}' out8r.tmp \ -| "$SHOWMAIL" \ + "$SHOWMAIL" 1 < out8r.tmp \ >> out8.tmp grep -q 'METHOD:REPLY' out8.tmp \ diff -r 5f8c76782d83 -r 967f8a54ef75 tests/test_resource_invitation_constraints_quota_recurring_limits.sh --- a/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Fri Apr 22 20:30:51 2016 +0200 +++ b/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Fri Apr 22 20:33:51 2016 +0200 @@ -108,11 +108,7 @@ # Since the email module used by showmail.py cannot stop after reading a single # message, the second message is obtained. - grep -n '^From ' out2r.tmp \ -| tail -n 1 \ -| cut -d ':' -f 1 \ -| xargs -I{} tail -n +'{}' out2r.tmp \ -| "$SHOWMAIL" \ + "$SHOWMAIL" 1 < out2r.tmp \ >> out2.tmp grep -q 'METHOD:REPLY' out2.tmp \ @@ -218,11 +214,7 @@ # Since the email module used by showmail.py cannot stop after reading a single # message, the second message is obtained. - grep -n '^From ' out4r.tmp \ -| tail -n 1 \ -| cut -d ':' -f 1 \ -| xargs -I{} tail -n +'{}' out4r.tmp \ -| "$SHOWMAIL" \ + "$SHOWMAIL" 1 < out4r.tmp \ >> out4.tmp grep -q 'METHOD:REPLY' out4.tmp \ diff -r 5f8c76782d83 -r 967f8a54ef75 tools/showmail.py --- a/tools/showmail.py Fri Apr 22 20:30:51 2016 +0200 +++ b/tools/showmail.py Fri Apr 22 20:33:51 2016 +0200 @@ -1,32 +1,69 @@ #!/usr/bin/env python from email import message_from_string +from email.generator import Generator import sys -def until_from(f): - l = [] - s = f.readline() - while s: - l.append(s) +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +def until_from(f, skip=0): + number = 0 + while number <= skip: + l = [] s = f.readline() - if s.startswith("From "): + while s: + l.append(s) + s = f.readline() + if s.startswith("From "): + number += 1 + break + else: + number += 1 break - return "".join(l) + if number > skip: + return "".join(l) + else: + return "" + +def as_string(message): + + """ + Return the string representation of 'message', attempting to preserve the + precise original formatting. + """ + + out = StringIO() + generator = Generator(out, False, 0) # disable reformatting measures + generator.flatten(message) + return out.getvalue() def decode(part): - for key, value in part.items(): - if key != "Content-Transfer-Encoding": - print "%s: %s" % (key, value) - print - decoded = part.get_payload(decode=True) - if decoded: - print decoded - print + + """ + Change the transfer encoding on 'part' and its subparts so that a plain text + representation may be displayed. + """ + + payload = part.get_payload(decode=True) + if payload: + encoding = part.get("Content-Transfer-Encoding") + if encoding: + del part["Content-Transfer-Encoding"] + part["Content-Transfer-Encoding"] = "8bit" + part.set_payload(payload) else: - for part in part.get_payload(): - decode(part) + for p in part.get_payload(): + decode(p) + +# Main program. -message = message_from_string(until_from(sys.stdin)) -decode(message) +if __name__ == "__main__": + skip = int((sys.argv[1:] or [0])[0]) + message = message_from_string(until_from(sys.stdin, skip)) + decode(message) + print as_string(message) # vim: tabstop=4 expandtab shiftwidth=4