imip-agent

Changeset

710:ebed7907e1ba
2015-09-08 Paul Boddie raw files shortlog changelog graph Added initial support for free/busy "offers" so that counter-proposals may be able to temporarily reserve periods and thus prevent the allocation of such periods to other events before an organiser is able to respond.
imip_store.py (file) imiptools/handlers/common.py (file) imiptools/handlers/resource.py (file) imiptools/period.py (file) tests/test_resource_invitation_constraints.sh (file)
     1.1 --- a/imip_store.py	Tue Sep 08 00:23:49 2015 +0200
     1.2 +++ b/imip_store.py	Tue Sep 08 00:26:45 2015 +0200
     1.3 @@ -629,6 +629,56 @@
     1.4  
     1.5      release_freebusy = release_lock
     1.6  
     1.7 +    # Tentative free/busy periods related to countering.
     1.8 +
     1.9 +    def get_freebusy_offers(self, user):
    1.10 +
    1.11 +        "Get free/busy offers for the given 'user'."
    1.12 +
    1.13 +        offers = []
    1.14 +        expired = []
    1.15 +        now = datetime.now()
    1.16 +
    1.17 +        # Expire old offers and save the collection if modified.
    1.18 +
    1.19 +        l = self.get_freebusy_for_update(user, "freebusy-offers")
    1.20 +        try:
    1.21 +            for fb in l:
    1.22 +                if fb.expires and get_datetime(fb.expires) <= now:
    1.23 +                    expired.append(fb)
    1.24 +                else:
    1.25 +                    offers.append(fb)
    1.26 +
    1.27 +            if expired:
    1.28 +                self.set_freebusy_offers_in_update(user, offers)
    1.29 +
    1.30 +        finally:
    1.31 +            self.release_freebusy(user)
    1.32 +
    1.33 +        return offers
    1.34 +
    1.35 +    def get_freebusy_offers_for_update(self, user):
    1.36 +
    1.37 +        """
    1.38 +        Get free/busy offers for the given 'user', locking the table. Dependent
    1.39 +        code must release this lock regardless of it completing successfully.
    1.40 +        """
    1.41 +
    1.42 +        self.acquire_lock(user)
    1.43 +        return self.get_freebusy_offers(user)
    1.44 +
    1.45 +    def set_freebusy_offers(self, user, freebusy):
    1.46 +
    1.47 +        "For the given 'user', set 'freebusy' offers."
    1.48 +
    1.49 +        return self.set_freebusy(user, freebusy, "freebusy-offers")
    1.50 +
    1.51 +    def set_freebusy_offers_in_update(self, user, freebusy):
    1.52 +
    1.53 +        "For the given 'user', set 'freebusy' offers during a compound update."
    1.54 +
    1.55 +        return self.set_freebusy_in_update(user, freebusy, "freebusy-offers")
    1.56 +
    1.57      # Object status details access.
    1.58  
    1.59      def _get_requests(self, user, queue):
     2.1 --- a/imiptools/handlers/common.py	Tue Sep 08 00:23:49 2015 +0200
     2.2 +++ b/imiptools/handlers/common.py	Tue Sep 08 00:26:45 2015 +0200
     2.3 @@ -125,4 +125,40 @@
     2.4          finally:
     2.5              self.store.release_freebusy(self.user)
     2.6  
     2.7 +    def update_event_in_freebusy_offers(self):
     2.8 +
     2.9 +        "Update free/busy offers when handling an object."
    2.10 +
    2.11 +        freebusy = self.store.get_freebusy_offers_for_update(self.user)
    2.12 +        try:
    2.13 +            # Obtain the attendance attributes for this user, if available.
    2.14 +
    2.15 +            self.update_freebusy_for_participant(freebusy, self.user)
    2.16 +
    2.17 +            # Remove original recurrence details replaced by additional
    2.18 +            # recurrences, as well as obsolete additional recurrences.
    2.19 +
    2.20 +            self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
    2.21 +            self.store.set_freebusy_offers_in_update(self.user, freebusy)
    2.22 +
    2.23 +        finally:
    2.24 +            self.store.release_freebusy(self.user)
    2.25 +
    2.26 +        return True
    2.27 +
    2.28 +    def remove_event_from_freebusy_offers(self):
    2.29 +
    2.30 +        "Remove free/busy offers when handling an object."
    2.31 +
    2.32 +        freebusy = self.store.get_freebusy_offers_for_update(self.user)
    2.33 +        try:
    2.34 +            self.remove_from_freebusy(freebusy)
    2.35 +            self.remove_freebusy_for_recurrences(freebusy)
    2.36 +            self.store.set_freebusy_offers_in_update(self.user, freebusy)
    2.37 +
    2.38 +        finally:
    2.39 +            self.store.release_freebusy(self.user)
    2.40 +
    2.41 +        return True
    2.42 +
    2.43  # vim: tabstop=4 expandtab shiftwidth=4
     3.1 --- a/imiptools/handlers/resource.py	Tue Sep 08 00:23:49 2015 +0200
     3.2 +++ b/imiptools/handlers/resource.py	Tue Sep 08 00:26:45 2015 +0200
     3.3 @@ -79,6 +79,7 @@
     3.4          "Attempt to schedule the current object for the current user."
     3.5  
     3.6          method = "REPLY"
     3.7 +        attendee_attr = self.obj.get_value_map("ATTENDEE")[self.user]
     3.8  
     3.9          # Check any constraints on the request.
    3.10  
    3.11 @@ -88,7 +89,7 @@
    3.12          # Refuse to schedule obviously invalid requests.
    3.13  
    3.14          except ValidityError:
    3.15 -            scheduled = False
    3.16 +            attendee_attr = self.update_participation(self.obj, "DECLINED")
    3.17  
    3.18          # With a valid request, determine whether the event can be scheduled.
    3.19  
    3.20 @@ -101,53 +102,69 @@
    3.21              # free/busy record and check for suitability.
    3.22  
    3.23              periods = self.obj.get_periods(tzid, self.get_window_end())
    3.24 -            freebusy = self.store.get_freebusy(self.user)
    3.25 -            scheduled = self.can_schedule(freebusy, periods)
    3.26 +
    3.27 +            freebusy = self.store.get_freebusy_for_update(self.user)
    3.28 +            try:
    3.29 +                offers = self.store.get_freebusy_offers(self.user)
    3.30  
    3.31 -            # Where the corrected object can be scheduled, issue a counter
    3.32 -            # request.
    3.33 +                # Check the periods against any scheduled events and against
    3.34 +                # any outstanding offers.
    3.35  
    3.36 -            if scheduled and corrected:
    3.37 -                method = "COUNTER"
    3.38 +                scheduled = self.can_schedule(freebusy, periods)
    3.39 +                scheduled = scheduled and self.can_schedule(offers, periods)
    3.40  
    3.41 -            # Find the next available slot if the event cannot be scheduled.
    3.42 +                # Where the corrected object can be scheduled, issue a counter
    3.43 +                # request.
    3.44  
    3.45 -            #elif not scheduled and len(periods) == 1:
    3.46 +                if scheduled and corrected:
    3.47 +                    method = "COUNTER"
    3.48  
    3.49 -            #    # Find a free period, update the object with the details.
    3.50 +                # Find the next available slot if the event cannot be scheduled.
    3.51 +
    3.52 +                #elif not scheduled and len(periods) == 1:
    3.53  
    3.54 -            #    duration = periods[0].get_duration()
    3.55 -            #    free = invert_freebusy(freebusy)
    3.56 +                #    # Find a free period, update the object with the details.
    3.57 +
    3.58 +                #    duration = periods[0].get_duration()
    3.59 +                #    free = invert_freebusy(freebusy)
    3.60  
    3.61 -            #    for found in periods_from(free, periods[0]):
    3.62 -            #        # NOTE: Correct the found period first.
    3.63 -            #        if found.get_duration() >= duration
    3.64 -            #            scheduled = True
    3.65 -            #            method = "COUNTER"
    3.66 -            #            # NOTE: Set the period using the original duration.
    3.67 -            #            break
    3.68 +                #    for found in periods_from(free, periods[0]):
    3.69 +                #        # NOTE: Correct the found period first.
    3.70 +                #        if found.get_duration() >= duration
    3.71 +                #            scheduled = True
    3.72 +                #            method = "COUNTER"
    3.73 +                #            # NOTE: Set the period using the original duration.
    3.74 +                #            break
    3.75  
    3.76 -        # Update the participation of the resource in the object.
    3.77 +                # Update the participation of the resource in the object.
    3.78 +
    3.79 +                attendee_attr = self.update_participation(self.obj,
    3.80 +                    scheduled and "ACCEPTED" or "DECLINED")
    3.81  
    3.82 -        if method == "REPLY":
    3.83 -            attendee_attr = self.update_participation(self.obj,
    3.84 -                scheduled and "ACCEPTED" or "DECLINED")
    3.85 +                # Update free/busy information.
    3.86 +
    3.87 +                if method == "REPLY":
    3.88 +                    self.update_event_in_freebusy(for_organiser=False)
    3.89 +                    self.remove_event_from_freebusy_offers()
    3.90  
    3.91 -            # Set the complete event or an additional occurrence.
    3.92 +                # For countered proposals, record the offer in the resource's
    3.93 +                # free/busy collection.
    3.94  
    3.95 -            event = self.obj.to_node()
    3.96 -            self.store.set_event(self.user, self.uid, self.recurrenceid, event)
    3.97 +                elif method == "COUNTER":
    3.98 +                    self.update_event_in_freebusy_offers()
    3.99  
   3.100 -            # Remove additional recurrences if handling a complete event.
   3.101 +            finally:
   3.102 +                self.store.release_freebusy(self.user)
   3.103  
   3.104 -            if not self.recurrenceid:
   3.105 -                self.store.remove_recurrences(self.user, self.uid)
   3.106 +        # Set the complete event or an additional occurrence.
   3.107 +
   3.108 +        event = self.obj.to_node()
   3.109 +        self.store.set_event(self.user, self.uid, self.recurrenceid, event)
   3.110  
   3.111 -            # Update free/busy information.
   3.112 +        # Remove additional recurrences if handling a complete event.
   3.113  
   3.114 -            self.update_event_in_freebusy(for_organiser=False)
   3.115 -        else:
   3.116 -            attendee_attr = self.obj.get_value_map("ATTENDEE")[self.user]
   3.117 +        if not self.recurrenceid:
   3.118 +            self.store.remove_recurrences(self.user, self.uid)
   3.119  
   3.120          # Make a version of the object with just this attendee, update the
   3.121          # DTSTAMP in the response, and return the object for sending.
   3.122 @@ -172,6 +189,12 @@
   3.123          self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
   3.124          self.store.cancel_event(self.user, self.uid, self.recurrenceid)
   3.125  
   3.126 +    def _revoke_for_attendee(self):
   3.127 +
   3.128 +        "Revoke any counter-proposal recorded as a free/busy offer."
   3.129 +
   3.130 +        self.remove_event_from_freebusy_offers()
   3.131 +
   3.132  class Event(ResourceHandler):
   3.133  
   3.134      "An event handler."
   3.135 @@ -196,12 +219,9 @@
   3.136  
   3.137      def declinecounter(self):
   3.138  
   3.139 -        """
   3.140 -        Since this handler does not send counter proposals, it will not handle
   3.141 -        replies to such proposals.
   3.142 -        """
   3.143 +        "Revoke any counter-proposal."
   3.144  
   3.145 -        pass
   3.146 +        self._record_and_respond(self._revoke_for_attendee)
   3.147  
   3.148      def publish(self):
   3.149  
     4.1 --- a/imiptools/period.py	Tue Sep 08 00:23:49 2015 +0200
     4.2 +++ b/imiptools/period.py	Tue Sep 08 00:26:45 2015 +0200
     4.3 @@ -156,12 +156,16 @@
     4.4  
     4.5      "A free/busy record abstraction."
     4.6  
     4.7 -    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, summary=None, organiser=None):
     4.8 +    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, summary=None, organiser=None, expires=None):
     4.9  
    4.10          """
    4.11          Initialise a free/busy period with the given 'start' and 'end' points,
    4.12          plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
    4.13          details.
    4.14 +
    4.15 +        An additional 'expires' parameter can be used to indicate an expiry
    4.16 +        datetime in conjunction with free/busy offers made when countering
    4.17 +        event proposals.
    4.18          """
    4.19  
    4.20          self.start = isinstance(start, datetime) and start or get_datetime(start)
    4.21 @@ -171,6 +175,7 @@
    4.22          self.recurrenceid = recurrenceid
    4.23          self.summary = summary
    4.24          self.organiser = organiser
    4.25 +        self.expires = expires
    4.26  
    4.27      def as_tuple(self, strings_only=False):
    4.28  
    4.29 @@ -187,7 +192,8 @@
    4.30              self.transp or strings_only and "OPAQUE" or None,
    4.31              self.recurrenceid or null(self.recurrenceid),
    4.32              self.summary or null(self.summary),
    4.33 -            self.organiser or null(self.organiser)
    4.34 +            self.organiser or null(self.organiser),
    4.35 +            self.expires or null(self.expires)
    4.36              )
    4.37  
    4.38      def __cmp__(self, other):
     5.1 --- a/tests/test_resource_invitation_constraints.sh	Tue Sep 08 00:23:49 2015 +0200
     5.2 +++ b/tests/test_resource_invitation_constraints.sh	Tue Sep 08 00:26:45 2015 +0200
     5.3 @@ -10,6 +10,10 @@
     5.4  PREFS=/tmp/prefs
     5.5  ARGS="-S $STORE -P $STATIC -p $PREFS -d"
     5.6  USER="mailto:resource-room-sauna@example.com"
     5.7 +FBFILE="$STORE/$USER/freebusy"
     5.8 +FBOFFERFILE="$STORE/$USER/freebusy-offers"
     5.9 +TAB=`printf '\t'`
    5.10 +
    5.11  ERROR=err.tmp
    5.12  
    5.13  rm -r $STORE
    5.14 @@ -41,6 +45,15 @@
    5.15  && echo "Success" \
    5.16  || echo "Failed"
    5.17  
    5.18 +   ! [ -e "$FBFILE" ] \
    5.19 +|| ! grep -q "^20141126T151500Z${TAB}20141126T170000Z" "$FBFILE" \
    5.20 +&& echo "Success" \
    5.21 +|| echo "Failed"
    5.22 +
    5.23 +   grep -q "^20141126T151500Z${TAB}20141126T170000Z" "$FBOFFERFILE" \
    5.24 +&& echo "Success" \
    5.25 +|| echo "Failed"
    5.26 +
    5.27    "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
    5.28  | "$SHOWMAIL" \
    5.29  > out3.tmp
    5.30 @@ -60,6 +73,15 @@
    5.31  && echo "Success" \
    5.32  || echo "Failed"
    5.33  
    5.34 +   grep -q "^20141126T150000Z${TAB}20141126T151500Z" "$FBFILE" \
    5.35 +&& echo "Success" \
    5.36 +|| echo "Failed"
    5.37 +
    5.38 +   ! grep -q "^20141126T150000Z${TAB}20141126T151500Z" "$FBOFFERFILE" \
    5.39 +&& ! grep -q "^20141126T151500Z${TAB}20141126T170000Z" "$FBOFFERFILE" \
    5.40 +&& echo "Success" \
    5.41 +|| echo "Failed"
    5.42 +
    5.43    "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
    5.44  | "$SHOWMAIL" \
    5.45  > out6.tmp