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