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