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