1.1 --- a/imiptools/client.py Mon Apr 18 21:07:41 2016 +0200
1.2 +++ b/imiptools/client.py Tue Apr 19 00:53:59 2016 +0200
1.3 @@ -385,6 +385,13 @@
1.4
1.5 return get_uri(self.obj.get_value("ORGANIZER")) == self.user
1.6
1.7 + def is_recurrence(self):
1.8 +
1.9 + "Return whether the current object is a recurrence of its parent."
1.10 +
1.11 + parent = self.get_parent_object()
1.12 + return parent and parent.has_recurrence(self.get_tzid(), self.obj.get_recurrenceid())
1.13 +
1.14 # Common operations on calendar data.
1.15
1.16 def update_senders(self, obj=None):
2.1 --- a/imiptools/data.py Mon Apr 18 21:07:41 2016 +0200
2.2 +++ b/imiptools/data.py Tue Apr 19 00:53:59 2016 +0200
2.3 @@ -44,6 +44,25 @@
2.4 "Access to calendar structures."
2.5
2.6 def __init__(self, fragment):
2.7 +
2.8 + """
2.9 + Initialise the object with the given 'fragment'. This must be a
2.10 + dictionary mapping an object type (such as "VEVENT") to a tuple
2.11 + containing the object details and attributes, each being a dictionary
2.12 + itself.
2.13 +
2.14 + The result of parse_object can be processed to obtain a fragment by
2.15 + obtaining a collection of records for an object type. For example:
2.16 +
2.17 + l = parse_object(f, encoding, "VCALENDAR")
2.18 + events = l["VEVENT"]
2.19 + event = events[0]
2.20 +
2.21 + Then, the specific object must be presented as follows:
2.22 +
2.23 + object = Object({"VEVENT" : event})
2.24 + """
2.25 +
2.26 self.objtype, (self.details, self.attr) = fragment.items()[0]
2.27
2.28 def get_uid(self):
2.29 @@ -219,7 +238,7 @@
2.30
2.31 return (dtstart, dtstart_attr), (dtend, dtend_attr)
2.32
2.33 - def get_periods(self, tzid, end=None):
2.34 + def get_periods(self, tzid, end=None, inclusive=False):
2.35
2.36 """
2.37 Return periods defined by this object, employing the given 'tzid' where
2.38 @@ -228,9 +247,32 @@
2.39
2.40 If 'end' is omitted, only explicit recurrences and recurrences from
2.41 explicitly-terminated rules will be returned.
2.42 +
2.43 + If 'inclusive' is set to a true value, any period occurring at the 'end'
2.44 + will be included.
2.45 """
2.46
2.47 - return get_periods(self, tzid, end)
2.48 + return get_periods(self, tzid, end, inclusive)
2.49 +
2.50 + def has_period(self, tzid, period):
2.51 +
2.52 + """
2.53 + Return whether this object, employing the given 'tzid' where no time
2.54 + zone information is defined, has the given 'period'.
2.55 + """
2.56 +
2.57 + return period in self.get_periods(tzid, period.get_start_point(), inclusive=True)
2.58 +
2.59 + def has_recurrence(self, tzid, recurrenceid):
2.60 +
2.61 + """
2.62 + Return whether this object, employing the given 'tzid' where no time
2.63 + zone information is defined, has the given 'recurrenceid'.
2.64 + """
2.65 +
2.66 + start_point = self.get_recurrence_start_point(recurrenceid, tzid)
2.67 + return [(p.get_start_point() == start_point)
2.68 + for p in self.get_periods(tzid, start_point, inclusive=True)]
2.69
2.70 def get_active_periods(self, recurrenceids, tzid, end=None):
2.71
3.1 --- a/imiptools/handlers/common.py Mon Apr 18 21:07:41 2016 +0200
3.2 +++ b/imiptools/handlers/common.py Tue Apr 19 00:53:59 2016 +0200
3.3 @@ -143,4 +143,35 @@
3.4
3.5 self.add_result("REFRESH", [get_address(organiser)], obj.to_part("REFRESH"))
3.6
3.7 + def ensure_occurrence(self):
3.8 +
3.9 + """
3.10 + Ensure that the object originating from an attendee corresponds to an
3.11 + existing occurrence of an event, creating or reviving a specific
3.12 + recurrence if necessary.
3.13 +
3.14 + Return whether a valid occurrence was found.
3.15 + """
3.16 +
3.17 + # Obtain any stored object.
3.18 +
3.19 + obj = self.get_stored_object_version()
3.20 +
3.21 + # Handle any newly-defined occurrence.
3.22 +
3.23 + if not obj:
3.24 +
3.25 + # Check for a valid occurrence.
3.26 +
3.27 + if not self.is_recurrence():
3.28 + return False
3.29 +
3.30 + # Set the complete event if not an additional occurrence. For any newly-
3.31 + # indicated occurrence, use the received event details.
3.32 +
3.33 + self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
3.34 + self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
3.35 +
3.36 + return True
3.37 +
3.38 # vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/imiptools/handlers/person.py Mon Apr 18 21:07:41 2016 +0200
4.2 +++ b/imiptools/handlers/person.py Tue Apr 19 00:53:59 2016 +0200
4.3 @@ -205,6 +205,11 @@
4.4
4.5 "As organiser, update attendance from valid attendees."
4.6
4.7 + if not self.ensure_occurrence():
4.8 + return False
4.9 +
4.10 + # Merge the attendance for the received object.
4.11 +
4.12 if self.merge_attendance(attendees):
4.13 self.update_freebusy_from_attendees(attendees)
4.14
5.1 --- a/imiptools/handlers/person_outgoing.py Mon Apr 18 21:07:41 2016 +0200
5.2 +++ b/imiptools/handlers/person_outgoing.py Tue Apr 19 00:53:59 2016 +0200
5.3 @@ -120,6 +120,9 @@
5.4 self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
5.5
5.6 else:
5.7 + if not self.ensure_occurrence():
5.8 + return False
5.9 +
5.10 # Obtain valid attendees, merging their attendance with the stored
5.11 # object.
5.12
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
6.2 +++ b/tests/templates/event-request-person-recurring-rdate.txt Tue Apr 19 00:53:59 2016 +0200
6.3 @@ -0,0 +1,36 @@
6.4 +Content-Type: multipart/alternative; boundary="===============0047278175=="
6.5 +MIME-Version: 1.0
6.6 +From: paul.boddie@example.com
6.7 +To: vincent.vole@example.com
6.8 +Subject: Invitation!
6.9 +
6.10 +--===============0047278175==
6.11 +Content-Type: text/plain; charset="us-ascii"
6.12 +MIME-Version: 1.0
6.13 +Content-Transfer-Encoding: 7bit
6.14 +
6.15 +This message contains an event.
6.16 +
6.17 +--===============0047278175==
6.18 +MIME-Version: 1.0
6.19 +Content-Transfer-Encoding: 7bit
6.20 +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST"
6.21 +
6.22 +BEGIN:VCALENDAR
6.23 +PRODID:-//imip-agent/test//EN
6.24 +METHOD:REQUEST
6.25 +VERSION:2.0
6.26 +BEGIN:VEVENT
6.27 +ORGANIZER:mailto:paul.boddie@example.com
6.28 +ATTENDEE;RSVP=TRUE:mailto:vincent.vole@example.com
6.29 +ATTENDEE;RSVP=TRUE:mailto:paul.boddie@example.com
6.30 +DTSTAMP:20141009T182400Z
6.31 +DTSTART;TZID=Europe/Oslo:20141010T100000
6.32 +DTEND;TZID=Europe/Oslo:20141010T110000
6.33 +RDATE;TZID=Europe/Oslo;VALUE=PERIOD:20141011T100000/20141011T110000
6.34 +SUMMARY:Recurring event
6.35 +UID:event26@example.com
6.36 +END:VEVENT
6.37 +END:VCALENDAR
6.38 +
6.39 +--===============0047278175==--
7.1 --- a/tests/test_handle.py Mon Apr 18 21:07:41 2016 +0200
7.2 +++ b/tests/test_handle.py Tue Apr 19 00:53:59 2016 +0200
7.3 @@ -25,6 +25,7 @@
7.4 from imiptools.mail import Messenger
7.5 from imiptools.period import RecurringPeriod
7.6 from imiptools.stores import get_store, get_journal
7.7 +from os.path import split
7.8 import sys
7.9
7.10 class TestClient(ClientForObject):
7.11 @@ -36,7 +37,7 @@
7.12
7.13 # Action methods.
7.14
7.15 - def handle_request(self, action, start=None, end=None):
7.16 + def handle_request(self, action, start=None, end=None, recurrenceid=None):
7.17
7.18 """
7.19 Process the current request for the current user. Return whether the
7.20 @@ -44,8 +45,16 @@
7.21
7.22 If 'start' and 'end' are specified, they will be used in any
7.23 counter-proposal.
7.24 +
7.25 + Where 'recurrenceid' is specified and refers to a new recurrence, the
7.26 + action will apply only to this new recurrence.
7.27 """
7.28
7.29 + have_new_recurrence = self.obj.get_recurrenceid() != recurrenceid
7.30 +
7.31 + if have_new_recurrence:
7.32 + self.obj["RECURRENCE-ID"] = [(recurrenceid, {})]
7.33 +
7.34 # Reply only on behalf of this user.
7.35
7.36 if action in ("accept", "decline"):
7.37 @@ -66,9 +75,14 @@
7.38 period = RecurringPeriod(start, end, period.tzid, period.origin, period.get_start_attr(), period.get_end_attr())
7.39 self.obj.set_period(period)
7.40 method = "COUNTER"
7.41 +
7.42 + # Nothing else is supported.
7.43 +
7.44 else:
7.45 return None
7.46
7.47 + # Where no attendees remain, no message is generated.
7.48 +
7.49 if not attendee_attr:
7.50 return None
7.51
7.52 @@ -93,6 +107,8 @@
7.53 # response message to standard output.
7.54
7.55 if __name__ == "__main__":
7.56 + progname = split(sys.argv[0])[-1]
7.57 +
7.58 try:
7.59 action, store_type, store_dir, journal_dir, preferences_dir, user = sys.argv[1:7]
7.60 if action == "counter":
7.61 @@ -104,13 +120,18 @@
7.62 uid, recurrenceid = (sys.argv[i:i+2] + [None] * 2)[:2]
7.63 except ValueError:
7.64 print >>sys.stderr, """\
7.65 +Usage: %s <action> <store type> <store directory> <journal directory>
7.66 + <preferences directory> <user URI> [ <start> <end> ]
7.67 + <uid> <recurrence-id>
7.68 +
7.69 Need 'accept', 'counter' or 'decline', a store type, a store directory, a
7.70 journal directory, a preferences directory, user URI, any counter-proposal
7.71 datetimes (see below), plus the appropriate event UID and RECURRENCE-ID (if a
7.72 recurrence is involved).
7.73
7.74 The RECURRENCE-ID must be in exactly the form employed by the store, not a
7.75 -different but equivalent representation.
7.76 +different but equivalent representation, if the identifier is to refer to an
7.77 +existing recurrence.
7.78
7.79 Alternatively, omit the UID and RECURRENCE-ID and provide event-only details on
7.80 standard input to force the script to handle an event not already present in the
7.81 @@ -127,6 +148,11 @@
7.82 if uid is not None:
7.83 fragment = store.get_event(user, uid, recurrenceid)
7.84
7.85 + # Permit new recurrences by getting the parent object.
7.86 +
7.87 + if not fragment:
7.88 + fragment = store.get_event(user, uid)
7.89 +
7.90 if not fragment:
7.91 print >>sys.stderr, "No such event:", uid, recurrenceid
7.92 sys.exit(1)
7.93 @@ -135,7 +161,7 @@
7.94
7.95 obj = Object(fragment)
7.96 handler = TestClient(obj, user, Messenger(), store, None, journal, preferences_dir)
7.97 - response = handler.handle_request(action, start, end)
7.98 + response = handler.handle_request(action, start, end, recurrenceid)
7.99
7.100 if response:
7.101 if uid is not None:
8.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
8.2 +++ b/tests/test_person_invitation_decline_instance.sh Tue Apr 19 00:53:59 2016 +0200
8.3 @@ -0,0 +1,139 @@
8.4 +#!/bin/sh
8.5 +
8.6 +. "`dirname \"$0\"`/common.sh"
8.7 +
8.8 +USER="mailto:vincent.vole@example.com"
8.9 +SENDER="mailto:paul.boddie@example.com"
8.10 +
8.11 +mkdir -p "$PREFS/$USER"
8.12 +echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
8.13 +echo 'share' > "$PREFS/$USER/freebusy_sharing"
8.14 +
8.15 +mkdir -p "$PREFS/$SENDER"
8.16 +echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
8.17 +
8.18 +# Test free/busy responses.
8.19 +
8.20 + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/fb-request-person-all.txt" 2>> $ERROR \
8.21 +| "$SHOWMAIL" \
8.22 +> out0.tmp
8.23 +
8.24 + grep -q 'METHOD:REPLY' out0.tmp \
8.25 +&& ! grep -q '^FREEBUSY' out0.tmp \
8.26 +&& echo "Success" \
8.27 +|| echo "Failed"
8.28 +
8.29 + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/fb-request-person.txt" 2>> $ERROR \
8.30 +| "$SHOWMAIL" \
8.31 +> out1.tmp
8.32 +
8.33 + grep -q 'METHOD:REPLY' out1.tmp \
8.34 +&& ! grep -q '^FREEBUSY' out1.tmp \
8.35 +&& echo "Success" \
8.36 +|| echo "Failed"
8.37 +
8.38 +# Publish an event, testing registration in the outgoing handler.
8.39 +
8.40 +"$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-rdate.txt" 2>> $ERROR
8.41 +
8.42 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
8.43 +> out1f.tmp
8.44 +
8.45 + grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out1f.tmp" \
8.46 +&& echo "Success" \
8.47 +|| echo "Failed"
8.48 +
8.49 +# Test registration in the incoming handler for the recipient.
8.50 +
8.51 + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-request-person-recurring-rdate.txt" 2>> $ERROR \
8.52 +| "$SHOWMAIL" \
8.53 +> out2.tmp
8.54 +
8.55 + ! grep -q 'METHOD:REPLY' out2.tmp \
8.56 +&& echo "Success" \
8.57 +|| echo "Failed"
8.58 +
8.59 + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
8.60 +> out2f.tmp
8.61 +
8.62 + ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2f.tmp" \
8.63 +&& echo "Success" \
8.64 +|| echo "Failed"
8.65 +
8.66 + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
8.67 +> out2o.tmp
8.68 +
8.69 + grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out2o.tmp" \
8.70 +&& echo "Success" \
8.71 +|| echo "Failed"
8.72 +
8.73 +# Test acceptance and registration in the outgoing handler.
8.74 +
8.75 + "$ACCEPT_SCRIPT" $ACCEPT_ARGS "$USER" "event26@example.com" 2>> $ERROR \
8.76 +| tee out3.tmp \
8.77 +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
8.78 +
8.79 + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
8.80 +> out3f.tmp
8.81 +
8.82 + grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out3f.tmp" \
8.83 +&& echo "Success" \
8.84 +|| echo "Failed"
8.85 +
8.86 +# Test registration in the incoming handler.
8.87 +
8.88 + "$PERSON_SCRIPT" $ARGS < out3.tmp 2>> $ERROR \
8.89 +| "$SHOWMAIL" \
8.90 +> out4.tmp
8.91 +
8.92 + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
8.93 +> out4f.tmp
8.94 +
8.95 + [ `grep "event26@example.com" "out4f.tmp" | wc -l` = '2' ] \
8.96 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4f.tmp" \
8.97 +&& echo "Success" \
8.98 +|| echo "Failed"
8.99 +
8.100 + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy_other" "$SENDER" \
8.101 +> out4o.tmp
8.102 +
8.103 + [ `grep "event26@example.com" "out4o.tmp" | wc -l` = '2' ] \
8.104 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out4o.tmp" \
8.105 +&& echo "Success" \
8.106 +|| echo "Failed"
8.107 +
8.108 +# Test recurrence declination in the outgoing handler.
8.109 +
8.110 + "$DECLINE_SCRIPT" $DECLINE_ARGS "$USER" "event26@example.com" "20141011T080000Z" 2>> $ERROR \
8.111 +| tee out5.tmp \
8.112 +| "$OUTGOING_SCRIPT" $ARGS 2>> $ERROR
8.113 +
8.114 + "$LIST_SCRIPT" $LIST_ARGS "$USER" "freebusy" \
8.115 +> out5s.tmp
8.116 +
8.117 + [ `grep "event26@example.com" "out5s.tmp" | wc -l` = '1' ] \
8.118 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out5s.tmp" \
8.119 +&& echo "Success" \
8.120 +|| echo "Failed"
8.121 +
8.122 +# Test declination in the incoming handler.
8.123 +
8.124 + "$PERSON_SCRIPT" $ARGS < out5.tmp 2>> $ERROR \
8.125 +| "$SHOWMAIL" \
8.126 +> out6.tmp
8.127 +
8.128 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy" \
8.129 +> out6f.tmp
8.130 +
8.131 + [ `grep "event26@example.com" "out6f.tmp" | wc -l` = '2' ] \
8.132 +&& grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6f.tmp" \
8.133 +&& echo "Success" \
8.134 +|| echo "Failed"
8.135 +
8.136 + "$LIST_SCRIPT" $LIST_ARGS "$SENDER" "freebusy_other" "$USER" \
8.137 +> out6o.tmp
8.138 +
8.139 + [ `grep "event26@example.com" "out6o.tmp" | wc -l` = '1' ] \
8.140 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "out6o.tmp" \
8.141 +&& echo "Success" \
8.142 +|| echo "Failed"