summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2015-02-16 23:24:29 +0100
committerThomas Bruederli <bruederli@kolabsys.com>2015-02-16 23:24:29 +0100
commitbe1851eab49b61e958840fb254f8460fd331efaa (patch)
treed1a2ad548968328a35cc726c7d79802028e3a952
parent2eaa92b373c72e677bd72ff2a721c0e0cdcae86d (diff)
downloadpykolab-be1851eab49b61e958840fb254f8460fd331efaa.tar.gz
Add support for handling recurrence exceptions to event object wrapper (#4552)
-rw-r--r--pykolab/xml/event.py80
-rw-r--r--tests/unit/test-003-event.py139
2 files changed, 211 insertions, 8 deletions
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 78a26dd..c061bcb 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -121,6 +121,7 @@ class Event(object):
def __init__(self, from_ical="", from_string=""):
self._attendees = []
self._categories = []
+ self._exceptions = []
self._attachment_parts = []
if isinstance(from_ical, str) and from_ical == "":
@@ -129,6 +130,7 @@ class Event(object):
else:
self.event = kolabformat.readEvent(from_string, False)
self._load_attendees()
+ self._load_exceptions()
else:
self.from_ical(from_ical, from_string)
@@ -140,6 +142,14 @@ class Event(object):
att.copy_from(a)
self._attendees.append(att)
+ def _load_exceptions(self):
+ for ex in self.event.exceptions():
+ exception = Event()
+ exception.uid = ex.uid()
+ exception.event = ex
+ exception._load_attendees()
+ self._exceptions.append(exception)
+
def add_attendee(self, email, name=None, rsvp=False, role=None, participant_status=None, cutype="INDIVIDUAL", params=None):
attendee = Attendee(email, name, rsvp, role, participant_status, cutype, params)
self._attendees.append(attendee)
@@ -166,6 +176,31 @@ class Event(object):
self.event.addExceptionDate(xmlutils.to_cdatetime(_datetime, True))
+ def add_exception(self, exception):
+ # sanity checks
+ if not self.is_recurring():
+ raise EventIntegrityError, "Cannot add exceptions to a non-recurring event"
+
+ recurrence_id = exception.get_recurrence_id()
+ if recurrence_id is None:
+ raise EventIntegrityError, "Recurrence exceptions require a Recurrence-ID property"
+
+ # check if an exception with the given recurrence-id already exists
+ append = True
+ vexceptions = self.event.exceptions()
+ for i, ex in enumerate(self._exceptions):
+ if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture:
+ # update the existing exception
+ vexceptions[i] = exception.event
+ self._exceptions[i] = exception
+ append = False
+
+ if append:
+ vexceptions.append(exception.event)
+ self._exceptions.append(exception)
+
+ self.event.setExceptions(vexceptions)
+
def as_string_itip(self, method="REQUEST"):
cal = icalendar.Calendar()
cal.add(
@@ -264,7 +299,7 @@ class Event(object):
self.event.setAttendees(self._attendees)
def from_ical(self, ical, raw=None):
- if isinstance(ical, icalendar.Event) or isinstance(ical_event, icalendar.Calendar):
+ if isinstance(ical, icalendar.Event) or isinstance(ical, icalendar.Calendar):
ical_event = ical
elif hasattr(icalendar.Event, 'from_ical'):
ical_event = icalendar.Event.from_ical(ical)
@@ -309,6 +344,7 @@ class Event(object):
from kolab.calendaring import EventCal
self.event = EventCal()
self.event.fromICal(ical)
+ self._load_exceptions()
def get_attendee_participant_status(self, attendee):
return attendee.get_participant_status()
@@ -423,6 +459,9 @@ class Event(object):
def get_exception_dates(self):
return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates())
+ def get_exceptions(self):
+ return self._exceptions
+
def get_attachments(self):
return self.event.attachments()
@@ -1245,18 +1284,53 @@ class Event(object):
if next_start:
instance = Event(from_string=str(self))
instance.set_start(next_start)
- instance.set_recurrence(kolabformat.RecurrenceRule()) # remove recurrence rules
instance.event.setRecurrenceID(xmlutils.to_cdatetime(next_start), False)
next_end = self.get_occurence_end_date(next_start)
if next_end:
instance.set_end(next_end)
- # TODO: copy data from matching exception
+ # unset recurrence rule and exceptions
+ instance.set_recurrence(kolabformat.RecurrenceRule())
+ instance.event.setExceptions(kolabformat.vectorevent())
+ instance.event.setExceptionDates(kolabformat.vectordatetime())
+ instance._exceptions = []
+ instance._isexception = False
+
+ # copy data from matching exception
+ # (give precedence to single occurrence exceptions over thisandfuture)
+ for exception in self._exceptions:
+ recurrence_id = exception.get_recurrence_id()
+ if recurrence_id == next_start and (not exception.thisandfuture or not instance._isexception):
+ instance = exception
+ instance._isexception = True
+ if not exception.thisandfuture:
+ break
+ elif exception.thisandfuture and next_start > recurrence_id:
+ # TODO: merge exception properties over this instance + adjust start/end with the according offset
+ pass
return instance
return None
+ def get_instance(self, _datetime):
+ # If no timezone information is given, use the one from event start
+ if _datetime.tzinfo == None:
+ _start = self.get_start()
+ _datetime = _datetime.replace(tzinfo=_start.tzinfo)
+
+ instance = self.get_next_instance(_datetime - datetime.timedelta(days=1))
+ while instance:
+ recurrence_id = instance.get_recurrence_id()
+ if type(recurrence_id) == type(_datetime) and recurrence_id <= _datetime:
+ if recurrence_id == _datetime:
+ return instance
+ instance = self.get_next_instance(instance.get_start())
+ else:
+ break
+
+ return None
+
def _recurrence_end(self):
"""
Determine a reasonable end date for infinitely recurring events
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 876dd57..10e6abd 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -25,7 +25,6 @@ UID:7a35527d-f783-4b58-b404-b1389bd2fc57
DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
CREATED;VALUE=DATE-TIME:20140407T122245Z
LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z
-RECURRENCE-ID;TZID=Europe/Zurich;RANGE=THISANDFUTURE:20140523T110000
DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000
DURATION:PT1H30M0S
RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
@@ -70,6 +69,25 @@ END:VALARM
END:VEVENT
"""
+ical_exception = """
+BEGIN:VEVENT
+UID:7a35527d-f783-4b58-b404-b1389bd2fc57
+DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
+CREATED;VALUE=DATE-TIME:20140407T122245Z
+LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z
+RECURRENCE-ID;TZID=Europe/Zurich;RANGE=THISANDFUTURE:20140606T110000
+DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140607T120000
+DTEND;TZID=Europe/Zurich;VALUE=DATE-TIME:20140607T143000
+SUMMARY:Exception
+CATEGORIES:Personal
+TRANSP:TRANSPARENT
+PRIORITY:2
+SEQUENCE:3
+STATUS:CANCELLED
+ORGANIZER;CN=Doe\, John:mailto:john.doe@example.org
+END:VEVENT
+"""
+
xml_event = """
<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
<vcalendar>
@@ -120,7 +138,7 @@ xml_event = """
<recur>
<freq>DAILY</freq>
<until>
- <date>2014-07-25</date>
+ <date>2015-07-25</date>
</until>
</recur>
</rrule>
@@ -242,6 +260,72 @@ xml_event = """
</valarm>
</components>
</vevent>
+ <vevent>
+ <properties>
+ <uid>
+ <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text>
+ </uid>
+ <created>
+ <date-time>2014-07-07T01:28:23Z</date-time>
+ </created>
+ <dtstamp>
+ <date-time>2014-07-07T01:28:23Z</date-time>
+ </dtstamp>
+ <sequence>
+ <integer>2</integer>
+ </sequence>
+ <class>
+ <text>PUBLIC</text>
+ </class>
+ <dtstart>
+ <parameters>
+ <tzid>
+ <text>/kolab.org/Europe/London</text>
+ </tzid>
+ </parameters>
+ <date-time>2014-08-16T13:00:00</date-time>
+ </dtstart>
+ <dtend>
+ <parameters>
+ <tzid><text>/kolab.org/Europe/London</text></tzid>
+ </parameters>
+ <date-time>2014-08-16T16:00:00</date-time>
+ </dtend>
+ <recurrence-id>
+ <parameters>
+ <tzid>
+ <text>/kolab.org/Europe/London</text>
+ </tzid>
+ <range>
+ <text>THISANDFUTURE</text>
+ </range>
+ </parameters>
+ <date-time>2014-08-16T10:00:00</date-time>
+ </recurrence-id>
+ <summary>
+ <text>exception</text>
+ </summary>
+ <description>
+ <text>exception</text>
+ </description>
+ <location>
+ <text>Room 101</text>
+ </location>
+ <organizer>
+ <parameters>
+ <cn><text>Doe, John</text></cn>
+ </parameters>
+ <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address>
+ </organizer>
+ <attendee>
+ <parameters>
+ <partstat><text>DECLINED</text></partstat>
+ <role><text>REQ-PARTICIPANT</text></role>
+ </parameters>
+ <cal-address>mailto:%3Cjane%40example.org%3E</cal-address>
+ </attendee>
+ </properties>
+ </vevent>
</components>
</vcalendar>
</icalendar>
@@ -390,8 +474,14 @@ METHOD:REQUEST
self.assertIsInstance(event.get_exception_dates()[0], datetime.datetime)
self.assertEqual(len(event.get_alarms()), 1)
self.assertEqual(len(event.get_attachments()), 2)
- self.assertIsInstance(event.get_recurrence_id(), datetime.datetime)
- self.assertEqual(event.thisandfuture, True)
+
+ # TODO: load ical_exception with main event
+ #self.assertEqual(len(event.get_exceptions()), 1)
+
+ exception = event_from_ical(ical_exception)
+ self.assertIsInstance(exception.get_recurrence_id(), datetime.datetime)
+ self.assertEqual(exception.thisandfuture, True)
+ self.assertEqual(str(exception.get_start()), "2014-06-07 12:00:00+02:00")
def test_018_ical_to_message(self):
event = event_from_ical(ical_event)
@@ -561,6 +651,23 @@ END:VEVENT
self.assertEqual(self.event.get_next_occurence(_start), None)
self.assertEqual(self.event.get_last_occurrence(), None)
+ def test_021_add_exceptions(self):
+ event = event_from_ical(ical_event)
+ exception = event_from_ical(ical_exception)
+ self.assertIsInstance(event, Event)
+ self.assertIsInstance(exception, Event)
+
+ event.add_exception(exception)
+ self.assertEquals(len(event.get_exceptions()), 1)
+
+ # second call shall replace the existing exception
+ event.add_exception(exception)
+ self.assertEquals(len(event.get_exceptions()), 1)
+
+ # first real occurrence should be our exception
+ occurrence = event.get_next_instance(event.get_start())
+ self.assertEqual(occurrence.get_summary(), "Exception")
+
def test_022_load_from_xml(self):
event = event_from_string(xml_event)
self.assertEqual(event.uid, '75c740bb-b3c6-442c-8021-ecbaeb0a025e')
@@ -569,7 +676,29 @@ END:VEVENT
self.assertEqual(len(event.get_attendee_by_email("somebody@else.com").get_delegated_to()), 1)
self.assertEqual(event.get_sequence(), 1)
self.assertIsInstance(event.get_start(), datetime.datetime)
- self.assertEqual(str(event.get_start()), "2014-08-13 10:00:00+00:00")
+ self.assertEqual(str(event.get_start()), "2014-08-13 10:00:00+01:00")
+ self.assertTrue(event.is_recurring())
+
+ exceptions = event.get_exceptions()
+ self.assertEqual(len(exceptions), 1)
+
+ exception = exceptions[0]
+ self.assertIsInstance(exception.get_recurrence_id(), datetime.datetime)
+ self.assertTrue(exception.thisandfuture)
+ self.assertEqual(str(exception.get_start()), "2014-08-16 13:00:00+01:00")
+ self.assertEqual(exception.get_attendee_by_email("jane@example.org").get_participant_status(), kolabformat.PartDeclined)
+ self.assertRaises(ValueError, exception.get_attendee, "somebody@else.com")
+
+ # get instances with exception data
+ occurrence = event.get_next_instance(exception.get_start() - datetime.timedelta(days=1))
+ self.assertEqual(occurrence.get_start(), exception.get_start())
+ self.assertEqual(occurrence.get_summary(), "exception")
+
+ # find instance directly by date
+ _recurrence_id = datetime.datetime(2014, 8, 15, 10, 0, 0)
+ occurrence = event.get_instance(_recurrence_id)
+ self.assertIsInstance(occurrence, Event)
+ self.assertEqual(str(occurrence.get_recurrence_id()), "2014-08-15 10:00:00+01:00")
def test_023_load_from_message(self):
event = event_from_message(event_from_ical(ical_event).to_message())