diff options
author | Thomas Bruederli <bruederli@kolabsys.com> | 2014-03-04 15:50:51 -0500 |
---|---|---|
committer | Thomas Bruederli <bruederli@kolabsys.com> | 2014-03-04 15:50:51 -0500 |
commit | f4b9812231a169f48fd0a45fa788d4aa09026387 (patch) | |
tree | 23c6d5a30e0a61b36749572d6abfb6932d483a54 | |
parent | 74c17bde3f0eb6d83730d3711c8b9d8f68c6c2dc (diff) | |
download | pykolab-f4b9812231a169f48fd0a45fa788d4aa09026387.tar.gz |
Basic support for recurring resource invitations
-rw-r--r-- | INSTALL | 1 | ||||
-rw-r--r-- | pykolab/xml/event.py | 38 | ||||
-rw-r--r-- | pykolab/xml/utils.py | 7 | ||||
-rw-r--r-- | tests/functional/test_wallace/test_005_resource_invitation.py | 52 | ||||
-rw-r--r-- | tests/unit/test-003-event.py | 8 | ||||
-rw-r--r-- | wallace/module_resources.py | 71 |
6 files changed, 140 insertions, 37 deletions
@@ -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@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'] |