pykolab/xml tests/unit

Thomas Brüderli bruederli at kolabsys.com
Tue Feb 17 20:10:20 CET 2015


 pykolab/xml/event.py         |   80 +++++++++++++++++++++++-
 tests/unit/test-003-event.py |  139 +++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 211 insertions(+), 8 deletions(-)

New commits:
commit be1851eab49b61e958840fb254f8460fd331efaa
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 16 23:24:29 2015 +0100

    Add support for handling recurrence exceptions to event object wrapper (#4552)

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 at 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 at 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 at example.org").get_participant_status(), kolabformat.PartDeclined)
+        self.assertRaises(ValueError, exception.get_attendee, "somebody at 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())




More information about the commits mailing list