# HG changeset patch # User Paul Boddie # Date 1461020039 -7200 # Node ID 94f66de18a1795df693ff34fbe3dcc32fcf8d300 # Parent 3cb8c82d705c702a22b306e3e20d5fb3ede9314e Support replies from attendees that refer to specific recurrences before the organiser does so, thus allowing attendees to selectively accept and decline recurrences. Allowed the test handler to refer to recurrences that have not been explicitly separated from their parent objects. Added a docstring for the Object initialiser as a reminder of how to use it. diff -r 3cb8c82d705c -r 94f66de18a17 imiptools/client.py --- a/imiptools/client.py Mon Apr 18 21:07:41 2016 +0200 +++ b/imiptools/client.py Tue Apr 19 00:53:59 2016 +0200 @@ -385,6 +385,13 @@ return get_uri(self.obj.get_value("ORGANIZER")) == self.user + def is_recurrence(self): + + "Return whether the current object is a recurrence of its parent." + + parent = self.get_parent_object() + return parent and parent.has_recurrence(self.get_tzid(), self.obj.get_recurrenceid()) + # Common operations on calendar data. def update_senders(self, obj=None): diff -r 3cb8c82d705c -r 94f66de18a17 imiptools/data.py --- a/imiptools/data.py Mon Apr 18 21:07:41 2016 +0200 +++ b/imiptools/data.py Tue Apr 19 00:53:59 2016 +0200 @@ -44,6 +44,25 @@ "Access to calendar structures." def __init__(self, fragment): + + """ + Initialise the object with the given 'fragment'. This must be a + dictionary mapping an object type (such as "VEVENT") to a tuple + containing the object details and attributes, each being a dictionary + itself. + + The result of parse_object can be processed to obtain a fragment by + obtaining a collection of records for an object type. For example: + + l = parse_object(f, encoding, "VCALENDAR") + events = l["VEVENT"] + event = events[0] + + Then, the specific object must be presented as follows: + + object = Object({"VEVENT" : event}) + """ + self.objtype, (self.details, self.attr) = fragment.items()[0] def get_uid(self): @@ -219,7 +238,7 @@ return (dtstart, dtstart_attr), (dtend, dtend_attr) - def get_periods(self, tzid, end=None): + def get_periods(self, tzid, end=None, inclusive=False): """ Return periods defined by this object, employing the given 'tzid' where @@ -228,9 +247,34 @@ If 'end' is omitted, only explicit recurrences and recurrences from explicitly-terminated rules will be returned. + + If 'inclusive' is set to a true value, any period occurring at the 'end' + will be included. + """ + + return get_periods(self, tzid, end, inclusive) + + def has_period(self, tzid, period): + + """ + Return whether this object, employing the given 'tzid' where no time + zone information is defined, has the given 'period'. """ - return get_periods(self, tzid, end) + return period in self.get_periods(tzid, period.get_start_point(), inclusive=True) + + def has_recurrence(self, tzid, recurrenceid): + + """ + Return whether this object, employing the given 'tzid' where no time + zone information is defined, has the given 'recurrenceid'. + """ + + start_point = self.get_recurrence_start_point(recurrenceid, tzid) + for p in self.get_periods(tzid, start_point, inclusive=True): + if p.get_start_point() == start_point: + return True + return False def get_active_periods(self, recurrenceids, tzid, end=None): diff -r 3cb8c82d705c -r 94f66de18a17 imiptools/handlers/common.py --- a/imiptools/handlers/common.py Mon Apr 18 21:07:41 2016 +0200 +++ b/imiptools/handlers/common.py Tue Apr 19 00:53:59 2016 +0200 @@ -143,4 +143,35 @@ self.add_result("REFRESH", [get_address(organiser)], obj.to_part("REFRESH")) + def ensure_occurrence(self): + + """ + Ensure that the object originating from an attendee corresponds to an + existing occurrence of an event, creating or reviving a specific + recurrence if necessary. + + Return whether a valid occurrence was found. + """ + + # Obtain any stored object. + + obj = self.get_stored_object_version() + + # Handle any newly-defined occurrence. + + if not obj: + + # Check for a valid occurrence. + + if not self.is_recurrence(): + return False + + # Set the complete event if not an additional occurrence. For any newly- + # indicated occurrence, use the received event details. + + self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) + self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) + + return True + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 3cb8c82d705c -r 94f66de18a17 imiptools/handlers/person.py --- a/imiptools/handlers/person.py Mon Apr 18 21:07:41 2016 +0200 +++ b/imiptools/handlers/person.py Tue Apr 19 00:53:59 2016 +0200 @@ -3,7 +3,7 @@ """ Handlers for a person for whom scheduling is performed. -Copyright (C) 2014, 2015 Paul Boddie +Copyright (C) 2014, 2015, 2016 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -205,6 +205,11 @@ "As organiser, update attendance from valid attendees." + if not self.ensure_occurrence(): + return False + + # Merge the attendance for the received object. + if self.merge_attendance(attendees): self.update_freebusy_from_attendees(attendees) diff -r 3cb8c82d705c -r 94f66de18a17 imiptools/handlers/person_outgoing.py --- a/imiptools/handlers/person_outgoing.py Mon Apr 18 21:07:41 2016 +0200 +++ b/imiptools/handlers/person_outgoing.py Tue Apr 19 00:53:59 2016 +0200 @@ -4,7 +4,7 @@ Handlers for a person for whom scheduling is performed, inspecting outgoing messages to obtain scheduling done externally. -Copyright (C) 2014, 2015 Paul Boddie +Copyright (C) 2014, 2015, 2016 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -120,6 +120,9 @@ self.store.remove_cancellation(self.user, self.uid, self.recurrenceid) else: + if not self.ensure_occurrence(): + return False + # Obtain valid attendees, merging their attendance with the stored # object. diff -r 3cb8c82d705c -r 94f66de18a17 tests/templates/event-request-person-recurring-rdate.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-person-recurring-rdate.txt Tue Apr 19 00:53:59 2016 +0200 @@ -0,0 +1,36 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: vincent.vole@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:vincent.vole@example.com +ATTENDEE;RSVP=TRUE:mailto:paul.boddie@example.com +DTSTAMP:20141009T182400Z +DTSTART;TZID=Europe/Oslo:20141010T100000 +DTEND;TZID=Europe/Oslo:20141010T110000 +RDATE;TZID=Europe/Oslo;VALUE=PERIOD:20141011T100000/20141011T110000 +SUMMARY:Recurring event +UID:event26@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r 3cb8c82d705c -r 94f66de18a17 tests/test_handle.py --- a/tests/test_handle.py Mon Apr 18 21:07:41 2016 +0200 +++ b/tests/test_handle.py Tue Apr 19 00:53:59 2016 +0200 @@ -25,6 +25,7 @@ from imiptools.mail import Messenger from imiptools.period import RecurringPeriod from imiptools.stores import get_store, get_journal +from os.path import split import sys class TestClient(ClientForObject): @@ -36,7 +37,7 @@ # Action methods. - def handle_request(self, action, start=None, end=None): + def handle_request(self, action, start=None, end=None, recurrenceid=None): """ Process the current request for the current user. Return whether the @@ -44,8 +45,16 @@ If 'start' and 'end' are specified, they will be used in any counter-proposal. + + Where 'recurrenceid' is specified and refers to a new recurrence, the + action will apply only to this new recurrence. """ + have_new_recurrence = self.obj.get_recurrenceid() != recurrenceid + + if have_new_recurrence: + self.obj["RECURRENCE-ID"] = [(recurrenceid, {})] + # Reply only on behalf of this user. if action in ("accept", "decline"): @@ -66,9 +75,14 @@ period = RecurringPeriod(start, end, period.tzid, period.origin, period.get_start_attr(), period.get_end_attr()) self.obj.set_period(period) method = "COUNTER" + + # Nothing else is supported. + else: return None + # Where no attendees remain, no message is generated. + if not attendee_attr: return None @@ -93,6 +107,8 @@ # response message to standard output. if __name__ == "__main__": + progname = split(sys.argv[0])[-1] + try: action, store_type, store_dir, journal_dir, preferences_dir, user = sys.argv[1:7] if action == "counter": @@ -104,13 +120,18 @@ uid, recurrenceid = (sys.argv[i:i+2] + [None] * 2)[:2] except ValueError: print >>sys.stderr, """\ +Usage: %s + [ ] + + Need 'accept', 'counter' or 'decline', a store type, a store directory, a journal directory, a preferences directory, user URI, any counter-proposal datetimes (see below), plus the appropriate event UID and RECURRENCE-ID (if a recurrence is involved). The RECURRENCE-ID must be in exactly the form employed by the store, not a -different but equivalent representation. +different but equivalent representation, if the identifier is to refer to an +existing recurrence. Alternatively, omit the UID and RECURRENCE-ID and provide event-only details on standard input to force the script to handle an event not already present in the @@ -127,6 +148,11 @@ if uid is not None: fragment = store.get_event(user, uid, recurrenceid) + # Permit new recurrences by getting the parent object. + + if not fragment: + fragment = store.get_event(user, uid) + if not fragment: print >>sys.stderr, "No such event:", uid, recurrenceid sys.exit(1) @@ -135,7 +161,7 @@ obj = Object(fragment) handler = TestClient(obj, user, Messenger(), store, None, journal, preferences_dir) - response = handler.handle_request(action, start, end) + response = handler.handle_request(action, start, end, recurrenceid) if response: if uid is not None: diff -r 3cb8c82d705c -r 94f66de18a17 tests/test_person_invitation_decline_instance.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_person_invitation_decline_instance.sh Tue Apr 19 00:53:59 2016 +0200 @@ -0,0 +1,139 @@ +#!/bin/sh + +. "`dirname \"$0\"`/common.sh" + +USER="mailto:vincent.vole@example.com" +SENDER="mailto:paul.boddie@example.com" + +mkdir -p "$PREFS/$USER" +echo 'Europe/Oslo' > "$PREFS/$USER/TZID" +echo 'share' > "$PREFS/$USER/freebusy_sharing" + +mkdir -p "$PREFS/$SENDER" +echo 'Europe/Oslo' > "$PREFS/$USER/TZID" + +# Test free/busy responses. + + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/fb-request-person-all.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out0.tmp + + grep -q 'METHOD:REPLY' out0.tmp \ +&& ! grep -q '^FREEBUSY' out0.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/fb-request-person.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out1.tmp + + grep -q 'METHOD:REPLY' out1.tmp \ +&& ! grep -q '^FREEBUSY' out1.tmp \ +&& echo "Success" \ +|| echo "Failed" + +# Publish an event, testing registration in the outgoing handler. + +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-rdate.txt" 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \ +> out1f.tmp + + grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out1f.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Test registration in the incoming handler for the recipient. + + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-rdate.txt" 2>> $ERROR \ +| "$SHOWMAIL" \ +> out2.tmp + + ! grep -q 'METHOD:REPLY' out2.tmp \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \ +> out2f.tmp + + ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2f.tmp" \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \ +> out2o.tmp + + grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2o.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Test acceptance and registration in the outgoing handler. + + "$ACCEPT_SCRIPT" $ACCEPT_ARGS "$USER" "event26@example.com" 2>> $ERROR \ +| tee out3.tmp \ +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \ +> out3f.tmp + + grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out3f.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Test registration in the incoming handler. + + "$PERSON_SCRIPT" $ARGS < out3.tmp 2>> $ERROR \ +| "$SHOWMAIL" \ +> out4.tmp + + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \ +> out4f.tmp + + [ `grep "event26@example.com" "out4f.tmp" | wc -l` = '2' ] \ +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4f.tmp" \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \ +> out4o.tmp + + [ `grep "event26@example.com" "out4o.tmp" | wc -l` = '2' ] \ +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4o.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Test recurrence declination in the outgoing handler. + + "$DECLINE_SCRIPT" $DECLINE_ARGS "$USER" "event26@example.com" "20141011T100000" 2>> $ERROR \ +| tee out5.tmp \ +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR + + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \ +> out5s.tmp + + [ `grep "event26@example.com" "out5s.tmp" | wc -l` = '1' ] \ +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out5s.tmp" \ +&& echo "Success" \ +|| echo "Failed" + +# Test declination in the incoming handler. + + "$PERSON_SCRIPT" $ARGS < out5.tmp 2>> $ERROR \ +| "$SHOWMAIL" \ +> out6.tmp + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \ +> out6f.tmp + + [ `grep "event26@example.com" "out6f.tmp" | wc -l` = '2' ] \ +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6f.tmp" \ +&& echo "Success" \ +|| echo "Failed" + + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \ +> out6o.tmp + + [ `grep "event26@example.com" "out6o.tmp" | wc -l` = '1' ] \ +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6o.tmp" \ +&& echo "Success" \ +|| echo "Failed"