2 commits - INSTALL pykolab/xml tests/functional tests/unit wallace/module_resources.py

Thomas Brüderli bruederli at kolabsys.com
Wed Mar 5 11:25:56 CET 2014


 INSTALL                                                       |    1 
 pykolab/xml/event.py                                          |   38 ++++-
 pykolab/xml/utils.py                                          |    7 
 tests/functional/test_wallace/test_005_resource_invitation.py |   52 ++++++-
 tests/unit/test-003-event.py                                  |    8 +
 wallace/module_resources.py                                   |   71 ++++++----
 6 files changed, 140 insertions(+), 37 deletions(-)

New commits:
commit a4c05082d6b365da9bc4ad138278d682bc1bce80
Merge: f4b9812 ae24a70
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 4 15:51:01 2014 -0500

    Merge branch 'master' of ssh://git.kolab.org/git/pykolab



commit f4b9812231a169f48fd0a45fa788d4aa09026387
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 4 15:50:51 2014 -0500

    Basic support for recurring resource invitations

diff --git a/INSTALL b/INSTALL
index 162869b..21c764b 100644
--- a/INSTALL
+++ b/INSTALL
@@ -11,4 +11,5 @@
 * python-kolabformat
 * python-kolab
 * python-nose
+* python-dateutil
 * python-twisted-core
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 181c270..a165bcf 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -624,6 +624,10 @@ class Event(object):
     def set_recurrence(self, recurrence):
         self.event.setRecurrenceRule(recurrence)
 
+        # reset eventcal instance
+        if hasattr(self, 'eventcal'):
+            del self.eventcal
+
     def set_start(self, _datetime):
         valid_datetime = False
         if isinstance(_datetime, datetime.date):
@@ -808,7 +812,13 @@ class Event(object):
             self.eventcal = self.to_event_cal()
 
         next_cdatetime = self.eventcal.getNextOccurence(xmlutils.to_cdatetime(datetime, True))
-        return xmlutils.from_cdatetime(next_cdatetime, True) if next_cdatetime is not None else None
+        next_datetime  = xmlutils.from_cdatetime(next_cdatetime, True) if next_cdatetime is not None else None
+
+        # cut infinite recurrence at a reasonable point
+        if next_datetime and not self.get_last_occurrence() and next_datetime > self._recurrence_end():
+            next_datetime = None
+
+        return next_datetime
 
     def get_occurence_end_date(self, datetime):
         if not datetime:
@@ -820,12 +830,18 @@ class Event(object):
         end_cdatetime = self.eventcal.getOccurenceEndDate(xmlutils.to_cdatetime(datetime, True))
         return xmlutils.from_cdatetime(end_cdatetime, True) if end_cdatetime is not None else None
 
-    def get_last_occurrence(self):
+    def get_last_occurrence(self, force=False):
         if not hasattr(self, 'eventcal'):
             self.eventcal = self.to_event_cal()
 
         last = self.eventcal.getLastOccurrence()
-        return xmlutils.from_cdatetime(last, True) if last is not None else None
+        last_datetime = xmlutils.from_cdatetime(last, True) if last is not None else None
+
+        # we're forced to return some date
+        if last_datetime is None and force:
+            last_datetime = self._recurrence_end()
+
+        return last_datetime
 
     def get_next_instance(self, datetime):
         next_start = self.get_next_occurence(datetime)
@@ -842,6 +858,22 @@ class Event(object):
 
         return None
 
+    def _recurrence_end(self):
+        """
+            Determine a reasonable end date for infinitely recurring events
+        """
+        rrule = self.event.recurrenceRule()
+        if rrule.isValid() and rrule.count() < 0 and not rrule.end().isValid():
+            now = datetime.datetime.now()
+            switch = {
+                kolabformat.RecurrenceRule.Yearly: 100,
+                kolabformat.RecurrenceRule.Monthly: 20
+            }
+            intvl = switch[rrule.frequency()] if rrule.frequency() in switch else 10
+            return self.get_start().replace(year=now.year + intvl)
+
+        return xmlutils.from_cdatetime(rrule.end(), True)
+
 
 class EventIntegrityError(Exception):
     def __init__(self, message):
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index 780932d..2fddb24 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -1,16 +1,17 @@
 import datetime
 import pytz
 import kolabformat
+from dateutil.tz import tzlocal
 
 def to_dt(dt):
     """
         Convert a naive date or datetime to a tz-aware datetime.
     """
 
-    if isinstance(dt, datetime.date) and not isinstance(dt, datetime.datetime) or not hasattr(dt, 'hour'):
-        dt = datetime.datetime(dt.year, dt.month, dt.day, 0, 0, 0, 0)
+    if isinstance(dt, datetime.date) and not isinstance(dt, datetime.datetime) or dt is not None and not hasattr(dt, 'hour'):
+        dt = datetime.datetime(dt.year, dt.month, dt.day, 0, 0, 0, 0, tzinfo=tzlocal())
 
-    else:
+    elif isinstance(dt, datetime.datetime):
         if dt.tzinfo == None:
             return dt.replace(tzinfo=pytz.utc)
 
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index e464c73..1800e19 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -128,6 +128,27 @@ END:VCALENDAR
 """
 
 
+itip_recurring = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:%s
+DTSTAMP:20140213T1254140
+DTSTART;TZID=Europe/Zurich:%s
+DTEND;TZID=Europe/Zurich:%s
+RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:%s
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
 mime_message = """MIME-Version: 1.0
 Content-Type: multipart/mixed;
  boundary="=_c8894dbdb8baeedacae836230e3436fd"
@@ -196,22 +217,22 @@ class TestResourceInvitation(unittest.TestCase):
         smtp = smtplib.SMTP('localhost', 10026)
         smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload))
 
-    def send_itip_invitation(self, resource_email, start=None, allday=False):
+    def send_itip_invitation(self, resource_email, start=None, allday=False, template=None):
         if start is None:
             start = datetime.datetime.now()
 
         uid = str(uuid.uuid4())
 
         if allday:
-            template = itip_allday
+            default_template = itip_allday
             end = start + datetime.timedelta(days=1)
             date_format = '%Y%m%d'
         else:
             end = start + datetime.timedelta(hours=4)
-            template = itip_invitation
+            default_template = itip_invitation
             date_format = '%Y%m%dT%H%M%S'
 
-        self.send_message(template % (
+        self.send_message((template if template is not None else default_template) % (
                 uid,
                 start.strftime(date_format),
                 end.strftime(date_format),
@@ -461,3 +482,26 @@ class TestResourceInvitation(unittest.TestCase):
         uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2, 16,0,0))
         response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
         self.assertIsInstance(response, email.message.Message)
+
+
+    def test_009_recurring_events(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        # register an infinitely recurring resource invitation
+        uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,20, 12,0,0),
+            template=itip_recurring.replace(";COUNT=10", ""))
+
+        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        self.assertIsInstance(accept, email.message.Message)
+
+        # check non-recurring against recurring
+        uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,13, 10,0,0))
+        response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        self.purge_mailbox(self.john['mailbox'])
+
+        # check recurring against recurring
+        uid3 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,22, 8,0,0), template=itip_recurring)
+        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        self.assertIsInstance(accept, email.message.Message)
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 3f9083e..429e5bc 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -190,6 +190,14 @@ END:VCALENDAR
 
         self.assertEqual(self.event.get_next_occurence(last_date), None)
 
+        # check infinite recurrence
+        rrule = kolabformat.RecurrenceRule()
+        rrule.setFrequency(kolabformat.RecurrenceRule.Monthly)
+        self.event.set_recurrence(rrule);
+
+        self.assertEqual(self.event.get_last_occurrence(), None)
+        self.assertIsInstance(self.event.get_last_occurrence(force=True), datetime.datetime)
+
         # check get_next_instance() which returns a clone of the base event
         next_instance = self.event.get_next_instance(next_date)
         self.assertIsInstance(next_instance, Event)
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index b6d7966..cf03520 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -435,28 +435,24 @@ def read_resource_calendar(resource_rec, itip_events):
 
                     for itip in itip_events:
                         _es = to_dt(event.get_start())
-                        _is = to_dt(itip['start'].dt)
-
                         _ee = to_dt(event.get_end())
-                        _ie = to_dt(itip['end'].dt)
-
-                        # TODO: add margin for all-day dates (+13h; -12h)
-
-                        if _es < _is:
-                            if _es <= _ie:
-                                if _ee <= _is:
-                                    conflict = False
-                                else:
-                                    conflict = True
-                            else:
-                                conflict = True
-                        elif _es == _is:
-                            conflict = True
-                        else: # _es > _is
-                            if _es <= _ie:
-                                conflict = True
-                            else:
-                                conflict = False
+
+                        conflict = False
+
+                        # naive loops to check for collisions in (recurring) events
+                        # TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday)
+                        while not conflict and _es is not None:
+                            _is = to_dt(itip['start'])
+                            _ie = to_dt(itip['end'])
+
+                            while not conflict and _is is not None:
+                                log.debug("* Comparing event dates at %s/%s with %s/%s" % (_es, _ee, _is, _ie), level=9)
+                                conflict = check_date_conflict(_es, _ee, _is, _ie)
+                                _is = to_dt(itip['xml'].get_next_occurence(_is)) if event.is_recurring() else None
+                                _ie = to_dt(itip['xml'].get_occurence_end_date(_is))
+
+                            _es = to_dt(event.get_next_occurence(_es)) if event.is_recurring() else None
+                            _ee = to_dt(event.get_occurence_end_date(_es))
 
                         if event.get_uid() == itip['uid']:
                             resource_rec['existing_events'].append(itip['uid'])
@@ -478,6 +474,29 @@ def read_resource_calendar(resource_rec, itip_events):
 
     return num_messages
 
+def check_date_conflict(_es, _ee, _is, _ie):
+    conflict = False
+
+    # TODO: add margin for all-day dates (+13h; -12h)
+
+    if _es < _is:
+        if _es <= _ie:
+            if _ee <= _is:
+                conflict = False
+            else:
+                conflict = True
+        else:
+            conflict = True
+    elif _es == _is:
+        conflict = True
+    else: # _es > _is
+        if _es <= _ie:
+            conflict = True
+        else:
+            conflict = False
+    
+    return conflict
+
 
 def accept_reservation_request(itip_event, resource, delegator=None):
     """
@@ -622,8 +641,6 @@ def itip_events_from_message(message):
                     # - organizer
                     # - attendees (if any)
                     # - resources (if any)
-                    # - TODO: recurrence rules (if any)
-                    #   Where are these stored actually?
                     #
 
                     itip['uid'] = str(c['uid'])
@@ -631,17 +648,17 @@ def itip_events_from_message(message):
                     itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0
 
                     if c.has_key('dtstart'):
-                        itip['start'] = c['dtstart']
+                        itip['start'] = c['dtstart'].dt
                     else:
                         log.error(_("iTip event without a start"))
                         continue
 
                     if c.has_key('dtend'):
-                        itip['end'] = c['dtend']
+                        itip['end'] = c['dtend'].dt
 
                     if c.has_key('duration'):
-                        itip['duration'] = c['duration']
-                        # TODO: translate start + duration into end
+                        itip['duration'] = c['duration'].dt
+                        itip['end'] = itip['start'] + c['duration'].dt
 
                     itip['organizer'] = c['organizer']
 




More information about the commits mailing list