diff options
author | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-07 10:21:49 -0400 |
---|---|---|
committer | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-07 10:21:49 -0400 |
commit | 0438fc64173d5e68822b7cf922ba807e1cabf95a (patch) | |
tree | 4a5dfc031ab55ccd0baaa48e036d525b9525fbcc | |
parent | cfc64210ee11c8e48e83a534059159e649d92906 (diff) | |
download | pykolab-0438fc64173d5e68822b7cf922ba807e1cabf95a.tar.gz |
Improve iCal import: support all event properties including alarms and attachments. We require full support if wallace directly copies invitations into user calendars
-rw-r--r-- | pykolab/xml/event.py | 111 | ||||
-rw-r--r-- | tests/unit/test-003-event.py | 59 |
2 files changed, 126 insertions, 44 deletions
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@doe.org -ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION - ;ROLE=OPT-PARTICIPANT;RSVP=FALSE:MAILTO:max@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@example.org +ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA + LSE:MAILTO:max@imum.com +ORGANIZER;CN=Doe\, John:mailto:john.doe@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@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") |