pykolab/xml tests/unit

Thomas Brüderli bruederli at kolabsys.com
Thu Jul 17 19:23:16 CEST 2014


 pykolab/xml/event.py         |  111 ++++++++++++++++++++++++++++---------------
 tests/unit/test-003-event.py |   59 ++++++++++++++++++++--
 2 files changed, 126 insertions(+), 44 deletions(-)

New commits:
commit 0438fc64173d5e68822b7cf922ba807e1cabf95a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 10:21:49 2014 -0400

    Improve iCal import: support all event properties including alarms and attachments. We require full support if wallace directly copies invitations into user calendars

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 7b0c811..fcb3a17 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -31,6 +31,12 @@ class Event(object):
             "CANCELLED": kolabformat.StatusCancelled,
         }
 
+    classification_map = {
+            "PUBLIC": kolabformat.ClassPublic,
+            "PRIVATE": kolabformat.ClassPrivate,
+            "CONFIDENTIAL": kolabformat.ClassConfidential,
+        }
+
     def __init__(self, from_ical="", from_string=""):
         self._attendees = []
         self._categories = []
@@ -56,7 +62,7 @@ class Event(object):
         self.event.setAttendees(self._attendees)
 
     def add_category(self, category):
-        self._categories.append(category)
+        self._categories.append(str(category))
         self.event.setCategories(self._categories)
 
     def add_exception_date(self, _datetime):
@@ -166,12 +172,18 @@ class Event(object):
         self.event.setAttendees(self._attendees)
 
     def from_ical(self, ical):
-        self.event = kolabformat.Event()
         if hasattr(icalendar.Event, 'from_ical'):
             ical_event = icalendar.Event.from_ical(ical)
         elif hasattr(icalendar.Event, 'from_string'):
             ical_event = icalendar.Event.from_string(ical)
 
+        # use the libkolab calendaring bindings to load the full iCal data
+        if ical_event.has_key('RRULE') or ical_event.has_key('ATTACH') \
+             or [part for part in ical_event.walk() if part.name == 'VALARM']:
+            self._xml_from_ical(ical)
+        else:
+            self.event = kolabformat.Event()
+
         # TODO: Clause the timestamps for zulu suffix causing datetime.datetime
         # to fail substitution.
         for attr in list(set(ical_event.required)):
@@ -188,13 +200,10 @@ class Event(object):
             if ical_event.has_key(attr):
                 self.set_from_ical(attr.lower(), ical_event[attr])
 
-        # HACK: use calendaring::EventCal::fromICal() to parse RRULEs
-        if ical_event.has_key('RRULE'):
-            from kolab.calendaring import EventCal
-            event_xml = EventCal()
-            event_xml.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR")
-            self.event.setRecurrenceRule(event_xml.recurrenceRule())
-            self.event.setExceptionDates(event_xml.exceptionDates())
+    def _xml_from_ical(self, ical):
+        from kolab.calendaring import EventCal
+        self.event = EventCal()
+        self.event.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR")
 
     def get_attendee_participant_status(self, attendee):
         return attendee.get_participant_status()
@@ -234,10 +243,10 @@ class Event(object):
         return self._attendees
 
     def get_categories(self):
-        return self.event.categories()
+        return [str(c) for c in self.event.categories()]
 
     def get_classification(self):
-        return self.classification()
+        return self.event.classification()
 
     def get_created(self):
         try:
@@ -273,6 +282,12 @@ class Event(object):
     def get_exception_dates(self):
         return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates())
 
+    def get_attachments(self):
+        return self.event.attachments()
+
+    def get_alarms(self):
+        return self.event.alarms()
+
     def get_ical_attendee(self):
         # TODO: Formatting, aye? See also the example snippet:
         #
@@ -393,6 +408,9 @@ class Event(object):
     def get_ical_sequence(self):
         return str(self.event.sequence()) if self.event.sequence() else None
 
+    def get_location(self):
+        return self.event.location()
+
     def get_lastmodified(self):
         try:
             _datetime = self.event.lastModified()
@@ -433,6 +451,9 @@ class Event(object):
     def get_sequence(self):
         return self.event.sequence()
 
+    def get_url(self):
+        return self.event.url()
+
     def get_transparency(self):
         return self.event.transparency()
 
@@ -449,8 +470,21 @@ class Event(object):
         attendee.set_participant_status(status)
         self.event.setAttendees(self._attendees)
 
+    def set_status(self, status):
+        if status in self.status_map.keys():
+            self.event.setStatus(self.status_map[status])
+        elif status in self.status_map.values():
+            self.event.setStatus(status)
+        else:
+            raise ValueError, _("Invalid status %r") % (status)
+
     def set_classification(self, classification):
-        self.event.setClassification(classification)
+        if classification in self.classification_map.keys():
+            self.event.setClassification(self.classification_map[classification])
+        elif classification in self.classification_map.values():
+            self.event.setClassification(status)
+        else:
+            raise ValueError, _("Invalid classification %r") % (classification)
 
     def set_created(self, _datetime=None):
         if _datetime == None:
@@ -459,7 +493,7 @@ class Event(object):
         self.event.setCreated(xmlutils.to_cdatetime(_datetime, False))
 
     def set_description(self, description):
-        self.event.setDescription(description)
+        self.event.setDescription(str(description))
 
     def set_dtstamp(self, _datetime):
         self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
@@ -486,26 +520,27 @@ class Event(object):
             self.add_exception_date(_datetime)
 
     def set_from_ical(self, attr, value):
+        ical_setter = 'set_ical_' + attr
+        default_setter = 'set_' + attr
+
         if attr == "dtend":
             self.set_ical_dtend(value.dt)
         elif attr == "dtstart":
             self.set_ical_dtstart(value.dt)
-        elif attr == "duration":
-            self.set_ical_duration(value)
-        elif attr == "status":
-            self.set_ical_status(value)
-        elif attr == "summary":
-            self.set_ical_summary(value)
-        elif attr == "priority":
-            self.set_ical_priority(value)
-        elif attr == "sequence":
-            self.set_ical_sequence(value)
-        elif attr == "attendee":
-            self.set_ical_attendee(value)
-        elif attr == "organizer":
-            self.set_ical_organizer(value)
-        elif attr == "uid":
-            self.set_ical_uid(value)
+        elif attr == "dtstamp":
+            self.set_ical_dtstamp(value.dt)
+        elif attr == "created":
+            self.set_created(value.dt)
+        elif attr == "lastmodified":
+            self.set_lastmodified(value.dt)
+        elif attr == "categories":
+            self.add_category(value)
+        elif attr == "class":
+            self.set_classification(value)
+        elif hasattr(self, ical_setter):
+            getattr(self, ical_setter)(value)
+        elif hasattr(self, default_setter):
+            getattr(self, default_setter)(value)
 
     def set_ical_attendee(self, _attendee):
         if isinstance(_attendee, basestring):
@@ -556,6 +591,9 @@ class Event(object):
     def set_ical_dtstart(self, dtstart):
         self.set_start(dtstart)
 
+    def set_ical_lastmodified(self, lastmod):
+        self.set_lastmodified(lastmod)
+
     def set_ical_duration(self, value):
         if value.dt:
             duration = kolabformat.Duration(value.dt.days, 0, 0, value.dt.seconds, False)
@@ -582,14 +620,6 @@ class Event(object):
     def set_ical_sequence(self, sequence):
         self.set_sequence(sequence)
 
-    def set_ical_status(self, status):
-        if status in self.status_map.keys():
-            self.event.setStatus(self.status_map[status])
-        elif status in self.status_map.values():
-            self.event.setStatus(status)
-        else:
-            raise ValueError, _("Invalid status %r") % (status)
-
     def set_ical_summary(self, summary):
         self.set_summary(str(summary))
 
@@ -614,7 +644,7 @@ class Event(object):
         self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
 
     def set_location(self, location):
-        self.event.setLocation(location)
+        self.event.setLocation(str(location))
 
     def set_organizer(self, email, name=None):
         contactreference = ContactReference(email)
@@ -629,6 +659,9 @@ class Event(object):
     def set_sequence(self, sequence):
         self.event.setSequence(int(sequence))
 
+    def set_url(self, url):
+        self.event.setUrl(str(url))
+
     def set_recurrence(self, recurrence):
         self.event.setRecurrenceRule(recurrence)
 
@@ -720,6 +753,8 @@ class Event(object):
 
         part.set_payload(str(self))
 
+        # TODO: extract attachment data to separate MIME parts
+
         part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"')
         part.replace_header('Content-Transfer-Encoding', '8bit')
 
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index a44c4ec..f9ef92e 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -118,25 +118,70 @@ class TestEventXML(unittest.TestCase):
     def test_018_load_from_ical(self):
         ical_str = """BEGIN:VCALENDAR
 VERSION:2.0
-PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+  2.1.3//EN
 CALSCALE:GREGORIAN
+METHOD:REQUEST
 BEGIN:VEVENT
+UID:7a35527d-f783-4b58-b404-b1389bd2fc57
+DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
+CREATED;VALUE=DATE-TIME:20140407T122245Z
+LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z
 DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000
 DURATION:PT1H30M0S
 RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
 EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000
 EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000
-UID:7a35527d-f783-4b58-b404-b1389bd2fc57
-ATTENDEE;CN="Doe, Jane";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED
- ;ROLE=REQ-PARTICIPANT;RSVP=FALSE:MAILTO:jane at doe.org
-ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION
- ;ROLE=OPT-PARTICIPANT;RSVP=FALSE:MAILTO:max at imum.com
+SUMMARY:Summary
+LOCATION:Location
+DESCRIPTION:Description\\n2 lines
+CATEGORIES:Personal
+TRANSP:OPAQUE
+PRIORITY:2
 SEQUENCE:2
+CLASS:PUBLIC
+ATTENDEE;CN="Manager, Jane";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYP
+ E=INDIVIDUAL;RSVP=TRUE:mailto:jane.manager at example.org
+ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA
+ LSE:MAILTO:max at imum.com
+ORGANIZER;CN=Doe\, John:mailto:john.doe at example.org
+URL:http://somelink.com/foo
+ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=silhouette.pn
+ g:iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAIAAADY27xgAAAAGXRFWHRTb2Z0d2FyZQBBZG9i
+ ZSBJbWFnZVJlYWR5ccllPAAAAsRJREFUeNrsmeluKjEMhTswrAWB4P3fECGx79CjsTDmOKRkpF
+ xxpfoHSmchX7ybFrfb7eszpPH1MfKH8ofyH6KUtd/c7/en0wmfWBdF0Wq1Op1Ou91uNGoer6iX
+ V1ar1Xa7xUJeB4qsr9frdyVlWWZH2VZyPp+xPXHIAoK70+m02+1m9JXj8bhcLi+Xi3J4xUCazS
+ bUltdtd7ud7ldUIhC3u+iTwF0sFhlR4Kds4LtRZK1w4te5UM6V6JaqhqC3CQ28OAsKggJfbZ3U
+ eozCqZ4koHIZCGmD9ivuos9YONFirmxrI0UNZG1kbZeUXdJQNJNa91RlqMn0ekYUMZDup6dXVV
+ m+1OSZhqLx6bVCELJGSsyFQtFrF15JGYMZgoxubWGDSDVhvTipDKWhoBOIpFobxtlbJ0Gh0/tg
+ lgXal4woUHi/36fQoBQncDAlupa8DeVwOPRe4lUyGAwQ+dl7W+xBXkJBhEUqR32UoJfYIKrR4d
+ ZBgcdIRqfEqn+mekl9FNRbSTA249la3ev1/kXHD47ZbEYR5L9kMplkd9vNZqMFyIYxxfN8Pk8q
+ QGlagT5QDtfrNYUMlWW9LiGNPPSmC/+OgpK2r4RO6dOatZd+4gAAemdIi6Fg9EKLD4vASWkzv3
+ ew06NSCiA40CumAIoaIrhrcAwjF7aDo58gUchgNV+0n1BAcDgcoAZrXV9mI4qkhtK6FJFhi9Fo
+ ZKPsgQI1ACJieH/Kd570t+xFoIzHYzl5Q40CFGrSqGuks3qmYIKJfIl0nPKLxAMFw7Dv1+2QYf
+ vFSOBQubbOFDSc7ZcfWvHv6DzhOzT6IeOVPuz8Roex0f6EgsE/2IL4qdg7hIXz7/pBie7q1uWr
+ tp66xrif0l1KwUE4P7Y9Gci/ZgtNRFX+Rw06Q2RigsjuDc3urwKHxuNITaaxyD9mT2WvSDAXn/
+ Pvhh8BBgBjyfPSGbSYcwAAAABJRU5ErkJggg==
+ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/plain;X-LABEL=text.txt:VGh
+ pcyBpcyBhIHRleHQgZmlsZQo=
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-PT30M
+END:VALARM
 END:VEVENT
 END:VCALENDAR
 """
         ical = icalendar.Calendar.from_ical(ical_str)
         event = event_from_ical(ical.walk('VEVENT')[0].to_ical())
+
+        self.assertEqual(event.get_location(), "Location")
+        self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11")
+        self.assertEqual(event.get_description(), "Description\n2 lines")
+        self.assertEqual(event.get_url(), "http://somelink.com/foo")
+        self.assertEqual(event.get_transparency(), False)
+        self.assertEqual(event.get_categories(), ["Personal"])
+        self.assertEqual(event.get_priority(), '2')
+        self.assertEqual(event.get_classification(), kolabformat.ClassPublic)
         self.assertEqual(event.get_attendee_by_email("max at imum.com").get_cutype(), kolabformat.CutypeResource)
         self.assertEqual(event.get_sequence(), 2)
         self.assertTrue(event.is_recurring())
@@ -145,6 +190,8 @@ END:VCALENDAR
         self.assertEqual(str(event.get_end()), "2014-05-23 12:30:00+01:00")
         self.assertEqual(len(event.get_exception_dates()), 2)
         self.assertIsInstance(event.get_exception_dates()[0], datetime.datetime)
+        self.assertEqual(len(event.get_alarms()), 1)
+        self.assertEqual(len(event.get_attachments()), 2)
 
     def test_019_as_string_itip(self):
         self.event.set_summary("test")




More information about the commits mailing list