# HG changeset patch # User Paul Boddie # Date 1441129420 -7200 # Node ID 70fb2f784339a58dfb7736331505b4d29b105083 # Parent 42c0ac3eb89c8c0ab828d422e368fb076ddcd855 Added updates to the free/busy providers when objects are handled, with new events being added to the providers and cancelled events being removed. Changed the order of object storage and free/busy updates in handlers so that incomplete objects (such as cancellation requests) do not affect inspection of any previously-stored objects. Added parameterisation of the freebusy tool for use in test scripts. diff -r 42c0ac3eb89c -r 70fb2f784339 imip_store.py --- a/imip_store.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imip_store.py Tue Sep 01 19:43:40 2015 +0200 @@ -351,6 +351,30 @@ # Free/busy period providers, upon extension of the free/busy records. + def _get_freebusy_providers(self, user): + + """ + Return the free/busy providers for the given 'user'. + + This function returns any stored datetime and a list of providers as a + 2-tuple. Each provider is itself a (uid, recurrenceid) tuple. + """ + + filename = self.get_object_in_store(user, "freebusy-providers") + if not filename or not exists(filename): + return None + + # Attempt to read providers, with a declaration of the datetime + # from which such providers are considered as still being active. + + t = self._get_table(user, filename, [(1, None)]) + try: + dt_string = t[0][0] + except IndexError: + return None + + return dt_string, t[1:] + def get_freebusy_providers(self, user, dt=None): """ @@ -361,32 +385,39 @@ details will be returned. Otherwise, if 'dt' is earlier than the datetime recorded for the known providers, None is returned, indicating that the list of providers must be recomputed. + + This function returns a list of (uid, recurrenceid) tuples upon success. """ + t = self._get_freebusy_providers(user) + if not t: + return None + + dt_string, t = t + + # If the requested datetime is earlier than the stated datetime, the + # providers will need to be recomputed. + + if dt: + providers_dt = get_datetime(dt_string) + if not providers_dt or providers_dt > dt: + return None + + # Otherwise, return the providers. + + return t[1:] + + def _set_freebusy_providers(self, user, dt_string, t): + + "Set the given provider timestamp 'dt_string' and table 't'." + filename = self.get_object_in_store(user, "freebusy-providers") - if not filename or not exists(filename): - return None - else: - # Attempt to read providers, with a declaration of the datetime - # from which such providers are considered as still being active. - - t = self._get_table(user, filename, [(1, None)]) - try: - dt_string = t[0][0] - except IndexError: - return None + if not filename: + return False - # If the requested datetime is earlier than the stated datetime, the - # providers will need to be recomputed. - - if dt: - providers_dt = get_datetime(dt_string) - if not providers_dt or providers_dt > dt: - return None - - # Otherwise, return the providers. - - return t[1:] + t.insert(0, (dt_string,)) + self._set_table(user, filename, t, [(1, "")]) + return True def set_freebusy_providers(self, user, dt, providers): @@ -395,17 +426,41 @@ given datetime 'dt'. """ - t = [(format_datetime(dt),)] + t = [] for obj in providers: - t.append((obj.get_uid(), obj.get_recurrenceid() or "")) + t.append((obj.get_uid(), obj.get_recurrenceid())) + + return self._set_freebusy_providers(user, format_datetime(dt), t) - filename = self.get_object_in_store(user, "freebusy-providers") - if not filename: + def append_freebusy_provider(self, user, provider): + + "For the given 'user', append the free/busy 'provider'." + + t = self._get_freebusy_providers(user) + if not t: return False - self._set_table(user, filename, t) - return True + dt_string, t = t + t.append((provider.get_uid(), provider.get_recurrenceid())) + + return self._set_freebusy_providers(user, dt_string, t) + + def remove_freebusy_provider(self, user, provider): + + "For the given 'user', remove the free/busy 'provider'." + + t = self._get_freebusy_providers(user) + if not t: + return False + + dt_string, t = t + try: + t.remove((provider.get_uid(), provider.get_recurrenceid())) + except ValueError: + return False + + return self._set_freebusy_providers(user, dt_string, t) # Free/busy period access. diff -r 42c0ac3eb89c -r 70fb2f784339 imiptools/client.py --- a/imiptools/client.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imiptools/client.py Tue Sep 01 19:43:40 2015 +0200 @@ -418,6 +418,18 @@ return False + def possibly_recurring_indefinitely(self): + + "Return whether the object recurs indefinitely." + + # Obtain the stored object to make sure that recurrence information + # is not being ignored. This might happen if a client sends a + # cancellation without the complete set of properties, for instance. + + return self.obj.possibly_recurring_indefinitely() or \ + self.get_stored_object_version() and \ + self.get_stored_object_version().possibly_recurring_indefinitely() + # Constraint application on event periods. def check_object(self): diff -r 42c0ac3eb89c -r 70fb2f784339 imiptools/data.py --- a/imiptools/data.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imiptools/data.py Tue Sep 01 19:43:40 2015 +0200 @@ -330,18 +330,23 @@ if p.get_end_point() > dt: return True + return self.possibly_recurring_indefinitely() + + def possibly_recurring_indefinitely(self): + + "Return whether this object may recur indefinitely." + rrule = self.get_value("RRULE") parameters = rrule and get_parameters(rrule) until = parameters and parameters.get("UNTIL") count = parameters and parameters.get("COUNT") - # Non-recurring periods or constrained recurrences that are not found to - # lie beyond the specified datetime. + # Non-recurring periods or constrained recurrences. if not rrule or until or count: return False - # Unconstrained recurring periods will always lie beyond the specified + # Unconstrained recurring periods will always lie beyond any specified # datetime. else: diff -r 42c0ac3eb89c -r 70fb2f784339 imiptools/handlers/common.py --- a/imiptools/handlers/common.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imiptools/handlers/common.py Tue Sep 01 19:43:40 2015 +0200 @@ -89,6 +89,12 @@ if self.publisher and self.is_sharing(): self.publisher.set_freebusy(self.user, freebusy) + # Update free/busy provider information if the event may recur + # indefinitely. + + if self.possibly_recurring_indefinitely(): + self.store.append_freebusy_provider(self.user, self.obj) + return True def remove_event_from_freebusy(self): @@ -103,4 +109,10 @@ if self.publisher and self.is_sharing(): self.publisher.set_freebusy(self.user, freebusy) + # Update free/busy provider information if the event may recur + # indefinitely. + + if self.possibly_recurring_indefinitely(): + self.store.remove_freebusy_provider(self.user, self.obj) + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 42c0ac3eb89c -r 70fb2f784339 imiptools/handlers/person.py --- a/imiptools/handlers/person.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imiptools/handlers/person.py Tue Sep 01 19:43:40 2015 +0200 @@ -62,10 +62,6 @@ if obj_attendees.has_key(organiser): obj_attendees[organiser]["PARTSTAT"] = "DECLINED" - # Set the complete event or an additional occurrence. - - self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) - # Remove additional recurrences if handling a complete event. if not self.recurrenceid: @@ -96,6 +92,10 @@ self.update_freebusy_from_organiser(organiser) + # Set the complete event or an additional occurrence. + + self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) + # As organiser, update attendance from valid attendees. else: diff -r 42c0ac3eb89c -r 70fb2f784339 imiptools/handlers/person_outgoing.py --- a/imiptools/handlers/person_outgoing.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imiptools/handlers/person_outgoing.py Tue Sep 01 19:43:40 2015 +0200 @@ -131,6 +131,11 @@ obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or [] obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or [] + # Update free/busy information. + + if cancel_entire_event or self.user in given_attendees: + self.remove_event_from_freebusy() + # Set the complete event if not an additional occurrence. For any newly- # indicated occurrence, use the received event details. @@ -140,11 +145,6 @@ self.store.dequeue_request(self.user, self.uid, self.recurrenceid) - # Update free/busy information. - - if cancel_entire_event or self.user in given_attendees: - self.remove_event_from_freebusy() - return True class Event(PersonHandler): diff -r 42c0ac3eb89c -r 70fb2f784339 imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imiptools/handlers/resource.py Tue Sep 01 19:43:40 2015 +0200 @@ -142,13 +142,15 @@ the current object. """ - self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) - self.store.cancel_event(self.user, self.uid, self.recurrenceid) - # Update free/busy information. self.remove_event_from_freebusy() + # Update the stored event and cancel it. + + self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) + self.store.cancel_event(self.user, self.uid, self.recurrenceid) + class Event(ResourceHandler): "An event handler." diff -r 42c0ac3eb89c -r 70fb2f784339 imipweb/resource.py --- a/imipweb/resource.py Tue Sep 01 15:59:42 2015 +0200 +++ b/imipweb/resource.py Tue Sep 01 19:43:40 2015 +0200 @@ -213,12 +213,24 @@ self.store.set_freebusy(self.user, freebusy) self.publish_freebusy(freebusy) + # Update free/busy provider information if the event may recur + # indefinitely. + + if obj.possibly_recurring_indefinitely(): + self.store.append_freebusy_provider(self.user, obj) + def remove_from_freebusy(self, uid, recurrenceid=None): freebusy = self.store.get_freebusy(self.user) remove_period(freebusy, uid, recurrenceid) self.store.set_freebusy(self.user, freebusy) self.publish_freebusy(freebusy) + # Update free/busy provider information if the event may recur + # indefinitely. + + if obj.possibly_recurring_indefinitely(): + self.store.remove_freebusy_provider(self.user, obj) + def publish_freebusy(self, freebusy): "Publish the details if configured to share them." diff -r 42c0ac3eb89c -r 70fb2f784339 tests/templates/event-cancel-recurring-indefinitely.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-cancel-recurring-indefinitely.txt Tue Sep 01 19:43:40 2015 +0200 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-room-confroom@example.com +Subject: Cancellation! + +Cancel the event for resource-room-confroom and paul.boddie. + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="CANCEL" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:CANCEL +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com +ATTENDEE;RSVP=TRUE:mailto:paul.boddie@example.com +DTSTAMP:20141009T182400Z +SUMMARY:Recurring event indefinitely +UID:event14@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r 42c0ac3eb89c -r 70fb2f784339 tests/templates/event-request-recurring-indefinitely.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-recurring-indefinitely.txt Tue Sep 01 19:43:40 2015 +0200 @@ -0,0 +1,34 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-room-confroom@example.com +Subject: Invitation! + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-confroom@example.com +DTSTAMP:20141009T182400Z +DTSTART;TZID=Europe/Oslo:20141010T100000 +DTEND;TZID=Europe/Oslo:20141010T110000 +RRULE:FREQ=MONTHLY;BYDAY=2FR +SUMMARY:Recurring event indefinitely +UID:event14@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r 42c0ac3eb89c -r 70fb2f784339 tests/test_resource_invitation_recurring_indefinitely.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_resource_invitation_recurring_indefinitely.sh Tue Sep 01 19:43:40 2015 +0200 @@ -0,0 +1,105 @@ +#!/bin/sh + +THIS_DIR=`dirname $0` +BASE_DIR="$THIS_DIR/.." + +TEMPLATES="$THIS_DIR/templates" +RESOURCE_SCRIPT="$BASE_DIR/imip_resource.py" +FREEBUSY_SCRIPT="$BASE_DIR/tools/make_freebusy.py" +SHOWMAIL="$BASE_DIR/tools/showmail.py" +STORE=/tmp/store +STATIC=/tmp/static +PREFS=/tmp/prefs +ARGS="-S $STORE -P $STATIC -p $PREFS -d" +FBARGS="-s -n" +USER="mailto:resource-room-confroom@example.com" +ERROR=err.tmp + +rm -r $STORE +rm -r $STATIC +rm -r $PREFS +rm $ERROR +rm out*.tmp + +mkdir -p "$PREFS/$USER" +echo 'Europe/Oslo' > "$PREFS/$USER/TZID" +echo 'share' > "$PREFS/$USER/freebusy_sharing" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out0.tmp + + grep -q 'METHOD:REPLY' out0.tmp \ +&& ! grep -q '^FREEBUSY' out0.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring-indefinitely.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out2.tmp + + grep -q 'METHOD:REPLY' out2.tmp \ +&& grep -q 'ATTENDEE;PARTSTAT=ACCEPTED' out2.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out3.tmp + + grep -q 'METHOD:REPLY' out3.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141114T090000Z/20141114T100000Z' out3.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141212T090000Z/20141212T100000Z' out3.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20150109T090000Z/20150109T100000Z' out3.tmp \ +&& echo "Success" \ +|| echo "Failed" + +PYTHONPATH="$BASE_DIR" "$FREEBUSY_SCRIPT" "$USER" $FBARGS $ARGS 2>> $ERROR + + grep -q 'event14@example.com' "$STORE/$USER/freebusy-providers" \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-recurring-indefinitely.txt" 2>> $ERROR +echo "Cancel..." + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out4.tmp + + grep -q 'METHOD:REPLY' out4.tmp \ +&& ! grep -q 'FREEBUSY;FBTYPE=BUSY:20141114T090000Z/20141114T100000Z' out4.tmp \ +&& ! grep -q 'FREEBUSY;FBTYPE=BUSY:20141212T090000Z/20141212T100000Z' out4.tmp \ +&& ! grep -q 'FREEBUSY;FBTYPE=BUSY:20150109T090000Z/20150109T100000Z' out4.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! grep -q 'event14@example.com' "$STORE/$USER/freebusy-providers" \ +&& echo "Success" \ +|| echo "Failed" + +# Re-add event to test scheduling and presence in the freebusy-providers file. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-recurring-indefinitely.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out5.tmp + + grep -q 'METHOD:REPLY' out5.tmp \ +&& grep -q 'ATTENDEE;PARTSTAT=ACCEPTED' out5.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out6.tmp + + grep -q 'METHOD:REPLY' out6.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141114T090000Z/20141114T100000Z' out6.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20141212T090000Z/20141212T100000Z' out6.tmp \ +&& grep -q 'FREEBUSY;FBTYPE=BUSY:20150109T090000Z/20150109T100000Z' out6.tmp \ +&& echo "Success" \ +|| echo "Failed" + + grep -q 'event14@example.com' "$STORE/$USER/freebusy-providers" \ +&& echo "Success" \ +|| echo "Failed" diff -r 42c0ac3eb89c -r 70fb2f784339 tools/make_freebusy.py --- a/tools/make_freebusy.py Tue Sep 01 15:59:42 2015 +0200 +++ b/tools/make_freebusy.py Tue Sep 01 19:43:40 2015 +0200 @@ -29,12 +29,14 @@ from imip_store import FileStore, FilePublisher import sys -def make_freebusy(user, participant, store_and_publish, include_needs_action, reset_updated_list, verbose): +def make_freebusy(store, publisher, preferences, user, participant, + store_and_publish, include_needs_action, reset_updated_list, verbose): """ - Make free/busy details for the records of the given 'user', generating - details for 'participant' if not indicated as None; otherwise, generating - free/busy details concerning the given user. + Using the given 'store', 'publisher' and 'preferences', make free/busy + details for the records of the given 'user', generating details for + 'participant' if not indicated as None; otherwise, generating free/busy + details concerning the given user. If 'store_and_publish' is set, the stored details will be updated; otherwise, the details will be written to standard output. @@ -51,8 +53,6 @@ """ participant = participant or user - - preferences = Preferences(user) tzid = preferences.get("TZID") or get_default_timezone() # Get the size of the free/busy window. @@ -63,9 +63,6 @@ window_size = 100 window_end = get_window_end(tzid, window_size) - store = FileStore() - publisher = FilePublisher() - # Get identifiers for uncancelled events either from a list of events # providing free/busy periods at the end of the given time window, or from # a list of all events. @@ -132,15 +129,33 @@ # Interpret the command line arguments. + participants = [] + args = [] + store_dir = [] + publishing_dir = [] + preferences_dir = [] + ignored = [] + + # Collect user details first, switching to other arguments when encountering + # switches. + + l = participants + + for arg in sys.argv[1:]: + if arg in ("-n", "-s", "-v", "-r"): + args.append(arg) + l = ignored + elif arg == "-S": + l = store_dir + elif arg == "-P": + l = publishing_dir + elif arg == "-p": + l = preferences_dir + else: + l.append(arg) + try: - user = sys.argv[1] - args = sys.argv[2:] - participant = args and args[0] not in ("-n", "-s", "-v", "-r") and args[0] or None - store_and_publish = "-s" in args - include_needs_action = "-n" in args - reset_updated_list = "-r" in args - verbose = "-v" in args - + user = participants[0] except IndexError: print >>sys.stderr, """\ Need a user and an optional participant (if different from the user), @@ -151,14 +166,39 @@ """ sys.exit(1) + # Define any other participant of interest plus options. + + participant = participants[1:] and participants[1] or None + store_and_publish = "-s" in args + include_needs_action = "-n" in args + reset_updated_list = "-r" in args + verbose = "-v" in args + + # Override defaults if indicated. + + store_dir = store_dir and store_dir[0] or None + publishing_dir = publishing_dir and publishing_dir[0] or None + preferences_dir = preferences_dir and preferences_dir[0] or None + + # Obtain store-related objects. + + store = FileStore(store_dir) + publisher = FilePublisher(publishing_dir) + preferences = Preferences(user, preferences_dir) + + # Obtain a list of users for processing. + if user in ("*", "all"): - users = FileStore().get_users() + users = store.get_users() else: users = [user] + # Process the given users. + for user in users: if verbose: print >>sys.stderr, user - make_freebusy(user, participant, store_and_publish, include_needs_action, reset_updated_list, verbose) + make_freebusy(store, publisher, preferences, user, participant, + store_and_publish, include_needs_action, reset_updated_list, verbose) # vim: tabstop=4 expandtab shiftwidth=4