2 commits - INSTALL pykolab/xml tests/unit
Thomas Brüderli
bruederli at kolabsys.com
Wed Aug 20 15:44:49 CEST 2014
INSTALL | 2
pykolab/xml/__init__.py | 12 ++
pykolab/xml/event.py | 29 ++---
pykolab/xml/todo.py | 202 +++++++++++++++++++++++++++++++++++++
pykolab/xml/utils.py | 5
tests/unit/test-016-todo.py | 240 ++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 472 insertions(+), 18 deletions(-)
New commits:
commit b87c86a62e3157de9ee17917783f74dc3b0d756c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Aug 19 23:35:48 2014 -0400
Add wrapper class for libkolabxml todo objects with ical import/export.
ATTENTION: requires python-icalendar version 3.8 or higher!
VTodo implemention is incomplete in older versions.
diff --git a/INSTALL b/INSTALL
index 21c764b..b882a0a 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@
* intltool
* rpm-build
-* python-icalendar
+* python-icalendar (version 3.8.x or higher)
* python-kolabformat
* python-kolab
* python-nose
diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 3e12716..20b7e9f 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -13,6 +13,12 @@ from event import event_from_ical
from event import event_from_string
from event import event_from_message
+from todo import Todo
+from todo import TodoIntegrityError
+from todo import todo_from_ical
+from todo import todo_from_string
+from todo import todo_from_message
+
from utils import to_dt
__all__ = [
@@ -20,9 +26,14 @@ __all__ = [
"Contact",
"ContactReference",
"Event",
+ "Todo",
"RecurrenceRule",
"event_from_ical",
"event_from_string",
+ "event_from_message",
+ "todo_from_ical",
+ "todo_from_string",
+ "todo_from_message",
"to_dt",
]
@@ -30,6 +41,7 @@ errors = [
"EventIntegrityError",
"InvalidEventDateError",
"InvalidAttendeeParticipantStatusError",
+ "TodoIntegrityError",
]
__all__.extend(errors)
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index c199a5a..34f857a 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -1,7 +1,5 @@
import datetime
import icalendar
-from icalendar import vDatetime
-from icalendar import vText
import kolabformat
import pytz
import time
@@ -49,6 +47,9 @@ class Event(object):
"TENTATIVE": kolabformat.StatusTentative,
"CONFIRMED": kolabformat.StatusConfirmed,
"CANCELLED": kolabformat.StatusCancelled,
+ "COMPLETD": kolabformat.StatusCompleted,
+ "IN-PROCESS": kolabformat.StatusInProcess,
+ "NEEDS-ACTION": kolabformat.StatusNeedsAction,
}
classification_map = {
@@ -655,20 +656,14 @@ class Event(object):
self.event.setCustomProperties(props)
def set_from_ical(self, attr, value):
+ attr = attr.replace('-', '')
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 == "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":
+ if isinstance(value, icalendar.vDDDTypes) and hasattr(value, 'dt'):
+ value = value.dt
+
+ if attr == "categories":
self.add_category(value)
elif attr == "class":
self.set_classification(value)
@@ -733,9 +728,11 @@ class Event(object):
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)
- self.event.setDuration(duration)
+ if hasattr(value, 'dt'):
+ value = value.dt
+
+ duration = kolabformat.Duration(value.days, 0, 0, value.seconds, False)
+ self.event.setDuration(duration)
def set_ical_organizer(self, organizer):
address = str(organizer).split(':')[-1]
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
new file mode 100644
index 0000000..28a7b4d
--- /dev/null
+++ b/pykolab/xml/todo.py
@@ -0,0 +1,202 @@
+import datetime
+import kolabformat
+import icalendar
+import pytz
+
+import pykolab
+from pykolab import constants
+from pykolab.xml import Event
+from pykolab.xml import utils as xmlutils
+from pykolab.xml.event import InvalidEventDateError
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.xml_todo')
+
+def todo_from_ical(string):
+ return Todo(from_ical=string)
+
+def todo_from_string(string):
+ return Todo(from_string=string)
+
+def todo_from_message(message):
+ todo = None
+ if message.is_multipart():
+ for part in message.walk():
+ if part.get_content_type() == "application/calendar+xml":
+ payload = part.get_payload(decode=True)
+ todo = todo_from_string(payload)
+
+ # append attachment parts to Todo object
+ elif todo and part.has_key('Content-ID'):
+ todo._attachment_parts.append(part)
+
+ return todo
+
+# FIXME: extend a generic pykolab.xml.Xcal class instead of Event
+class Todo(Event):
+
+ def __init__(self, from_ical="", from_string=""):
+ self._attendees = []
+ self._categories = []
+ self._attachment_parts = []
+
+ self.properties_map.update({
+ "due": "get_due",
+ "percent-complete": "get_percentcomplete",
+ "duration": "void",
+ "end": "void"
+ })
+
+ if 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.uid = self.get_uid()
+
+ def from_ical(self, ical):
+ if 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)
+ else:
+ self.event = kolabformat.Todo()
+
+ for attr in list(set(ical_todo.required)):
+ if ical_todo.has_key(attr):
+ self.set_from_ical(attr.lower(), ical_todo[attr])
+
+ for attr in list(set(ical_todo.singletons)):
+ if ical_todo.has_key(attr):
+ self.set_from_ical(attr.lower(), ical_todo[attr])
+
+ for attr in list(set(ical_todo.multiple)):
+ if ical_todo.has_key(attr):
+ self.set_from_ical(attr.lower(), ical_todo[attr])
+
+ # although specified by RFC 2445/5545, icalendar doesn't have this property listed
+ if ical_todo.has_key('PERCENT-COMPLETE'):
+ self.set_from_ical('percentcomplete', ical_todo['PERCENT-COMPLETE'])
+
+ def _xml_from_ical(self, ical):
+ self.event = Todo()
+ self.event.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR")
+
+ def set_ical_due(self, due):
+ self.set_due(due)
+
+ def set_due(self, _datetime):
+ valid_datetime = False
+ if isinstance(_datetime, datetime.date):
+ valid_datetime = True
+
+ if isinstance(_datetime, datetime.datetime):
+ # If no timezone information is passed on, make it UTC
+ if _datetime.tzinfo == None:
+ _datetime = _datetime.replace(tzinfo=pytz.utc)
+
+ valid_datetime = True
+
+ if not valid_datetime:
+ raise InvalidEventDateError, _("Todo due needs datetime.date or datetime.datetime instance")
+
+ self.event.setDue(xmlutils.to_cdatetime(_datetime, True))
+
+ def set_ical_percent(self, percent):
+ self.set_percentcomplete(percent)
+
+ def set_percentcomplete(self, percent):
+ self.event.setPercentComplete(int(percent))
+
+ def get_due(self):
+ return xmlutils.from_cdatetime(self.event.due(), True)
+
+ def get_ical_due(self):
+ dt = self.get_due()
+ if dt:
+ return icalendar.vDatetime(dt)
+ return None
+
+ def get_percentcomplete(self):
+ return self.event.percentComplete()
+
+ def get_duration(self):
+ return None
+
+ def as_string_itip(self, method="REQUEST"):
+ cal = icalendar.Calendar()
+ cal.add(
+ 'prodid',
+ '-//pykolab-%s-%s//kolab.org//' % (
+ constants.__version__,
+ constants.__release__
+ )
+ )
+
+ cal.add('version', '2.0')
+ cal.add('calscale', 'GREGORIAN')
+ cal.add('method', method)
+
+ ical_todo = icalendar.Todo()
+
+ singletons = list(set(ical_todo.singletons))
+ singletons.extend(['PERCENT-COMPLETE'])
+ for attr in singletons:
+ ical_getter = 'get_ical_%s' % (attr.lower())
+ default_getter = 'get_%s' % (attr.lower())
+ retval = None
+ if hasattr(self, ical_getter):
+ retval = getattr(self, ical_getter)()
+ if not retval == None and not retval == "":
+ ical_todo.add(attr.lower(), retval)
+ elif hasattr(self, default_getter):
+ retval = getattr(self, default_getter)()
+ if not retval == None and not retval == "":
+ ical_todo.add(attr.lower(), retval, encode=0)
+
+ for attr in list(set(ical_todo.multiple)):
+ ical_getter = 'get_ical_%s' % (attr.lower())
+ default_getter = 'get_%s' % (attr.lower())
+ retval = None
+ if hasattr(self, ical_getter):
+ retval = getattr(self, ical_getter)()
+ elif hasattr(self, default_getter):
+ retval = getattr(self, default_getter)()
+
+ if isinstance(retval, list) and not len(retval) == 0:
+ for _retval in retval:
+ ical_todo.add(attr.lower(), _retval, encode=0)
+
+ # copy custom properties to iCal
+ for cs in self.event.customProperties():
+ ical_todo.add(cs.identifier, cs.value)
+
+ cal.add_component(ical_todo)
+
+ if hasattr(cal, 'to_ical'):
+ return cal.to_ical()
+ elif hasattr(cal, 'as_string'):
+ return cal.as_string()
+
+ def __str__(self):
+ xml = kolabformat.writeTodo(self.event)
+
+ error = kolabformat.error()
+
+ if error == None or not error:
+ return xml
+ else:
+ raise TodoIntegrityError, kolabformat.errorMessage()
+
+
+class TodoIntegrityError(Exception):
+ def __init__(self, message):
+ Exception.__init__(self, message)
diff --git a/tests/unit/test-016-todo.py b/tests/unit/test-016-todo.py
new file mode 100644
index 0000000..a7e9394
--- /dev/null
+++ b/tests/unit/test-016-todo.py
@@ -0,0 +1,240 @@
+import datetime
+import pytz
+import sys
+import unittest
+import kolabformat
+import icalendar
+
+from pykolab.xml import Attendee
+from pykolab.xml import Todo
+from pykolab.xml import TodoIntegrityError
+from pykolab.xml import todo_from_ical
+from pykolab.xml import todo_from_string
+from pykolab.xml import todo_from_message
+
+ical_todo = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTODO
+UID:18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+LAST-MODIFIED;VALUE=DATE-TIME:20140820T101333Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/London:20140818T180000
+DUE;VALUE=DATE-TIME;TZID=Europe/London:20140822T133000
+SUMMARY:Sample Task assignment
+DESCRIPTION:Summary: Sample Task assignment\\nDue Date: 08/11/14\\nDue Time:
+ \\n13:30 AM
+SEQUENCE:3
+CATEGORIES:iTip
+PRIORITY:1
+STATUS:IN-PROCESS
+PERCENT-COMPLETE:20
+ATTENDEE;CN="Doe, John";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYPE=
+ INDIVIDUAL;RSVP=TRUE:mailto:john.doe at example.org
+ORGANIZER;CN=Thomas:mailto:thomas.bruederli at example.org
+END:VTODO
+END:VCALENDAR
+"""
+
+xml_todo = """
+<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
+ <vcalendar>
+ <properties>
+ <prodid>
+ <text>Roundcube-libkolab-1.1 Libkolabxml-1.1</text>
+ </prodid>
+ <version>
+ <text>2.0</text>
+ </version>
+ <x-kolab-version>
+ <text>3.1.0</text>
+ </x-kolab-version>
+ </properties>
+ <components>
+ <vtodo>
+ <properties>
+ <uid>
+ <text>18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0</text>
+ </uid>
+ <created>
+ <date-time>2014-07-31T10:07:04Z</date-time>
+ </created>
+ <dtstamp>
+ <date-time>2014-08-20T10:13:33Z</date-time>
+ </dtstamp>
+ <sequence>
+ <integer>3</integer>
+ </sequence>
+ <class>
+ <text>PUBLIC</text>
+ </class>
+ <categories>
+ <text>iTip</text>
+ </categories>
+ <dtstart>
+ <parameters>
+ <tzid><text>/kolab.org/Europe/Berlin</text></tzid>
+ </parameters>
+ <date-time>2014-08-18T18:00:00</date-time>
+ </dtstart>
+ <due>
+ <parameters>
+ <tzid><text>/kolab.org/Europe/Berlin</text></tzid>
+ </parameters>
+ <date-time>2014-08-22T13:30:00</date-time>
+ </due>
+ <summary>
+ <text>Sample Task assignment</text>
+ </summary>
+ <description>
+ <text>Summary: Sample Task assignment
+Due Date: 08/11/14
+Due Time: 13:30 AM</text>
+ </description>
+ <priority>
+ <integer>1</integer>
+ </priority>
+ <status>
+ <text>IN-PROCESS</text>
+ </status>
+ <percent-complete>
+ <integer>20</integer>
+ </percent-complete>
+ <organizer>
+ <parameters>
+ <cn><text>Thomas</text></cn>
+ </parameters>
+ <cal-address>mailto:%3Cthomas%40example.org%3E</cal-address>
+ </organizer>
+ <attendee>
+ <parameters>
+ <cn><text>Doe, John</text></cn>
+ <partstat><text>NEEDS-ACTION</text></partstat>
+ <role><text>REQ-PARTICIPANT</text></role>
+ <rsvp><boolean>true</boolean></rsvp>
+ </parameters>
+ <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address>
+ </attendee>
+ </properties>
+ <components>
+ <valarm>
+ <properties>
+ <action>
+ <text>DISPLAY</text>
+ </action>
+ <description>
+ <text>alarm 1</text>
+ </description>
+ <trigger>
+ <parameters>
+ <related>
+ <text>START</text>
+ </related>
+ </parameters>
+ <duration>-PT2H</duration>
+ </trigger>
+ </properties>
+ </valarm>
+ </components>
+ </vtodo>
+ </components>
+ </vcalendar>
+</icalendar>
+"""
+
+class TestTodoXML(unittest.TestCase):
+ todo = Todo()
+
+ def assertIsInstance(self, _value, _type):
+ if hasattr(unittest.TestCase, 'assertIsInstance'):
+ return unittest.TestCase.assertIsInstance(self, _value, _type)
+ else:
+ if (type(_value)) == _type:
+ return True
+ else:
+ raise AssertionError, "%s != %s" % (type(_value), _type)
+
+ def test_001_minimal(self):
+ self.todo.set_summary("test")
+ self.assertEqual("test", self.todo.get_summary())
+ self.assertIsInstance(self.todo.__str__(), str)
+
+ def test_002_full(self):
+ pass
+
+ def test_010_load_from_xml(self):
+ todo = todo_from_string(xml_todo)
+ self.assertEqual(todo.uid, '18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0')
+ self.assertEqual(todo.get_sequence(), 3)
+ self.assertIsInstance(todo.get_due(), datetime.datetime)
+ self.assertEqual(str(todo.get_due()), "2014-08-22 13:30:00+01:00")
+ self.assertEqual(str(todo.get_start()), "2014-08-18 18:00:00+01:00")
+ self.assertEqual(todo.get_categories(), ['iTip'])
+ self.assertEqual(todo.get_attendee_by_email("john at example.org").get_participant_status(), kolabformat.PartNeedsAction)
+ self.assertIsInstance(todo.get_organizer(), kolabformat.ContactReference)
+ self.assertEqual(todo.get_organizer().name(), "Thomas")
+ self.assertEqual(todo.get_status(True), "IN-PROCESS")
+
+
+ def test_020_load_from_ical(self):
+ ical_str = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1.0//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+ """ + ical_todo + "END:VCALENDAR"
+
+ ical = icalendar.Calendar.from_ical(ical_str)
+ vtodo = ical.walk('VTODO')[0]
+ #print vtodo
+ todo = todo_from_ical(ical.walk('VTODO')[0].to_ical())
+ self.assertEqual(todo.get_summary(), "Sample Task assignment")
+ self.assertIsInstance(todo.get_start(), datetime.datetime)
+ self.assertEqual(todo.get_percentcomplete(), 20)
+ #print str(todo)
+
+ def test_021_as_string_itip(self):
+ self.todo.set_summary("test")
+ self.todo.set_start(datetime.datetime(2014, 9, 20, 11, 00, 00, tzinfo=pytz.timezone("Europe/London")))
+ self.todo.set_due(datetime.datetime(2014, 9, 23, 12, 30, 00, tzinfo=pytz.timezone("Europe/London")))
+ self.todo.set_sequence(3)
+ self.todo.add_custom_property('X-Custom', 'check')
+
+ # render iCal and parse again using the icalendar lib
+ ical = icalendar.Calendar.from_ical(self.todo.as_string_itip())
+ vtodo = ical.walk('VTODO')[0]
+
+ self.assertEqual(vtodo['uid'], self.todo.get_uid())
+ self.assertEqual(vtodo['summary'], "test")
+ self.assertEqual(vtodo['sequence'], 3)
+ self.assertEqual(vtodo['X-CUSTOM'], "check")
+ self.assertIsInstance(vtodo['due'].dt, datetime.datetime)
+ self.assertIsInstance(vtodo['dtstamp'].dt, datetime.datetime)
+
+
+ def test_030_to_dict(self):
+ data = todo_from_string(xml_todo).to_dict()
+
+ self.assertIsInstance(data, dict)
+ self.assertIsInstance(data['start'], datetime.datetime)
+ self.assertIsInstance(data['due'], datetime.datetime)
+ self.assertEqual(data['uid'], '18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0')
+ self.assertEqual(data['summary'], 'Sample Task assignment')
+ self.assertEqual(data['description'], "Summary: Sample Task assignment\nDue Date: 08/11/14\nDue Time: 13:30 AM")
+ self.assertEqual(data['priority'], 1)
+ self.assertEqual(data['sequence'], 3)
+ self.assertEqual(data['status'], 'IN-PROCESS')
+
+ self.assertIsInstance(data['alarm'], list)
+ self.assertEqual(len(data['alarm']), 1)
+ self.assertEqual(data['alarm'][0]['action'], 'DISPLAY')
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
commit 50ecd9edf92d3d50492f23408e009c900a63d882
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Aug 19 23:02:17 2014 -0400
Translate UTC and GMT timezones into the according isUTC flag
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index 2fddb24..bcaa480 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -92,6 +92,9 @@ def to_cdatetime(_datetime, with_timezone=True):
_cdatetime = kolabformat.cDateTime(year, month, day)
if with_timezone and hasattr(_datetime, "tzinfo"):
- _cdatetime.setTimezone(_datetime.tzinfo.__str__())
+ if _datetime.tzinfo.__str__() in ['UTC','GMT']:
+ _cdatetime.setUTC(True)
+ else:
+ _cdatetime.setTimezone(_datetime.tzinfo.__str__())
return _cdatetime
More information about the commits
mailing list