# HG changeset patch # User Paul Boddie # Date 1441664805 -7200 # Node ID ebed7907e1bab3e70b5d80a5f22edb4640253abe # Parent a679617f46fa74cdb143dadb384d147c93e03995 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. diff -r a679617f46fa -r ebed7907e1ba imip_store.py --- a/imip_store.py Tue Sep 08 00:23:49 2015 +0200 +++ b/imip_store.py Tue Sep 08 00:26:45 2015 +0200 @@ -629,6 +629,56 @@ release_freebusy = release_lock + # Tentative free/busy periods related to countering. + + def get_freebusy_offers(self, user): + + "Get free/busy offers for the given 'user'." + + offers = [] + expired = [] + now = datetime.now() + + # Expire old offers and save the collection if modified. + + l = self.get_freebusy_for_update(user, "freebusy-offers") + try: + for fb in l: + if fb.expires and get_datetime(fb.expires) <= now: + expired.append(fb) + else: + offers.append(fb) + + if expired: + self.set_freebusy_offers_in_update(user, offers) + + finally: + self.release_freebusy(user) + + return offers + + def get_freebusy_offers_for_update(self, user): + + """ + Get free/busy offers for the given 'user', locking the table. Dependent + code must release this lock regardless of it completing successfully. + """ + + self.acquire_lock(user) + return self.get_freebusy_offers(user) + + def set_freebusy_offers(self, user, freebusy): + + "For the given 'user', set 'freebusy' offers." + + return self.set_freebusy(user, freebusy, "freebusy-offers") + + def set_freebusy_offers_in_update(self, user, freebusy): + + "For the given 'user', set 'freebusy' offers during a compound update." + + return self.set_freebusy_in_update(user, freebusy, "freebusy-offers") + # Object status details access. def _get_requests(self, user, queue): diff -r a679617f46fa -r ebed7907e1ba imiptools/handlers/common.py --- a/imiptools/handlers/common.py Tue Sep 08 00:23:49 2015 +0200 +++ b/imiptools/handlers/common.py Tue Sep 08 00:26:45 2015 +0200 @@ -125,4 +125,40 @@ finally: self.store.release_freebusy(self.user) + def update_event_in_freebusy_offers(self): + + "Update free/busy offers when handling an object." + + freebusy = self.store.get_freebusy_offers_for_update(self.user) + try: + # Obtain the attendance attributes for this user, if available. + + self.update_freebusy_for_participant(freebusy, self.user) + + # Remove original recurrence details replaced by additional + # recurrences, as well as obsolete additional recurrences. + + self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid)) + self.store.set_freebusy_offers_in_update(self.user, freebusy) + + finally: + self.store.release_freebusy(self.user) + + return True + + def remove_event_from_freebusy_offers(self): + + "Remove free/busy offers when handling an object." + + freebusy = self.store.get_freebusy_offers_for_update(self.user) + try: + self.remove_from_freebusy(freebusy) + self.remove_freebusy_for_recurrences(freebusy) + self.store.set_freebusy_offers_in_update(self.user, freebusy) + + finally: + self.store.release_freebusy(self.user) + + return True + # vim: tabstop=4 expandtab shiftwidth=4 diff -r a679617f46fa -r ebed7907e1ba imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Tue Sep 08 00:23:49 2015 +0200 +++ b/imiptools/handlers/resource.py Tue Sep 08 00:26:45 2015 +0200 @@ -79,6 +79,7 @@ "Attempt to schedule the current object for the current user." method = "REPLY" + attendee_attr = self.obj.get_value_map("ATTENDEE")[self.user] # Check any constraints on the request. @@ -88,7 +89,7 @@ # Refuse to schedule obviously invalid requests. except ValidityError: - scheduled = False + attendee_attr = self.update_participation(self.obj, "DECLINED") # With a valid request, determine whether the event can be scheduled. @@ -101,53 +102,69 @@ # free/busy record and check for suitability. periods = self.obj.get_periods(tzid, self.get_window_end()) - freebusy = self.store.get_freebusy(self.user) - scheduled = self.can_schedule(freebusy, periods) + + freebusy = self.store.get_freebusy_for_update(self.user) + try: + offers = self.store.get_freebusy_offers(self.user) - # Where the corrected object can be scheduled, issue a counter - # request. + # Check the periods against any scheduled events and against + # any outstanding offers. - if scheduled and corrected: - method = "COUNTER" + scheduled = self.can_schedule(freebusy, periods) + scheduled = scheduled and self.can_schedule(offers, periods) - # Find the next available slot if the event cannot be scheduled. + # Where the corrected object can be scheduled, issue a counter + # request. - #elif not scheduled and len(periods) == 1: + if scheduled and corrected: + method = "COUNTER" - # # Find a free period, update the object with the details. + # Find the next available slot if the event cannot be scheduled. + + #elif not scheduled and len(periods) == 1: - # duration = periods[0].get_duration() - # free = invert_freebusy(freebusy) + # # Find a free period, update the object with the details. + + # duration = periods[0].get_duration() + # free = invert_freebusy(freebusy) - # for found in periods_from(free, periods[0]): - # # NOTE: Correct the found period first. - # if found.get_duration() >= duration - # scheduled = True - # method = "COUNTER" - # # NOTE: Set the period using the original duration. - # break + # for found in periods_from(free, periods[0]): + # # NOTE: Correct the found period first. + # if found.get_duration() >= duration + # scheduled = True + # method = "COUNTER" + # # NOTE: Set the period using the original duration. + # break - # Update the participation of the resource in the object. + # Update the participation of the resource in the object. + + attendee_attr = self.update_participation(self.obj, + scheduled and "ACCEPTED" or "DECLINED") - if method == "REPLY": - attendee_attr = self.update_participation(self.obj, - scheduled and "ACCEPTED" or "DECLINED") + # Update free/busy information. + + if method == "REPLY": + self.update_event_in_freebusy(for_organiser=False) + self.remove_event_from_freebusy_offers() - # Set the complete event or an additional occurrence. + # For countered proposals, record the offer in the resource's + # free/busy collection. - event = self.obj.to_node() - self.store.set_event(self.user, self.uid, self.recurrenceid, event) + elif method == "COUNTER": + self.update_event_in_freebusy_offers() - # Remove additional recurrences if handling a complete event. + finally: + self.store.release_freebusy(self.user) - if not self.recurrenceid: - self.store.remove_recurrences(self.user, self.uid) + # Set the complete event or an additional occurrence. + + event = self.obj.to_node() + self.store.set_event(self.user, self.uid, self.recurrenceid, event) - # Update free/busy information. + # Remove additional recurrences if handling a complete event. - self.update_event_in_freebusy(for_organiser=False) - else: - attendee_attr = self.obj.get_value_map("ATTENDEE")[self.user] + if not self.recurrenceid: + self.store.remove_recurrences(self.user, self.uid) # Make a version of the object with just this attendee, update the # DTSTAMP in the response, and return the object for sending. @@ -172,6 +189,12 @@ self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) self.store.cancel_event(self.user, self.uid, self.recurrenceid) + def _revoke_for_attendee(self): + + "Revoke any counter-proposal recorded as a free/busy offer." + + self.remove_event_from_freebusy_offers() + class Event(ResourceHandler): "An event handler." @@ -196,12 +219,9 @@ def declinecounter(self): - """ - Since this handler does not send counter proposals, it will not handle - replies to such proposals. - """ + "Revoke any counter-proposal." - pass + self._record_and_respond(self._revoke_for_attendee) def publish(self): diff -r a679617f46fa -r ebed7907e1ba imiptools/period.py --- a/imiptools/period.py Tue Sep 08 00:23:49 2015 +0200 +++ b/imiptools/period.py Tue Sep 08 00:26:45 2015 +0200 @@ -156,12 +156,16 @@ "A free/busy record abstraction." - def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, summary=None, organiser=None): + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, summary=None, organiser=None, expires=None): """ Initialise a free/busy period with the given 'start' and 'end' points, plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' details. + + An additional 'expires' parameter can be used to indicate an expiry + datetime in conjunction with free/busy offers made when countering + event proposals. """ self.start = isinstance(start, datetime) and start or get_datetime(start) @@ -171,6 +175,7 @@ self.recurrenceid = recurrenceid self.summary = summary self.organiser = organiser + self.expires = expires def as_tuple(self, strings_only=False): @@ -187,7 +192,8 @@ self.transp or strings_only and "OPAQUE" or None, self.recurrenceid or null(self.recurrenceid), self.summary or null(self.summary), - self.organiser or null(self.organiser) + self.organiser or null(self.organiser), + self.expires or null(self.expires) ) def __cmp__(self, other): diff -r a679617f46fa -r ebed7907e1ba tests/test_resource_invitation_constraints.sh --- a/tests/test_resource_invitation_constraints.sh Tue Sep 08 00:23:49 2015 +0200 +++ b/tests/test_resource_invitation_constraints.sh Tue Sep 08 00:26:45 2015 +0200 @@ -10,6 +10,10 @@ PREFS=/tmp/prefs ARGS="-S $STORE -P $STATIC -p $PREFS -d" USER="mailto:resource-room-sauna@example.com" +FBFILE="$STORE/$USER/freebusy" +FBOFFERFILE="$STORE/$USER/freebusy-offers" +TAB=`printf '\t'` + ERROR=err.tmp rm -r $STORE @@ -41,6 +45,15 @@ && echo "Success" \ || echo "Failed" + ! [ -e "$FBFILE" ] \ +|| ! grep -q "^20141126T151500Z${TAB}20141126T170000Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed" + + grep -q "^20141126T151500Z${TAB}20141126T170000Z" "$FBOFFERFILE" \ +&& echo "Success" \ +|| echo "Failed" + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \ | "$SHOWMAIL" \ > out3.tmp @@ -60,6 +73,15 @@ && echo "Success" \ || echo "Failed" + grep -q "^20141126T150000Z${TAB}20141126T151500Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed" + + ! grep -q "^20141126T150000Z${TAB}20141126T151500Z" "$FBOFFERFILE" \ +&& ! grep -q "^20141126T151500Z${TAB}20141126T170000Z" "$FBOFFERFILE" \ +&& echo "Success" \ +|| echo "Failed" + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \ | "$SHOWMAIL" \ > out6.tmp