1.1 --- a/imiptools/handlers/resource.py Fri Apr 22 20:30:51 2016 +0200
1.2 +++ b/imiptools/handlers/resource.py Fri Apr 22 20:33:51 2016 +0200
1.3 @@ -19,6 +19,7 @@
1.4 this program. If not, see <http://www.gnu.org/licenses/>.
1.5 """
1.6
1.7 +from email.mime.text import MIMEText
1.8 from imiptools.data import get_address, to_part, uri_dict
1.9 from imiptools.handlers import Handler
1.10 from imiptools.handlers.common import CommonFreebusy, CommonEvent
1.11 @@ -92,7 +93,7 @@
1.12
1.13 # Attempt to schedule the event.
1.14
1.15 - scheduled = self.schedule()
1.16 + scheduled, description = self.schedule()
1.17
1.18 try:
1.19 # Update the participation of the resource in the object.
1.20 @@ -140,13 +141,19 @@
1.21 finally:
1.22 self.finish_scheduling()
1.23
1.24 + recipients = map(get_address, self.obj.get_values("ORGANIZER"))
1.25 +
1.26 + # Add any description of the scheduling decision.
1.27 +
1.28 + self.add_result(None, recipients, MIMEText(description))
1.29 +
1.30 # Make a version of the object with just this attendee, update the
1.31 # DTSTAMP in the response, and return the object for sending.
1.32
1.33 self.update_sender(attendee_attr)
1.34 self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
1.35 self.update_dtstamp()
1.36 - self.add_result(method, map(get_address, self.obj.get_values("ORGANIZER")), to_part(method, [self.obj.to_node()]))
1.37 + self.add_result(method, recipients, to_part(method, [self.obj.to_node()]))
1.38
1.39 def _cancel_for_attendee(self):
1.40
2.1 --- a/imiptools/handlers/scheduling/__init__.py Fri Apr 22 20:30:51 2016 +0200
2.2 +++ b/imiptools/handlers/scheduling/__init__.py Fri Apr 22 20:33:51 2016 +0200
2.3 @@ -34,6 +34,9 @@
2.4 Apply the scheduling functions for the current object of the given
2.5 'handler'. This function starts a transaction that should be finalised using
2.6 the 'finish_scheduling' function.
2.7 +
2.8 + Return a tuple containing the scheduling decision and any accompanying
2.9 + description.
2.10 """
2.11
2.12 # First, lock the resources to be used.
2.13 @@ -47,24 +50,26 @@
2.14 # Then, invoke the scheduling functions.
2.15
2.16 response = "ACCEPTED"
2.17 + description = None
2.18
2.19 for fn, args in schedulers:
2.20
2.21 # NOTE: Should signal an error for incorrectly configured resources.
2.22
2.23 if not fn:
2.24 - return "DECLINED"
2.25 + return "DECLINED", None
2.26
2.27 # Keep evaluating scheduling functions, stopping only if one
2.28 # declines or gives a null response.
2.29
2.30 else:
2.31 result = fn(handler, args)
2.32 + result, description = result or ("DECLINED", None)
2.33
2.34 # Return a negative result immediately.
2.35
2.36 - if not result or result == "DECLINED":
2.37 - return result
2.38 + if result == "DECLINED":
2.39 + return result, description
2.40
2.41 # Modify the eventual response from acceptance if a countering
2.42 # result is obtained.
2.43 @@ -72,7 +77,7 @@
2.44 elif response == "ACCEPTED":
2.45 response = result
2.46
2.47 - return response
2.48 + return response, description
2.49
2.50 def confirm_scheduling(handler):
2.51
3.1 --- a/imiptools/handlers/scheduling/access.py Fri Apr 22 20:30:51 2016 +0200
3.2 +++ b/imiptools/handlers/scheduling/access.py Fri Apr 22 20:33:51 2016 +0200
3.3 @@ -108,7 +108,7 @@
3.4 if match:
3.5 response = result
3.6
3.7 - return response
3.8 + return standard_responses(handler, response)
3.9
3.10 def same_domain_only(handler, args):
3.11
3.12 @@ -123,7 +123,24 @@
3.13 organiser_domain = organiser.rsplit("@", 1)[-1]
3.14 user_domain = user.rsplit("@", 1)[-1]
3.15
3.16 - return organiser_domain == user_domain and "ACCEPTED" or "DECLINED"
3.17 + response = organiser_domain == user_domain and "ACCEPTED" or "DECLINED"
3.18 + return standard_responses(handler, response)
3.19 +
3.20 +def standard_responses(handler, response):
3.21 +
3.22 + """
3.23 + Using 'handler' to translate descriptions, return a tuple containing the
3.24 + 'response' and a suitable description.
3.25 + """
3.26 +
3.27 + _ = handler.get_translator()
3.28 +
3.29 + if response == "ACCEPTED":
3.30 + return response, _("The recipient has scheduled the requested period.")
3.31 + elif response == "DECLINED":
3.32 + return response, _("The recipient has refused to schedule the requested period.")
3.33 + else:
3.34 + return response, None
3.35
3.36 # Registry of scheduling functions.
3.37
4.1 --- a/imiptools/handlers/scheduling/freebusy.py Fri Apr 22 20:30:51 2016 +0200
4.2 +++ b/imiptools/handlers/scheduling/freebusy.py Fri Apr 22 20:33:51 2016 +0200
4.3 @@ -34,6 +34,8 @@
4.4 free/busy records will be used.
4.5 """
4.6
4.7 + _ = handler.get_translator()
4.8 +
4.9 # If newer than any old version, discard old details from the
4.10 # free/busy record and check for suitability.
4.11
4.12 @@ -48,7 +50,7 @@
4.13 scheduled = handler.can_schedule(freebusy, periods)
4.14 scheduled = scheduled and handler.can_schedule(offers, periods)
4.15
4.16 - return scheduled and "ACCEPTED" or "DECLINED"
4.17 + return standard_responses(handler, scheduled and "ACCEPTED" or "DECLINED")
4.18
4.19 def schedule_corrected_in_freebusy(handler, args):
4.20
4.21 @@ -58,6 +60,8 @@
4.22 returning an indication of the kind of response to be returned.
4.23 """
4.24
4.25 + _ = handler.get_translator()
4.26 +
4.27 obj = handler.obj.copy()
4.28
4.29 # Check any constraints on the request.
4.30 @@ -73,17 +77,19 @@
4.31 # With a valid request, determine whether the event can be scheduled.
4.32
4.33 scheduled = schedule_in_freebusy(handler, args)
4.34 + response, description = scheduled or ("DECLINED", None)
4.35
4.36 # Restore the original object if it was corrected but could not be
4.37 # scheduled.
4.38
4.39 - if scheduled == "DECLINED" and corrected:
4.40 + if response == "DECLINED" and corrected:
4.41 handler.set_object(obj)
4.42
4.43 # Where the corrected object can be scheduled, issue a counter
4.44 # request.
4.45
4.46 - return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
4.47 + response = response == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED"
4.48 + return standard_responses(handler, response)
4.49
4.50 def schedule_next_available_in_freebusy(handler, args):
4.51
4.52 @@ -95,9 +101,12 @@
4.53 of response to be returned.
4.54 """
4.55
4.56 - scheduled = schedule_corrected_in_freebusy(handler, args)
4.57 + _ = handler.get_translator()
4.58
4.59 - if scheduled in ("ACCEPTED", "COUNTER"):
4.60 + scheduled = schedule_corrected_in_freebusy(handler, args)
4.61 + response, description = scheduled or ("DECLINED", None)
4.62 +
4.63 + if response in ("ACCEPTED", "COUNTER"):
4.64 return scheduled
4.65
4.66 # There should already be free/busy information for the user.
4.67 @@ -191,7 +200,7 @@
4.68 # Where no period can be found, decline the invitation.
4.69
4.70 else:
4.71 - return "DECLINED"
4.72 + return "DECLINED", _("The recipient is unavailable in the requested period.")
4.73
4.74 # Use the found period to set the start of the next window to search.
4.75
4.76 @@ -205,11 +214,29 @@
4.77 # Check one last time, reverting the change if not scheduled.
4.78
4.79 scheduled = schedule_in_freebusy(handler, args, busy)
4.80 + response, description = scheduled or ("DECLINED", None)
4.81
4.82 - if scheduled == "DECLINED":
4.83 + if response == "DECLINED":
4.84 handler.set_object(obj)
4.85
4.86 - return scheduled == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
4.87 + response = response == "ACCEPTED" and (changed and "COUNTER" or "ACCEPTED") or "DECLINED"
4.88 + return standard_responses(handler, response)
4.89 +
4.90 +def standard_responses(handler, response):
4.91 +
4.92 + """
4.93 + Using 'handler' to translate descriptions, return a tuple containing the
4.94 + 'response' and a suitable description.
4.95 + """
4.96 +
4.97 + _ = handler.get_translator()
4.98 +
4.99 + if response == "ACCEPTED":
4.100 + return response, _("The recipient has scheduled the requested period.")
4.101 + elif response == "COUNTER":
4.102 + return response, _("The recipient has suggested a different period.")
4.103 + else:
4.104 + return response, _("The recipient is unavailable in the requested period.")
4.105
4.106 # Registry of scheduling functions.
4.107
5.1 --- a/imiptools/handlers/scheduling/quota.py Fri Apr 22 20:30:51 2016 +0200
5.2 +++ b/imiptools/handlers/scheduling/quota.py Fri Apr 22 20:33:51 2016 +0200
5.3 @@ -33,6 +33,8 @@
5.4 quota.
5.5 """
5.6
5.7 + _ = handler.get_translator()
5.8 +
5.9 quota, group = _get_quota_and_group(handler, args)
5.10
5.11 # Obtain the journal entries and check the balance.
5.12 @@ -46,21 +48,21 @@
5.13
5.14 limit = limits.get(group) or limits.get("*")
5.15 if not limit:
5.16 - return "DECLINED"
5.17 + return "DECLINED", _("You have no quota allocation for the recipient.")
5.18
5.19 # Decline events whose durations exceed the balance.
5.20
5.21 total = _get_duration(handler)
5.22
5.23 if total == Endless():
5.24 - return "DECLINED"
5.25 + return "DECLINED", _("The event period exceeds your quota allocation for the recipient.")
5.26
5.27 balance = get_duration(limit) - _get_usage(entries)
5.28
5.29 if total > balance:
5.30 - return "DECLINED"
5.31 + return "DECLINED", _("The event period exceeds your quota allocation for the recipient.")
5.32 else:
5.33 - return "ACCEPTED"
5.34 + return "ACCEPTED", _("The recipient has scheduled the requested period.")
5.35
5.36 def add_to_quota(handler, args):
5.37
5.38 @@ -188,6 +190,8 @@
5.39 managed by the quota.
5.40 """
5.41
5.42 + _ = handler.get_translator()
5.43 +
5.44 quota, organiser = _get_quota_and_identity(handler, args)
5.45
5.46 # If newer than any old version, discard old details from the
5.47 @@ -197,7 +201,10 @@
5.48 freebusy = handler.get_journal().get_freebusy(quota, organiser)
5.49 scheduled = handler.can_schedule(freebusy, periods)
5.50
5.51 - return scheduled and "ACCEPTED" or "DECLINED"
5.52 + if scheduled:
5.53 + return "ACCEPTED", _("The recipient has scheduled the requested period.")
5.54 + else:
5.55 + return "DECLINED", _("The requested period cannot be scheduled.")
5.56
5.57 def add_to_quota_freebusy(handler, args):
5.58
6.1 --- a/tests/test_resource_invitation_constraints.sh Fri Apr 22 20:30:51 2016 +0200
6.2 +++ b/tests/test_resource_invitation_constraints.sh Fri Apr 22 20:33:51 2016 +0200
6.3 @@ -104,6 +104,7 @@
6.4 # Present the request to the resource.
6.5
6.6 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-rival.txt" 2>> $ERROR \
6.7 +| tee out3r.tmp \
6.8 | "$SHOWMAIL" \
6.9 > out3.tmp
6.10
6.11 @@ -114,7 +115,7 @@
6.12
6.13 # Present the response to the organiser.
6.14
6.15 - "$PERSON_SCRIPT" $ARGS < out3.tmp 2>> $ERROR \
6.16 + "$PERSON_SCRIPT" $ARGS < out3r.tmp 2>> $ERROR \
6.17 | tee out4r.tmp \
6.18 | "$SHOWMAIL" \
6.19 > out4.tmp
7.1 --- a/tests/test_resource_invitation_constraints_alternative.sh Fri Apr 22 20:30:51 2016 +0200
7.2 +++ b/tests/test_resource_invitation_constraints_alternative.sh Fri Apr 22 20:33:51 2016 +0200
7.3 @@ -35,6 +35,7 @@
7.4 # Present the request to the resource.
7.5
7.6 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-bad.txt" 2>> $ERROR \
7.7 +| tee out1r.tmp \
7.8 | "$SHOWMAIL" \
7.9 > out1.tmp
7.10
7.11 @@ -58,7 +59,7 @@
7.12
7.13 # Present the response to the organiser.
7.14
7.15 - "$PERSON_SCRIPT" $ARGS < out1.tmp 2>> $ERROR \
7.16 + "$PERSON_SCRIPT" $ARGS < out1r.tmp 2>> $ERROR \
7.17 | tee out2r.tmp \
7.18 | "$SHOWMAIL" \
7.19 > out2.tmp
7.20 @@ -99,6 +100,7 @@
7.21 "$OUTGOING_SCRIPT" $ARGS < out3.tmp 2>> $ERROR
7.22
7.23 "$RESOURCE_SCRIPT" $ARGS < out3.tmp 2>> $ERROR \
7.24 +| tee out4r.tmp \
7.25 | "$SHOWMAIL" \
7.26 > out4.tmp
7.27
7.28 @@ -131,6 +133,7 @@
7.29 # Present the request to the resource.
7.30
7.31 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-rival.txt" 2>> $ERROR \
7.32 +| tee out5r.tmp \
7.33 | "$SHOWMAIL" \
7.34 > out5.tmp
7.35
7.36 @@ -154,16 +157,17 @@
7.37
7.38 # Present the response to the organiser.
7.39
7.40 - "$PERSON_SCRIPT" $ARGS < out5.tmp 2>> $ERROR \
7.41 + "$PERSON_SCRIPT" $ARGS < out5r.tmp 2>> $ERROR \
7.42 +| tee out6r.tmp \
7.43 | "$SHOWMAIL" \
7.44 > out6.tmp
7.45
7.46 "$LIST_SCRIPT" $LIST_ARGS "$RIVALSENDER" "freebusy" \
7.47 -> out6r.tmp
7.48 +> out6R.tmp
7.49
7.50 - ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out6r.tmp" \
7.51 -&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out6r.tmp" \
7.52 -&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out6r.tmp" \
7.53 + ! grep -q "^20141126T151000Z${TAB}20141126T154500Z" "out6R.tmp" \
7.54 +&& ! grep -q "^20141126T151500Z${TAB}20141126T154500Z" "out6R.tmp" \
7.55 +&& grep -q "^20141126T153000Z${TAB}20141126T154500Z" "out6R.tmp" \
7.56 && echo "Success" \
7.57 || echo "Failed"
7.58
8.1 --- a/tests/test_resource_invitation_constraints_quota.sh Fri Apr 22 20:30:51 2016 +0200
8.2 +++ b/tests/test_resource_invitation_constraints_quota.sh Fri Apr 22 20:33:51 2016 +0200
8.3 @@ -335,11 +335,7 @@
8.4 # Since the email module used by showmail.py cannot stop after reading a single
8.5 # message, the second message is obtained.
8.6
8.7 - grep -n '^From ' out8r.tmp \
8.8 -| tail -n 1 \
8.9 -| cut -d ':' -f 1 \
8.10 -| xargs -I{} tail -n +'{}' out8r.tmp \
8.11 -| "$SHOWMAIL" \
8.12 + "$SHOWMAIL" 1 < out8r.tmp \
8.13 >> out8.tmp
8.14
8.15 grep -q 'METHOD:REPLY' out8.tmp \
9.1 --- a/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Fri Apr 22 20:30:51 2016 +0200
9.2 +++ b/tests/test_resource_invitation_constraints_quota_recurring_limits.sh Fri Apr 22 20:33:51 2016 +0200
9.3 @@ -108,11 +108,7 @@
9.4 # Since the email module used by showmail.py cannot stop after reading a single
9.5 # message, the second message is obtained.
9.6
9.7 - grep -n '^From ' out2r.tmp \
9.8 -| tail -n 1 \
9.9 -| cut -d ':' -f 1 \
9.10 -| xargs -I{} tail -n +'{}' out2r.tmp \
9.11 -| "$SHOWMAIL" \
9.12 + "$SHOWMAIL" 1 < out2r.tmp \
9.13 >> out2.tmp
9.14
9.15 grep -q 'METHOD:REPLY' out2.tmp \
9.16 @@ -218,11 +214,7 @@
9.17 # Since the email module used by showmail.py cannot stop after reading a single
9.18 # message, the second message is obtained.
9.19
9.20 - grep -n '^From ' out4r.tmp \
9.21 -| tail -n 1 \
9.22 -| cut -d ':' -f 1 \
9.23 -| xargs -I{} tail -n +'{}' out4r.tmp \
9.24 -| "$SHOWMAIL" \
9.25 + "$SHOWMAIL" 1 < out4r.tmp \
9.26 >> out4.tmp
9.27
9.28 grep -q 'METHOD:REPLY' out4.tmp \
10.1 --- a/tools/showmail.py Fri Apr 22 20:30:51 2016 +0200
10.2 +++ b/tools/showmail.py Fri Apr 22 20:33:51 2016 +0200
10.3 @@ -1,32 +1,69 @@
10.4 #!/usr/bin/env python
10.5
10.6 from email import message_from_string
10.7 +from email.generator import Generator
10.8 import sys
10.9
10.10 -def until_from(f):
10.11 - l = []
10.12 - s = f.readline()
10.13 - while s:
10.14 - l.append(s)
10.15 +try:
10.16 + from cStringIO import StringIO
10.17 +except ImportError:
10.18 + from StringIO import StringIO
10.19 +
10.20 +def until_from(f, skip=0):
10.21 + number = 0
10.22 + while number <= skip:
10.23 + l = []
10.24 s = f.readline()
10.25 - if s.startswith("From "):
10.26 + while s:
10.27 + l.append(s)
10.28 + s = f.readline()
10.29 + if s.startswith("From "):
10.30 + number += 1
10.31 + break
10.32 + else:
10.33 + number += 1
10.34 break
10.35 - return "".join(l)
10.36 + if number > skip:
10.37 + return "".join(l)
10.38 + else:
10.39 + return ""
10.40 +
10.41 +def as_string(message):
10.42 +
10.43 + """
10.44 + Return the string representation of 'message', attempting to preserve the
10.45 + precise original formatting.
10.46 + """
10.47 +
10.48 + out = StringIO()
10.49 + generator = Generator(out, False, 0) # disable reformatting measures
10.50 + generator.flatten(message)
10.51 + return out.getvalue()
10.52
10.53 def decode(part):
10.54 - for key, value in part.items():
10.55 - if key != "Content-Transfer-Encoding":
10.56 - print "%s: %s" % (key, value)
10.57 - print
10.58 - decoded = part.get_payload(decode=True)
10.59 - if decoded:
10.60 - print decoded
10.61 - print
10.62 +
10.63 + """
10.64 + Change the transfer encoding on 'part' and its subparts so that a plain text
10.65 + representation may be displayed.
10.66 + """
10.67 +
10.68 + payload = part.get_payload(decode=True)
10.69 + if payload:
10.70 + encoding = part.get("Content-Transfer-Encoding")
10.71 + if encoding:
10.72 + del part["Content-Transfer-Encoding"]
10.73 + part["Content-Transfer-Encoding"] = "8bit"
10.74 + part.set_payload(payload)
10.75 else:
10.76 - for part in part.get_payload():
10.77 - decode(part)
10.78 + for p in part.get_payload():
10.79 + decode(p)
10.80 +
10.81 +# Main program.
10.82
10.83 -message = message_from_string(until_from(sys.stdin))
10.84 -decode(message)
10.85 +if __name__ == "__main__":
10.86 + skip = int((sys.argv[1:] or [0])[0])
10.87 + message = message_from_string(until_from(sys.stdin, skip))
10.88 + decode(message)
10.89 + print as_string(message)
10.90
10.91 # vim: tabstop=4 expandtab shiftwidth=4