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