imip-agent

Changeset

1153:967f8a54ef75
2016-04-22 Paul Boddie raw files shortlog changelog graph 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.
imiptools/handlers/resource.py (file) imiptools/handlers/scheduling/__init__.py (file) imiptools/handlers/scheduling/access.py (file) imiptools/handlers/scheduling/freebusy.py (file) imiptools/handlers/scheduling/quota.py (file) tests/test_resource_invitation_constraints.sh (file) tests/test_resource_invitation_constraints_alternative.sh (file) tests/test_resource_invitation_constraints_quota.sh (file) tests/test_resource_invitation_constraints_quota_recurring_limits.sh (file) tools/showmail.py (file)
     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