summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2015-02-10 09:26:56 +0100
committerThomas Bruederli <bruederli@kolabsys.com>2015-02-10 09:26:56 +0100
commit21c116a670c3a34a1e084e1af7c75541a1783f27 (patch)
treee2879e84682bcd5678cf15f33371396374154623
parente40632b31347e5c25a2fdae2cbfec91eb521b09a (diff)
downloadpykolab-21c116a670c3a34a1e084e1af7c75541a1783f27.tar.gz
Fix importing ical VTODO objects with attachments (#4532):
- Avoid serializing and re-parsing: accept icalendar.* instances in pykolab.xml.* wrapper classes - Don't attempt to load iCal data with libkolabxml as there's no support for it in the python bindings - Import ATTACH properties from iCal into the XML Todo object
-rw-r--r--pykolab/itip/__init__.py4
-rw-r--r--pykolab/xml/event.py23
-rw-r--r--pykolab/xml/todo.py60
-rw-r--r--tests/unit/test-016-todo.py47
4 files changed, 116 insertions, 18 deletions
diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 93dc8bf..cae8075 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -115,9 +115,9 @@ def objects_from_message(message, objnames, methods=None):
try:
# distinguish event and todo here
if itip['type'] == 'task':
- itip['xml'] = todo_from_ical(c.to_ical())
+ itip['xml'] = todo_from_ical(c, itip_payload)
else:
- itip['xml'] = event_from_ical(c.to_ical())
+ itip['xml'] = event_from_ical(c, itip_payload)
except Exception, e:
log.error("event|todo_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
continue
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 625f555..b179b8f 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -35,8 +35,8 @@ def ustr(s):
return s
-def event_from_ical(string):
- return Event(from_ical=string)
+def event_from_ical(ical, string=None):
+ return Event(from_ical=ical, from_string=string)
def event_from_string(string):
return Event(from_string=string)
@@ -121,14 +121,14 @@ class Event(object):
self._categories = []
self._attachment_parts = []
- if from_ical == "":
+ if isinstance(from_ical, str) and from_ical == "":
if from_string == "":
self.event = kolabformat.Event()
else:
self.event = kolabformat.readEvent(from_string, False)
self._load_attendees()
else:
- self.from_ical(from_ical)
+ self.from_ical(from_ical, from_string)
self.uid = self.get_uid()
@@ -259,16 +259,27 @@ class Event(object):
self.event.setAttendees(self._attendees)
- def from_ical(self, ical):
+ def from_ical(self, ical, raw=None):
+ if isinstance(ical, icalendar.Todo):
+ ical_todo = ical
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)
+ # VCALENDAR block was given, find the first VEVENT item
+ if isinstance(ical_event, icalendar.Calendar):
+ for c in ical_event.walk():
+ if c.name == 'VEVENT':
+ ical_event = c
+ break
+
# 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)
+ if raw is None or raw == "":
+ raw = ical if isinstance(ical, str) else ical.to_ical()
+ self._xml_from_ical(raw)
else:
self.event = kolabformat.Event()
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
index 0d34c63..5859cb2 100644
--- a/pykolab/xml/todo.py
+++ b/pykolab/xml/todo.py
@@ -2,6 +2,7 @@ import datetime
import kolabformat
import icalendar
import pytz
+import base64
import pykolab
from pykolab import constants
@@ -12,8 +13,8 @@ from pykolab.translate import _
log = pykolab.getLogger('pykolab.xml_todo')
-def todo_from_ical(string):
- return Todo(from_ical=string)
+def todo_from_ical(ical, string=None):
+ return Todo(from_ical=ical, from_string=string)
def todo_from_string(string):
return Todo(from_string=string)
@@ -48,26 +49,40 @@ class Todo(Event):
"end": "void"
})
- if from_ical == "":
+ if isinstance(from_ical, str) and from_ical == "":
if from_string == "":
self.event = kolabformat.Todo()
else:
self.event = kolabformat.readTodo(from_string, False)
self._load_attendees()
else:
- self.from_ical(from_ical)
+ self.from_ical(from_ical, from_string)
self.uid = self.get_uid()
- def from_ical(self, ical):
- if hasattr(icalendar.Todo, 'from_ical'):
+ def from_ical(self, ical, raw):
+ if isinstance(ical, icalendar.Todo):
+ ical_todo = ical
+ elif hasattr(icalendar.Todo, 'from_ical'):
ical_todo = icalendar.Todo.from_ical(ical)
elif hasattr(icalendar.Todo, 'from_string'):
ical_todo = icalendar.Todo.from_string(ical)
- # use the libkolab calendaring bindings to load the full iCal data
- if ical_todo.has_key('ATTACH') or [part for part in ical_todo.walk() if part.name == 'VALARM']:
- self._xml_from_ical(ical)
+ # VCALENDAR block was given, find the first VTODO item
+ if isinstance(ical_todo, icalendar.Calendar):
+ for c in ical_todo.walk():
+ if c.name == 'VTODO':
+ ical_todo = c
+ break
+
+ log.debug("Todo.from_ical(); %r, %r, %r" % (type(ical_todo), ical_todo.has_key('ATTACH'), ical_todo.has_key('ATTENDEE')), level=8)
+
+ # DISABLED: use the libkolab calendaring bindings to load the full iCal data
+ # TODO: this requires support for iCal parsing in the kolab.calendaring bindings
+ if False and ical_todo.has_key('ATTACH') or [part for part in ical_todo.walk() if part.name == 'VALARM']:
+ if raw is None or raw == "":
+ raw = ical if isinstance(ical, str) else ical.to_ical()
+ self._xml_from_ical(raw)
else:
self.event = kolabformat.Todo()
@@ -88,8 +103,33 @@ class Todo(Event):
self.set_from_ical('percentcomplete', ical_todo['PERCENT-COMPLETE'])
def _xml_from_ical(self, ical):
+ # FIXME: kolabformat or kolab.calendaring modules do not provide bindings to import Todo from iCal
self.event = Todo()
- self.event.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR")
+
+ def set_ical_attach(self, attachment):
+ if hasattr(attachment, 'params'):
+ params = attachment.params
+ else:
+ params = {}
+
+ _attachment = kolabformat.Attachment()
+ if params.has_key('FMTTYPE'):
+ mimetype = str(params['FMTTYPE'])
+ else:
+ mimetype = 'application/octet-stream'
+
+ if params.has_key('X-LABEL'):
+ _attachment.setLabel(str(params['X-LABEL']))
+
+ decode = False
+ if params.has_key('ENCODING'):
+ if params['ENCODING'] == "BASE64" or params['ENCODING'] == "B":
+ decode = True
+
+ _attachment.setData(base64.b64decode(str(attachment)) if decode else str(attachment), mimetype)
+ vattach = self.event.attachments()
+ vattach.append(_attachment)
+ self.event.setAttachments(vattach)
def set_ical_due(self, due):
self.set_due(due)
diff --git a/tests/unit/test-016-todo.py b/tests/unit/test-016-todo.py
index 2e55c63..dcb89c5 100644
--- a/tests/unit/test-016-todo.py
+++ b/tests/unit/test-016-todo.py
@@ -41,6 +41,43 @@ END:VTODO
END:VCALENDAR
"""
+ical_todo_attachment = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTODO
+UID:18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+LAST-MODIFIED;VALUE=DATE-TIME:20140820T101333Z
+DUE;VALUE=DATE-TIME;TZID=Europe/London:20150228T133000
+SUMMARY:Task with attachment
+SEQUENCE:3
+PRIORITY:1
+STATUS:IN-PROCESS
+ORGANIZER;CN=Thomas:mailto:thomas.bruederli@example.org
+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==
+END:VTODO
+END:VCALENDAR
+"""
+
xml_todo = """
<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
<vcalendar>
@@ -223,6 +260,16 @@ METHOD:REQUEST
self.assertIsInstance(vtodo['dtstamp'].dt, datetime.datetime)
+ def test_022_ical_with_attachment(self):
+ todo = todo_from_ical(ical_todo_attachment)
+
+ vattachment = todo.get_attachments()
+ self.assertEqual(len(vattachment), 1)
+
+ attachment = vattachment[0]
+ self.assertEqual(attachment.mimetype(), 'image/png')
+ self.assertEqual(attachment.label(), 'silhouette.png')
+
def test_030_to_dict(self):
data = todo_from_string(xml_todo).to_dict()