2 commits - INSTALL pykolab/itip pykolab/xml tests/functional tests/unit wallace/module_invitationpolicy.py
Thomas Brüderli
bruederli at kolabsys.com
Thu Aug 21 16:31:34 CEST 2014
INSTALL | 2
pykolab/itip/__init__.py | 28
pykolab/xml/attendee.py | 11
pykolab/xml/contact.py | 2
pykolab/xml/event.py | 14
pykolab/xml/todo.py | 5
pykolab/xml/utils.py | 13
tests/functional/test_wallace/test_005_resource_invitation.py | 3
tests/functional/test_wallace/test_007_invitationpolicy.py | 258 +++++-
tests/unit/test-012-wallace_invitationpolicy.py | 32
tests/unit/test-016-todo.py | 1
wallace/module_invitationpolicy.py | 405 ++++++----
12 files changed, 559 insertions(+), 215 deletions(-)
New commits:
commit b05296d7d41c7a42620d996641db9054e9da2f23
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Aug 21 10:31:16 2014 -0400
Refactored the wallace invitationpolicy module to work for automated task iTip processing as well + add functional tests for task assignments (#3240)
diff --git a/INSTALL b/INSTALL
index b882a0a..0f83cf9 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@
* intltool
* rpm-build
-* python-icalendar (version 3.8.x or higher)
+* python-icalendar (version 3.8.2 or higher)
* python-kolabformat
* python-kolab
* python-nose
diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index ddcb392..1a361c1 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -1,8 +1,10 @@
import icalendar
import pykolab
+import traceback
from pykolab.xml import to_dt
from pykolab.xml import event_from_ical
+from pykolab.xml import todo_from_ical
from pykolab.xml import participant_status_label
from pykolab.translate import _
@@ -10,13 +12,13 @@ log = pykolab.getLogger('pykolab.wallace')
def events_from_message(message, methods=None):
- return objects_from_message(message, "VEVENT", methods)
+ return objects_from_message(message, ["VEVENT"], methods)
def todos_from_message(message, methods=None):
- return objects_from_message(message, "VTODO", methods)
+ return objects_from_message(message, ["VTODO"], methods)
-def objects_from_message(message, objname, methods=None):
+def objects_from_message(message, objnames, methods=None):
"""
Obtain the iTip payload from email.message <message>
"""
@@ -60,7 +62,7 @@ def objects_from_message(message, objname, methods=None):
return []
for c in cal.walk():
- if c.name == objname:
+ if c.name in objnames:
itip = {}
if c['uid'] in seen_uids:
@@ -80,13 +82,14 @@ def objects_from_message(message, objname, methods=None):
# - resources (if any)
#
+ itip['type'] = 'task' if c.name == 'VTODO' else 'event'
itip['uid'] = str(c['uid'])
itip['method'] = str(cal['method']).upper()
itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0
if c.has_key('dtstart'):
itip['start'] = c['dtstart'].dt
- else:
+ elif itip['type'] == 'VEVENT':
log.error(_("iTip event without a start"))
continue
@@ -110,17 +113,20 @@ def objects_from_message(message, objname, methods=None):
itip['raw'] = itip_payload
try:
- # TODO: distinguish event and todo here
- itip['xml'] = event_from_ical(c.to_ical())
+ # distinguish event and todo here
+ if itip['type'] == 'task':
+ itip['xml'] = todo_from_ical(c.to_ical())
+ else:
+ itip['xml'] = event_from_ical(c.to_ical())
except Exception, e:
- log.error("event_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
+ log.error("event|todo_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
continue
itip_objects.append(itip)
seen_uids.append(c['uid'])
- # end if c.name == "VEVENT"
+ # end if c.name in objnames
# end for c in cal.walk()
@@ -212,6 +218,8 @@ def send_reply(from_address, itip_events, response_text, subject=None):
attendee = itip_event['xml'].get_attendee_by_email(from_address)
participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
+ log.debug(_("Send iTip reply %s for %s %r") % (participant_status, itip_event['xml'].type, itip_event['xml'].uid), level=8)
+
event_summary = itip_event['xml'].get_summary()
message_text = response_text % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() }
@@ -226,7 +234,7 @@ def send_reply(from_address, itip_events, response_text, subject=None):
subject=subject
)
except Exception, e:
- log.error(_("Failed to compose iTip reply message: %r") % (e))
+ log.error(_("Failed to compose iTip reply message: %r: %s") % (e, traceback.format_exc()))
return
smtp = smtplib.SMTP("localhost", 10026) # replies go through wallace again
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index a6384e9..cdfe86a 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -12,13 +12,16 @@ participant_status_labels = {
"TENTATIVE": N_("Tentatively Accepted"),
"DELEGATED": N_("Delegated"),
"COMPLETED": N_("Completed"),
- "IN-PROCESS": N_("In Process"),
+ "IN-PROCESS": N_("Started"),
# support integer values, too
kolabformat.PartNeedsAction: N_("Needs Action"),
kolabformat.PartAccepted: N_("Accepted"),
kolabformat.PartDeclined: N_("Declined"),
kolabformat.PartTentative: N_("Tentatively Accepted"),
kolabformat.PartDelegated: N_("Delegated"),
+ # waiting for libkolabxml to support these (#3472)
+ #kolabformat.PartCompleted: N_("Completed"),
+ #kolabformat.PartInProcess: N_("Started"),
}
def participant_status_label(status):
@@ -38,9 +41,9 @@ class Attendee(kolabformat.Attendee):
"DECLINED": kolabformat.PartDeclined,
"TENTATIVE": kolabformat.PartTentative,
"DELEGATED": kolabformat.PartDelegated,
- # Not yet implemented
- #"COMPLETED": ,
- #"IN-PROCESS": ,
+ # waiting for libkolabxml to support these (#3472)
+ #"COMPLETED": kolabformat.PartCompleted,
+ #"IN-PROCESS": kolabformat.PartInProcess,
}
# See RFC 2445, 5445
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
index b04b233..0d34c63 100644
--- a/pykolab/xml/todo.py
+++ b/pykolab/xml/todo.py
@@ -117,6 +117,10 @@ class Todo(Event):
def set_percentcomplete(self, percent):
self.event.setPercentComplete(int(percent))
+ def set_transparency(self, transp):
+ # empty stub
+ pass
+
def get_due(self):
return xmlutils.from_cdatetime(self.event.due(), True)
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index a4e1ebe..4e69f2f 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -189,6 +189,9 @@ class TestResourceInvitation(unittest.TestCase):
@classmethod
def setup_class(self, *args, **kw):
+ # set language to default
+ pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
self.itip_reply_subject = _("Reservation Request for %(summary)s was %(status)s")
from tests.functional.purge_users import purge_users
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index bdbfc98..dffc9df 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -12,6 +12,7 @@ from wallace import module_resources
from pykolab.translate import _
from pykolab.xml import event_from_message
+from pykolab.xml import todo_from_message
from pykolab.xml import participant_status_label
from email import message_from_string
from twisted.trial import unittest
@@ -126,6 +127,75 @@ END:VEVENT
END:VCALENDAR
"""
+itip_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:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:%(start)s
+DUE;VALUE=DATE-TIME;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:NEEDS-ACTION
+PERCENT-COMPLETE:0
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+END:VTODO
+END:VCALENDAR
+"""
+
+itip_todo_reply = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REPLY
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140821T085424Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:%(start)s
+DUE;VALUE=DATE-TIME;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:NEEDS-ACTION
+PERCENT-COMPLETE:40
+ATTENDEE;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL:mailto:%(mailto)s
+ORGANIZER;CN="Doe, John":mailto:%(organizer)s
+END:VTODO
+END:VCALENDAR
+"""
+
+itip_todo_cancel = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:CANCELLED
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+END:VTODO
+END:VCALENDAR
+"""
+
mime_message = """MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="=_c8894dbdb8baeedacae836230e3436fd"
@@ -164,6 +234,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
@classmethod
def setup_class(self, *args, **kw):
+ # set language to default
+ pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
self.itip_reply_subject = _('"%(summary)s" has been %(status)s')
from tests.functional.purge_users import purge_users
@@ -175,7 +248,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
'dn': 'uid=doe,ou=People,dc=example,dc=org',
'preferredlanguage': 'en_US',
'mailbox': 'user/john.doe at example.org',
- 'kolabtargetfolder': 'user/john.doe/Calendar at example.org',
+ 'kolabcalendarfolder': 'user/john.doe/Calendar at example.org',
+ 'kolabtasksfolder': 'user/john.doe/Tasks at example.org',
'kolabinvitationpolicy': ['ACT_UPDATE_AND_NOTIFY','ACT_MANUAL']
}
@@ -185,8 +259,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
'dn': 'uid=manager,ou=People,dc=example,dc=org',
'preferredlanguage': 'en_US',
'mailbox': 'user/jane.manager at example.org',
- 'kolabtargetfolder': 'user/jane.manager/Calendar at example.org',
- 'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','ACT_UPDATE']
+ 'kolabcalendarfolder': 'user/jane.manager/Calendar at example.org',
+ 'kolabtasksfolder': 'user/jane.manager/Tasks at example.org',
+ 'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','TASK_ACCEPT','ACT_UPDATE']
}
self.jack = {
@@ -195,8 +270,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
'dn': 'uid=tentative,ou=People,dc=example,dc=org',
'preferredlanguage': 'en_US',
'mailbox': 'user/jack.tentative at example.org',
- 'kolabtargetfolder': 'user/jack.tentative/Calendar at example.org',
- 'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
+ 'kolabcalendarfolder': 'user/jack.tentative/Calendar at example.org',
+ 'kolabtasksfolder': 'user/jack.tentative/Tasks at example.org',
+ 'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ALL_SAVE_TO_FOLDER','ACT_UPDATE']
}
self.mark = {
@@ -205,7 +281,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
'dn': 'uid=german,ou=People,dc=example,dc=org',
'preferredlanguage': 'de_DE',
'mailbox': 'user/mark.german at example.org',
- 'kolabtargetfolder': 'user/mark.german/Calendar at example.org',
+ 'kolabcalendarfolder': 'user/mark.german/Calendar at example.org',
+ 'kolabtasksfolder': 'user/mark.german/Tasks at example.org',
'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE_AND_NOTIFY']
}
@@ -298,8 +375,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
return uid
- def send_itip_cancel(self, attendee_email, uid, summary="test", sequence=1):
- self.send_message(itip_cancellation % {
+ def send_itip_cancel(self, attendee_email, uid, template=None, summary="test", sequence=1):
+ self.send_message((template if template is not None else itip_cancellation) % {
'uid': uid,
'mailto': attendee_email,
'summary': summary,
@@ -342,7 +419,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
imap = IMAP()
imap.connect()
- mailbox = imap.folder_quote(user['kolabtargetfolder'])
+ mailbox = imap.folder_quote(user['kolabcalendarfolder'])
imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
imap.imap.m.select(mailbox)
@@ -355,11 +432,45 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
return event.get_uid()
+ def create_task_assignment(self, due=None, summary="test", sequence=0, user=None, attendees=None):
+ if due is None:
+ due = datetime.datetime.now(pytz.timezone("Europe/Berlin")) + datetime.timedelta(days=2)
+ if user is None:
+ user = self.john
+ if attendees is None:
+ attendees = [self.jane]
+
+ todo = pykolab.xml.Todo()
+ todo.set_due(due)
+ todo.set_organizer(user['mail'], user['displayname'])
+
+ for attendee in attendees:
+ todo.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True)
+
+ todo.set_summary(summary)
+ todo.set_sequence(sequence)
+
+ imap = IMAP()
+ imap.connect()
+
+ mailbox = imap.folder_quote(user['kolabtasksfolder'])
+ imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
+ imap.imap.m.select(mailbox)
+
+ result = imap.imap.m.append(
+ mailbox,
+ None,
+ None,
+ todo.to_message().as_string()
+ )
+
+ return todo.get_uid()
+
def update_calendar_event(self, uid, start=None, summary=None, sequence=0, user=None):
if user is None:
user = self.john
- event = self.check_user_calendar_event(user['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(user['kolabcalendarfolder'], uid)
if event:
if start is not None:
event.set_start(start)
@@ -371,7 +482,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
imap = IMAP()
imap.connect()
- mailbox = imap.folder_quote(user['kolabtargetfolder'])
+ mailbox = imap.folder_quote(user['kolabcalendarfolder'])
imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
imap.imap.m.select(mailbox)
@@ -416,6 +527,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
return found
def check_user_calendar_event(self, mailbox, uid=None):
+ return self.check_user_imap_object(mailbox, uid)
+
+ def check_user_imap_object(self, mailbox, uid=None, type='event'):
imap = IMAP()
imap.connect()
@@ -429,16 +543,20 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
while not found and retries > 0:
retries -= 1
- typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")')
+ typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.' + type + '")')
for num in data[0].split():
typ, data = imap.imap.m.fetch(num, '(RFC822)')
- event_message = message_from_string(data[0][1])
+ object_message = message_from_string(data[0][1])
# return matching UID or first event found
- if uid and event_message['subject'] != uid:
+ if uid and object_message['subject'] != uid:
continue
- found = event_from_message(event_message)
+ if type == 'task':
+ found = todo_from_message(object_message)
+ else:
+ found = event_from_message(object_message)
+
if found:
break
@@ -468,7 +586,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
- event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_summary(), "test")
@@ -476,7 +594,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.send_itip_update(self.jane['mail'], uid, start, summary="test updated", sequence=0, partstat='ACCEPTED')
time.sleep(10)
- event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_summary(), "test updated")
self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
@@ -489,7 +607,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
- event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_summary(), "test2")
@@ -515,7 +633,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jack['mail'])
self.assertEqual(response, None, "No reply expected")
- event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_summary(), "test2")
self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
@@ -530,7 +648,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
- event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_summary(), "test")
@@ -543,7 +661,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
- event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_start(), new_start)
self.assertEqual(event.get_sequence(), 1)
@@ -551,7 +669,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
def test_005_invite_rescheduling_reject(self):
self.purge_mailbox(self.john['mailbox'])
- self.purge_mailbox(self.jack['kolabtargetfolder'])
+ self.purge_mailbox(self.jack['kolabcalendarfolder'])
start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
uid = self.send_itip_invitation(self.jack['mail'], start)
@@ -568,7 +686,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.assertEqual(response, None)
# verify re-scheduled copy in jack's calendar with NEEDS-ACTION
- event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_start(), new_start)
self.assertEqual(event.get_sequence(), 1)
@@ -584,7 +702,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
start = datetime.datetime(2014,8,18, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
uid = self.create_calendar_event(start, user=self.john)
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
# send a reply from jane to john
@@ -592,7 +710,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
# check for the updated event in john's calendar
time.sleep(10)
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
attendee = event.get_attendee(self.jane['mail'])
@@ -611,7 +729,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
start = datetime.datetime(2014,8,28, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
uid = self.create_calendar_event(start, user=self.john)
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
# send a reply from jane to john
@@ -619,7 +737,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
# check for the updated event in john's calendar
time.sleep(10)
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
attendee = event.get_attendee(self.jane['mail'])
@@ -646,7 +764,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
time.sleep(10)
- event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_summary(), "cancelled")
self.assertEqual(event.get_status(True), 'CANCELLED')
@@ -723,7 +841,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
# verify jane's attendee status was not updated
time.sleep(10)
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_sequence(), 2)
self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartNeedsAction)
@@ -735,7 +853,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
start = datetime.datetime(2014,8,21, 13,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack, self.external])
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
# send invitations to jack and jane
@@ -745,7 +863,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
# wait for replies from jack and jane to be processed and propagated
time.sleep(10)
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
# check updated event in organizer's calendar
@@ -753,12 +871,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
# check updated partstats in jane's calendar
- janes = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ janes = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
self.assertEqual(janes.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
self.assertEqual(janes.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
# check updated partstats in jack's calendar
- jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ jacks = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
@@ -773,7 +891,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
# wait for replies to be processed and propagated
time.sleep(10)
- event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
# check updated event in organizer's calendar (jack didn't reply yet)
@@ -781,8 +899,78 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
# check partstats in jack's calendar: jack's status should remain needs-action
- jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ jacks = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
+ def test_011_task_assignment_accept(self):
+ start = datetime.datetime(2014,9,10, 19,0,0)
+ uid = self.send_itip_invitation(self.jane['mail'], start, summary='work', template=itip_todo)
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'work', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+ self.assertIsInstance(todo, pykolab.xml.Todo)
+ self.assertEqual(todo.get_summary(), "work")
+
+ # send update with the same sequence: no re-scheduling
+ self.send_itip_update(self.jane['mail'], uid, start, summary='work updated', template=itip_todo, sequence=0, partstat='ACCEPTED')
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'work updated', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+ self.assertEqual(response, None)
+
+ time.sleep(10)
+ todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+ self.assertIsInstance(todo, pykolab.xml.Todo)
+ self.assertEqual(todo.get_summary(), "work updated")
+ self.assertEqual(todo.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+
+ def test_012_task_assignment_reply(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ due = datetime.datetime(2014,9,12, 14,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.create_task_assignment(due, user=self.john)
+
+ todo = self.check_user_imap_object(self.john['kolabtasksfolder'], uid, 'task')
+ self.assertIsInstance(todo, pykolab.xml.Todo)
+
+ # send a reply from jane to john
+ partstat = 'DECLINED'
+ self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=due, template=itip_todo_reply, partstat=partstat)
+
+ # check for the updated task in john's tasklist
+ time.sleep(10)
+ todo = self.check_user_imap_object(self.john['kolabtasksfolder'], uid, 'task')
+ self.assertIsInstance(todo, pykolab.xml.Todo)
+
+ attendee = todo.get_attendee(self.jane['mail'])
+ self.assertIsInstance(attendee, pykolab.xml.Attendee)
+ self.assertEqual(attendee.get_participant_status(True), partstat)
+
+ # this should trigger an update notification
+ notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+ self.assertIsInstance(notification, email.message.Message)
+
+ notification_text = str(notification.get_payload());
+ self.assertIn(participant_status_label(partstat), notification_text)
+
+
+ def test_013_task_cancellation(self):
+ uid = self.send_itip_invitation(self.jane['mail'], summary='more work', template=itip_todo)
+
+ time.sleep(10)
+ self.send_itip_cancel(self.jane['mail'], uid, template=itip_todo_cancel, summary="cancelled")
+
+ time.sleep(10)
+ todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+ self.assertIsInstance(todo, pykolab.xml.Todo)
+ self.assertEqual(todo.get_summary(), "more work")
+ self.assertEqual(todo.get_status(True), 'CANCELLED')
+
+ # this should trigger a notification message
+ notification = self.check_message_received(_('"%s" has been cancelled') % ('more work'), self.john['mail'], mailbox=self.jane['mailbox'])
+ self.assertIsInstance(notification, email.message.Message)
+
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
index aabf676..3366950 100644
--- a/tests/unit/test-012-wallace_invitationpolicy.py
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -117,16 +117,18 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
def test_003_get_matching_invitation_policy(self):
user = { 'kolabinvitationpolicy': [
- 'ACT_ACCEPT:example.org',
- 'ACT_REJECT:gmail.com',
- 'ACT_MANUAL:*'
+ 'TASK_REJECT:*',
+ 'EVENT_ACCEPT:example.org',
+ 'EVENT_REJECT:gmail.com',
+ 'ALL_MANUAL:*'
] }
- self.assertEqual(MIP.get_matching_invitation_policies(user, 'a at fastmail.net'), [MIP.ACT_MANUAL])
- self.assertEqual(MIP.get_matching_invitation_policies(user, 'b at example.org'), [MIP.ACT_ACCEPT,MIP.ACT_MANUAL])
- self.assertEqual(MIP.get_matching_invitation_policies(user, 'c at gmail.com'), [MIP.ACT_REJECT,MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'a at fastmail.net', MIP.COND_TYPE_EVENT), [MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'b at example.org', MIP.COND_TYPE_EVENT), [MIP.ACT_ACCEPT, MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'c at gmail.com', MIP.COND_TYPE_EVENT), [MIP.ACT_REJECT, MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net', MIP.COND_TYPE_TASK), [MIP.ACT_REJECT, MIP.ACT_MANUAL])
user = { 'kolabinvitationpolicy': ['ACT_ACCEPT:example.org', 'ACT_MANUAL:others'] }
- self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net'), [MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net', MIP.COND_TYPE_ALL), [MIP.ACT_MANUAL])
def test_004_write_locks(self):
user = { 'cn': 'John Doe', 'mail': "doe at example.org" }
@@ -150,12 +152,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
accept_some = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_SAVE_TO_CALENDAR:example.org', 'ACT_REJECT_IF_CONFLICT' ]
accept_avail = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT:example.org' ]
- self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual }, 'user at domain.org'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none }, 'user at domain.org'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all }, 'user at domain.com'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond }, 'user at domain.com'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'user at domain.com'))
- self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'sam at example.org'))
- self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user at domain.com'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john at example.org'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual }, 'user at domain.org', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none }, 'user at domain.org', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all }, 'user at domain.com', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond }, 'user at domain.com', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'user at domain.com', 'event'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'sam at example.org', 'event'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user at domain.com', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john at example.org', 'event'))
\ No newline at end of file
diff --git a/tests/unit/test-016-todo.py b/tests/unit/test-016-todo.py
index a7e9394..c6a1178 100644
--- a/tests/unit/test-016-todo.py
+++ b/tests/unit/test-016-todo.py
@@ -18,7 +18,6 @@ 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
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 8e77335..e753c38 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -41,30 +41,32 @@ from pykolab.auth import Auth
from pykolab.conf import Conf
from pykolab.imap import IMAP
from pykolab.xml import to_dt
+from pykolab.xml import todo_from_message
from pykolab.xml import event_from_message
from pykolab.xml import participant_status_label
-from pykolab.itip import events_from_message
+from pykolab.itip import objects_from_message
from pykolab.itip import check_event_conflict
from pykolab.itip import send_reply
from pykolab.translate import _
# define some contstants used in the code below
-COND_IF_AVAILABLE = 32
-COND_IF_CONFLICT = 64
-COND_TENTATIVE = 128
-COND_NOTIFY = 256
ACT_MANUAL = 1
ACT_ACCEPT = 2
ACT_DELEGATE = 4
ACT_REJECT = 8
ACT_UPDATE = 16
-ACT_TENTATIVE = ACT_ACCEPT + COND_TENTATIVE
-ACT_ACCEPT_IF_NO_CONFLICT = ACT_ACCEPT + COND_IF_AVAILABLE
-ACT_TENTATIVE_IF_NO_CONFLICT = ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE
-ACT_DELEGATE_IF_CONFLICT = ACT_DELEGATE + COND_IF_CONFLICT
-ACT_REJECT_IF_CONFLICT = ACT_REJECT + COND_IF_CONFLICT
-ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY
-ACT_SAVE_TO_CALENDAR = 512
+ACT_SAVE_TO_FOLDER = 32
+
+COND_IF_AVAILABLE = 64
+COND_IF_CONFLICT = 128
+COND_TENTATIVE = 256
+COND_NOTIFY = 512
+COND_TYPE_EVENT = 1024
+COND_TYPE_TASK = 2048
+COND_TYPE_ALL = COND_TYPE_EVENT + COND_TYPE_TASK
+
+ACT_TENTATIVE = ACT_ACCEPT + COND_TENTATIVE
+ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY
FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type'
@@ -72,21 +74,56 @@ MESSAGE_PROCESSED = 1
MESSAGE_FORWARD = 2
policy_name_map = {
- 'ACT_MANUAL': ACT_MANUAL,
- 'ACT_ACCEPT': ACT_ACCEPT,
- 'ACT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT_IF_NO_CONFLICT,
- 'ACT_TENTATIVE': ACT_TENTATIVE,
- 'ACT_TENTATIVE_IF_NO_CONFLICT': ACT_TENTATIVE_IF_NO_CONFLICT,
- 'ACT_DELEGATE': ACT_DELEGATE,
- 'ACT_DELEGATE_IF_CONFLICT': ACT_DELEGATE_IF_CONFLICT,
- 'ACT_REJECT': ACT_REJECT,
- 'ACT_REJECT_IF_CONFLICT': ACT_REJECT_IF_CONFLICT,
- 'ACT_UPDATE': ACT_UPDATE,
- 'ACT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY,
- 'ACT_SAVE_TO_CALENDAR': ACT_SAVE_TO_CALENDAR
+ # policy values applying to all object types
+ 'ALL_MANUAL': ACT_MANUAL + COND_TYPE_ALL,
+ 'ALL_ACCEPT': ACT_ACCEPT + COND_TYPE_ALL,
+ 'ALL_REJECT': ACT_REJECT + COND_TYPE_ALL,
+ 'ALL_DELEGATE': ACT_DELEGATE + COND_TYPE_ALL, # not implemented
+ 'ALL_UPDATE': ACT_UPDATE + COND_TYPE_ALL,
+ 'ALL_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
+ 'ALL_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_ALL,
+ # event related policy values
+ 'EVENT_MANUAL': ACT_MANUAL + COND_TYPE_EVENT,
+ 'EVENT_ACCEPT': ACT_ACCEPT + COND_TYPE_EVENT,
+ 'EVENT_TENTATIVE': ACT_TENTATIVE + COND_TYPE_EVENT,
+ 'EVENT_REJECT': ACT_REJECT + COND_TYPE_EVENT,
+ 'EVENT_DELEGATE': ACT_DELEGATE + COND_TYPE_EVENT, # not implemented
+ 'EVENT_UPDATE': ACT_UPDATE + COND_TYPE_EVENT,
+ 'EVENT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_EVENT,
+ 'EVENT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+ 'EVENT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+ 'EVENT_DELEGATE_IF_CONFLICT': ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
+ 'EVENT_REJECT_IF_CONFLICT': ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
+ 'EVENT_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
+ # task related policy values
+ 'TASK_MANUAL': ACT_MANUAL + COND_TYPE_TASK,
+ 'TASK_ACCEPT': ACT_ACCEPT + COND_TYPE_TASK,
+ 'TASK_REJECT': ACT_REJECT + COND_TYPE_TASK,
+ 'TASK_DELEGATE': ACT_DELEGATE + COND_TYPE_TASK, # not implemented
+ 'TASK_UPDATE': ACT_UPDATE + COND_TYPE_TASK,
+ 'TASK_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_TASK,
+ 'TASK_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_TASK,
+ # legacy values
+ 'ACT_MANUAL': ACT_MANUAL + COND_TYPE_ALL,
+ 'ACT_ACCEPT': ACT_ACCEPT + COND_TYPE_ALL,
+ 'ACT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+ 'ACT_TENTATIVE': ACT_TENTATIVE + COND_TYPE_EVENT,
+ 'ACT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+ 'ACT_DELEGATE': ACT_DELEGATE + COND_TYPE_ALL,
+ 'ACT_DELEGATE_IF_CONFLICT': ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
+ 'ACT_REJECT': ACT_REJECT + COND_TYPE_ALL,
+ 'ACT_REJECT_IF_CONFLICT': ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
+ 'ACT_UPDATE': ACT_UPDATE + COND_TYPE_ALL,
+ 'ACT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
+ 'ACT_SAVE_TO_CALENDAR': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
}
-policy_value_map = dict([(v, k) for (k, v) in policy_name_map.iteritems()])
+policy_value_map = dict([(v &~ COND_TYPE_ALL, k) for (k, v) in policy_name_map.iteritems()])
+
+object_type_conditons = {
+ 'event': COND_TYPE_EVENT,
+ 'task': COND_TYPE_TASK
+}
log = pykolab.getLogger('pykolab.wallace')
conf = pykolab.getConf()
@@ -210,17 +247,17 @@ def execute(*args, **kw):
# An iTip message may contain multiple events. Later on, test if the message
# is an iTip message by checking the length of this list.
try:
- itip_events = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL'])
+ itip_events = objects_from_message(message, ['VEVENT','VTODO'], ['REQUEST', 'REPLY', 'CANCEL'])
except Exception, e:
- log.error(_("Failed to parse iTip events from message: %r" % (e)))
+ log.error(_("Failed to parse iTip objects from message: %r" % (e)))
itip_events = []
if not len(itip_events) > 0:
- log.info(_("Message is not an iTip message or does not contain any (valid) iTip events."))
+ log.info(_("Message is not an iTip message or does not contain any (valid) iTip objects."))
else:
any_itips = True
- log.debug(_("iTip events attached to this message contain the following information: %r") % (itip_events), level=9)
+ log.debug(_("iTip objects attached to this message contain the following information: %r") % (itip_events), level=9)
# See if any iTip actually allocates a user.
if any_itips and len([x['uid'] for x in itip_events if x.has_key('attendees') or x.has_key('organizer')]) > 0:
@@ -239,7 +276,7 @@ def execute(*args, **kw):
log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5)
return filepath
- # we're looking at the first itip event object
+ # we're looking at the first itip object
itip_event = itip_events[0]
# for replies, the organizer is the recipient
@@ -267,7 +304,8 @@ def execute(*args, **kw):
pykolab.translate.setUserLanguage(receiving_user['preferredlanguage'])
# find user's kolabInvitationPolicy settings and the matching policy values
- policies = get_matching_invitation_policies(receiving_user, sender_email)
+ type_condition = object_type_conditons.get(itip_event['type'], COND_TYPE_ALL)
+ policies = get_matching_invitation_policies(receiving_user, sender_email, type_condition)
# select a processing function according to the iTip request method
method_processing_map = {
@@ -326,31 +364,32 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
return MESSAGE_FORWARD
# process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
+ is_task = itip_event['type'] == 'task'
nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant
partstat = receiving_attendee.get_participant_status()
- save_event = not nonpart or not partstat == kolabformat.PartNeedsAction
+ save_object = not nonpart or not partstat == kolabformat.PartNeedsAction
rsvp = receiving_attendee.get_rsvp()
scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction
respond_with = receiving_attendee.get_participant_status(True)
condition_fulfilled = True
# find existing event in user's calendar
- existing = find_existing_event(itip_event['uid'], receiving_user, True)
+ existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
# compare sequence number to determine a (re-)scheduling request
if existing is not None:
- log.debug(_("Existing event: %r") % (existing), level=9)
+ log.debug(_("Existing %s: %r") % (existing.type, existing), level=9)
scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence()
- save_event = True
+ save_object = True
- # if scheduling: check availability
+ # if scheduling: check availability (skip that for tasks)
if scheduling_required:
- if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
+ if not is_task and policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
condition_fulfilled = check_availability(itip_event, receiving_user)
- if policy & COND_IF_CONFLICT:
+ if not is_task and policy & COND_IF_CONFLICT:
condition_fulfilled = not condition_fulfilled
- log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
+ log.debug(_("Precondition for object %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
respond_with = None
if policy & ACT_ACCEPT and condition_fulfilled:
@@ -373,13 +412,13 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
# send iTip reply
if respond_with is not None:
receiving_attendee.set_participant_status(respond_with)
- send_reply(recipient_email, itip_event, invitation_response_text(),
+ send_reply(recipient_email, itip_event, invitation_response_text(itip_event['type']),
subject=_('"%(summary)s" has been %(status)s'))
- elif policy & ACT_SAVE_TO_CALENDAR:
- # copy the invitation into the user's calendar with PARTSTAT=NEEDS-ACTION
+ elif policy & ACT_SAVE_TO_FOLDER:
+ # copy the invitation into the user's default folder with PARTSTAT=NEEDS-ACTION
itip_event['xml'].set_attendee_participant_status(receiving_attendee, 'NEEDS-ACTION')
- save_event = True
+ save_object = True
else:
# policy doesn't match, pass on to next one
@@ -389,17 +428,17 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
log.debug(_("No RSVP for recipient %r requested") % (receiving_user['mail']), level=8)
# TODO: only update if policy & ACT_UPDATE ?
- if save_event:
+ if save_object:
targetfolder = None
if existing:
# delete old version from IMAP
targetfolder = existing._imap_folder
- delete_event(existing)
+ delete_object(existing)
if not nonpart or existing:
# save new copy from iTip
- if store_event(itip_event['xml'], receiving_user, targetfolder):
+ if store_object(itip_event['xml'], receiving_user, targetfolder):
return MESSAGE_PROCESSED
return None
@@ -426,23 +465,23 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
# find existing event in user's calendar
# sets/checks lock to avoid concurrent wallace processes trying to update the same event simultaneously
- existing = find_existing_event(itip_event['uid'], receiving_user, True)
+ existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
if existing:
# compare sequence number to avoid outdated replies?
if not itip_event['sequence'] == existing.get_sequence():
- log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Forwarding to Inbox.") % (
+ log.info(_("The iTip reply sequence (%r) doesn't match the referred object version (%r). Forwarding to Inbox.") % (
itip_event['sequence'], existing.get_sequence()
))
remove_write_lock(existing._lock_key)
return MESSAGE_FORWARD
- log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
+ log.debug(_("Auto-updating %s %r on iTip REPLY") % (existing.type, existing.uid), level=8)
try:
existing_attendee = existing.get_attendee(sender_email)
existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), rsvp=False)
except Exception, e:
- log.error("Could not find corresponding attende in organizer's event: %r" % (e))
+ log.error("Could not find corresponding attende in organizer's copy: %r" % (e))
# append delegated-from attendee ?
if len(sender_attendee.get_delegated_from()) > 0:
@@ -475,19 +514,19 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
except Exception, e:
log.error("Could not find delegated-to attendee: %r" % (e))
- # update the organizer's copy of the event
- if update_event(existing, receiving_user):
+ # update the organizer's copy of the object
+ if update_object(existing, receiving_user):
if policy & COND_NOTIFY:
send_reply_notification(existing, receiving_user)
# update all other attendee's copies
if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
- propagate_changes_to_attendees_calendars(existing)
+ propagate_changes_to_attendees_accounts(existing)
return MESSAGE_PROCESSED
else:
- log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+ log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox."))
return MESSAGE_FORWARD
return None
@@ -505,18 +544,21 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei
# auto-update the local copy with STATUS=CANCELLED
if policy & ACT_UPDATE:
- # find existing event in user's calendar
- existing = find_existing_event(itip_event['uid'], receiving_user, True)
+ # find existing object in user's folders
+ existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
if existing:
existing.set_status('CANCELLED')
existing.set_transparency(True)
- if update_event(existing, receiving_user):
- # TODO: send cancellation notification if policy & ACT_UPDATE_AND_NOTIFY: ?
+ if update_object(existing, receiving_user):
+ # send cancellation notification
+ if policy & ACT_UPDATE_AND_NOTIFY:
+ send_cancel_notification(existing, receiving_user)
+
return MESSAGE_PROCESSED
else:
- log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+ log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox."))
return MESSAGE_FORWARD
return None
@@ -562,7 +604,7 @@ def user_dn_from_email_address(email_address):
user_dn_from_email_address.cache = {}
-def get_matching_invitation_policies(receiving_user, sender_email):
+def get_matching_invitation_policies(receiving_user, sender_email, type_condition=COND_TYPE_ALL):
# get user's kolabInvitationPolicy settings
policies = receiving_user['kolabinvitationpolicy'] if receiving_user.has_key('kolabinvitationpolicy') else []
if policies and not isinstance(policies, list):
@@ -583,7 +625,10 @@ def get_matching_invitation_policies(receiving_user, sender_email):
if domain == '' or domain == '*' or str(sender_email).endswith(domain):
value = value.upper()
if policy_name_map.has_key(value):
- matches.append(policy_name_map[value])
+ val = policy_name_map[value]
+ # append if type condition matches
+ if val & type_condition:
+ matches.append(val &~ COND_TYPE_ALL)
# add manual as default action
if len(matches) == 0:
@@ -594,7 +639,7 @@ def get_matching_invitation_policies(receiving_user, sender_email):
def imap_proxy_auth(user_rec):
"""
-
+ Perform IMAP login using proxy authentication with admin credentials
"""
global imap
@@ -624,23 +669,23 @@ def imap_proxy_auth(user_rec):
return True
-def list_user_calendars(user_rec):
+def list_user_folders(user_rec, type):
"""
- Get a list of the given user's private calendar folders
+ Get a list of the given user's private calendar/tasks folders
"""
global imap
# return cached list
- if user_rec.has_key('_calendar_folders'):
- return user_rec['_calendar_folders'];
+ if user_rec.has_key('_imap_folders'):
+ return user_rec['_imap_folders'];
- calendars = []
+ result = []
if not imap_proxy_auth(user_rec):
- return calendars
+ return result
folders = imap.list_folders('*')
- log.debug(_("List calendar folders for user %r: %r") % (user_rec['mail'], folders), level=8)
+ log.debug(_("List %r folders for user %r: %r") % (type, user_rec['mail'], folders), level=8)
(ns_personal, ns_other, ns_shared) = imap.namespaces()
@@ -658,23 +703,23 @@ def list_user_calendars(user_rec):
metadata = imap.get_metadata(folder)
log.debug(_("IMAP metadata for %r: %r") % (folder, metadata), level=9)
if metadata.has_key(folder) and ( \
- metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith('event') \
- or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith('event')):
- calendars.append(folder)
+ metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith(type) \
+ or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith(type)):
+ result.append(folder)
- # store default calendar folder in user record
+ # store default folder folder in user record
if metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].endswith('.default'):
- user_rec['_default_calendar'] = folder
+ user_rec['_default_folder'] = folder
# cache with user record
- user_rec['_calendar_folders'] = calendars
+ user_rec['_imap_folders'] = result
- return calendars
+ return result
-def find_existing_event(uid, user_rec, lock=False):
+def find_existing_object(uid, type, user_rec, lock=False):
"""
- Search user's calendar folders for the given event (by UID)
+ Search user's private folders for the given object (by UID+type)
"""
global imap
@@ -685,8 +730,8 @@ def find_existing_event(uid, user_rec, lock=False):
set_write_lock(lock_key)
event = None
- for folder in list_user_calendars(user_rec):
- log.debug(_("Searching folder %r for event %r") % (folder, uid), level=8)
+ for folder in list_user_folders(user_rec, type):
+ log.debug(_("Searching folder %r for %s %r") % (folder, type, uid), level=8)
imap.imap.m.select(imap.folder_utf7(folder))
typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid))
@@ -694,11 +739,15 @@ def find_existing_event(uid, user_rec, lock=False):
typ, data = imap.imap.m.fetch(num, '(RFC822)')
try:
- event = event_from_message(message_from_string(data[0][1]))
+ if type == 'task':
+ event = todo_from_message(message_from_string(data[0][1]))
+ else:
+ event = event_from_message(message_from_string(data[0][1]))
+
setattr(event, '_imap_folder', folder)
setattr(event, '_lock_key', lock_key)
except Exception, e:
- log.error(_("Failed to parse event from message %s/%s: %s") % (folder, num, traceback.format_exc()))
+ log.error(_("Failed to parse %s from message %s/%s: %s") % (type, folder, num, traceback.format_exc()))
continue
if event and event.uid == uid:
@@ -723,7 +772,7 @@ def check_availability(itip_event, receiving_user):
if itip_event.has_key('_conflicts'):
return not itip_event['_conflicts']
- for folder in list_user_calendars(receiving_user):
+ for folder in list_user_folders(receiving_user, 'event'):
log.debug(_("Listing events from folder %r") % (folder), level=8)
imap.imap.m.select(imap.folder_utf7(folder))
@@ -810,39 +859,40 @@ def get_lock_key(user, uid):
return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest()
-def update_event(event, user_rec):
+def update_object(object, user_rec):
"""
- Update the given event in IMAP (i.e. delete + append)
+ Update the given object in IMAP (i.e. delete + append)
"""
success = False
- if hasattr(event, '_imap_folder'):
- delete_event(event)
- success = store_event(event, user_rec, event._imap_folder)
+ if hasattr(object, '_imap_folder'):
+ delete_object(object)
+ object.set_lastmodified() # update last-modified timestamp
+ success = store_object(object, user_rec, object._imap_folder)
# remove write lock for this event
- if hasattr(event, '_lock_key') and event._lock_key is not None:
- remove_write_lock(event._lock_key)
+ if hasattr(object, '_lock_key') and object._lock_key is not None:
+ remove_write_lock(object._lock_key)
return success
-def store_event(event, user_rec, targetfolder=None):
+def store_object(object, user_rec, targetfolder=None):
"""
- Append the given event object to the user's default calendar
+ Append the given object to the user's default calendar/tasklist
"""
-
- # find default calendar folder to save event to
+
+ # find default calendar folder to save object to
if targetfolder is None:
- targetfolder = list_user_calendars(user_rec)[0]
- if user_rec.has_key('_default_calendar'):
- targetfolder = user_rec['_default_calendar']
+ targetfolder = list_user_folders(user_rec, object.type)[0]
+ if user_rec.has_key('_default_folder'):
+ targetfolder = user_rec['_default_folder']
if not targetfolder:
- log.error(_("Failed to save event: no calendar folder found for user %r") % (user_rec['mail']))
+ log.error(_("Failed to save %s: no target folder found for user %r") % (object.type, user_rec['mail']))
return Fasle
- log.debug(_("Save event %r to user calendar %r") % (event.uid, targetfolder), level=8)
+ log.debug(_("Save %s %r to user folder %r") % (object.type, object.uid, targetfolder), level=8)
try:
imap.imap.m.select(imap.folder_utf7(targetfolder))
@@ -850,29 +900,29 @@ def store_event(event, user_rec, targetfolder=None):
imap.folder_utf7(targetfolder),
None,
None,
- event.to_message(creator="Kolab Server <wallace at localhost>").as_string()
+ object.to_message(creator="Kolab Server <wallace at localhost>").as_string()
)
return result
except Exception, e:
- log.error(_("Failed to save event to user calendar at %r: %r") % (
- targetfolder, e
+ log.error(_("Failed to save %s to user folder at %r: %r") % (
+ object.type, targetfolder, e
))
return False
-def delete_event(existing):
+def delete_object(existing):
"""
- Removes the IMAP object with the given UID from a user's calendar folder
+ Removes the IMAP object with the given UID from a user's folder
"""
targetfolder = existing._imap_folder
imap.imap.m.select(imap.folder_utf7(targetfolder))
typ, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % existing.uid)
- log.debug(_("Delete event %r in %r: %r") % (
- existing.uid, targetfolder, data
+ log.debug(_("Delete %s %r in %r: %r") % (
+ existing.type, existing.uid, targetfolder, data
), level=8)
for num in data[0].split():
@@ -881,7 +931,7 @@ def delete_event(existing):
imap.imap.m.expunge()
-def send_reply_notification(event, receiving_user):
+def send_reply_notification(object, receiving_user):
"""
Send a (consolidated) notification about the current participant status to organizer
"""
@@ -891,18 +941,18 @@ def send_reply_notification(event, receiving_user):
from email.MIMEText import MIMEText
from email.Utils import formatdate
- log.debug(_("Compose participation status summary for event %r to user %r") % (
- event.uid, receiving_user['mail']
+ log.debug(_("Compose participation status summary for %s %r to user %r") % (
+ object.type, object.uid, receiving_user['mail']
), level=8)
- organizer = event.get_organizer()
+ organizer = object.get_organizer()
orgemail = organizer.email()
orgname = organizer.name()
auto_replies_expected = 0
auto_replies_received = 0
- partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] }
- for attendee in event.get_attendees():
+ partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] }
+ for attendee in object.get_attendees():
parstat = attendee.get_participant_status(True)
if partstats.has_key(parstat):
partstats[parstat].append(attendee.get_displayname())
@@ -921,7 +971,7 @@ def send_reply_notification(event, receiving_user):
if attendee_dn:
attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
- if is_auto_reply(attendee_rec, orgemail):
+ if is_auto_reply(attendee_rec, orgemail, object.type):
auto_replies_expected += 1
if not parstat == 'NEEDS-ACTION':
auto_replies_received += 1
@@ -938,21 +988,33 @@ def send_reply_notification(event, receiving_user):
if len(attendees) > 0:
roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
- message_text = """
- The event '%(summary)s' at %(start)s has been updated in your calendar.
- %(roundup)s
- """ % {
- 'summary': event.get_summary(),
- 'start': event.get_start().strftime('%Y-%m-%d %H:%M %Z'),
- 'roundup': roundup
- }
+ # compose different notification texts for events/tasks
+ if object.type == 'task':
+ message_text = """
+ The assignment for '%(summary)s' has been updated in your tasklist.
+ %(roundup)s
+ """ % {
+ 'summary': object.get_summary(),
+ 'roundup': roundup
+ }
+ else:
+ message_text = """
+ The event '%(summary)s' at %(start)s has been updated in your calendar.
+ %(roundup)s
+ """ % {
+ 'summary': object.get_summary(),
+ 'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+ 'roundup': roundup
+ }
+
+ message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
# compose mime message
msg = MIMEText(utils.stripped_message(message_text))
msg['To'] = receiving_user['mail']
msg['Date'] = formatdate(localtime=True)
- msg['Subject'] = _('"%s" has been updated') % (event.get_summary())
+ msg['Subject'] = _('"%s" has been updated') % (object.get_summary())
msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
smtp = smtplib.SMTP("localhost", 10027)
@@ -968,10 +1030,68 @@ def send_reply_notification(event, receiving_user):
smtp.quit()
-def is_auto_reply(user, sender_email):
+def send_cancel_notification(object, receiving_user):
+ """
+ Send a notification about event/task cancellation
+ """
+ import smtplib
+ from email.MIMEText import MIMEText
+ from email.Utils import formatdate
+
+ log.debug(_("Send cancellation notification for %s %r to user %r") % (
+ object.type, object.uid, receiving_user['mail']
+ ), level=8)
+
+ organizer = object.get_organizer()
+ orgemail = organizer.email()
+ orgname = organizer.name()
+
+ # compose different notification texts for events/tasks
+ if object.type == 'task':
+ message_text = """
+ The assignment for '%(summary)s' has been cancelled by %(organizer)s.
+ The copy in your tasklist as been marked as cancelled accordingly.
+ """ % {
+ 'summary': object.get_summary(),
+ 'organizer': orgname if orgname else orgemail
+ }
+ else:
+ message_text = """
+ The event '%(summary)s' at %(start)s has been cancelled by %(organizer)s.
+ The copy in your calendar as been marked as cancelled accordingly.
+ """ % {
+ 'summary': object.get_summary(),
+ 'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+ 'organizer': orgname if orgname else orgemail
+ }
+
+ message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
+
+ # compose mime message
+ msg = MIMEText(utils.stripped_message(message_text))
+
+ msg['To'] = receiving_user['mail']
+ msg['Date'] = formatdate(localtime=True)
+ msg['Subject'] = _('"%s" has been cancelled') % (object.get_summary())
+ msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
+
+ smtp = smtplib.SMTP("localhost", 10027)
+
+ if conf.debuglevel > 8:
+ smtp.set_debuglevel(True)
+
+ try:
+ smtp.sendmail(orgemail, receiving_user['mail'], msg.as_string())
+ except Exception, e:
+ log.error(_("SMTP sendmail error: %r") % (e))
+
+ smtp.quit()
+
+
+def is_auto_reply(user, sender_email, type):
accept_available = False
accept_conflicts = False
- for policy in get_matching_invitation_policies(user, sender_email):
+ for policy in get_matching_invitation_policies(user, sender_email, object_type_conditons.get(type, COND_TYPE_EVENT)):
if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE):
if check_policy_condition(policy, True):
accept_available = True
@@ -983,7 +1103,7 @@ def is_auto_reply(user, sender_email):
return True
# manual action reached
- if policy & (ACT_MANUAL | ACT_SAVE_TO_CALENDAR):
+ if policy & (ACT_MANUAL | ACT_SAVE_TO_FOLDER):
return False
return False
@@ -998,45 +1118,46 @@ def check_policy_condition(policy, available):
return condition_fulfilled
-def propagate_changes_to_attendees_calendars(event):
+def propagate_changes_to_attendees_accounts(object):
"""
- Find and update copies of this event in all attendee's calendars
+ Find and update copies of this object in all attendee's personal folders
"""
- for attendee in event.get_attendees():
+ for attendee in object.get_attendees():
attendee_user_dn = user_dn_from_email_address(attendee.get_email())
if attendee_user_dn:
attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*'])
- attendee_event = find_existing_event(event.uid, attendee_user, True) # does IMAP authenticate
- if attendee_event:
+ attendee_object = find_existing_object(object.uid, object.type, attendee_user, True) # does IMAP authenticate
+ if attendee_object:
try:
- attendee_entry = attendee_event.get_attendee_by_email(attendee_user['mail'])
+ attendee_entry = attendee_object.get_attendee_by_email(attendee_user['mail'])
except:
attendee_entry = None
- # copy all attendees from master event (covers additions and removals)
+ # copy all attendees from master object (covers additions and removals)
new_attendees = kolabformat.vectorattendee();
- for a in event.get_attendees():
+ for a in object.get_attendees():
# keep my own entry intact
if attendee_entry is not None and attendee_entry.get_email() == a.get_email():
new_attendees.append(attendee_entry)
else:
new_attendees.append(a)
- attendee_event.event.setAttendees(new_attendees)
+ attendee_object.event.setAttendees(new_attendees)
- success = update_event(attendee_event, attendee_user)
- log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], event.uid, success), level=8)
+ success = update_object(attendee_object, attendee_user)
+ log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], object.uid, success), level=8)
else:
- log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], event.uid), level=8)
+ log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], object.uid), level=8)
else:
log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8)
-def invitation_response_text():
- return _("""
- %(name)s has %(status)s your invitation for %(summary)s.
+def invitation_response_text(type):
+ footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***")
- *** This is an automated response sent by the Kolab Invitation system ***
- """)
+ if type == 'task':
+ return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer
+ else:
+ return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer
commit c5978eb22295fea9a09066dc177a5d167c58fb2c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Aug 21 07:14:10 2014 -0400
Make sure created and last-modified dates are saved in UTC; add folder type property to groupware objects
diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py
index 9a2c103..97987d9 100644
--- a/pykolab/xml/contact.py
+++ b/pykolab/xml/contact.py
@@ -1,6 +1,8 @@
import kolabformat
class Contact(kolabformat.Contact):
+ type = 'contact'
+
def __init__(self, *args, **kw):
kolabformat.Contact.__init__(self, *args, **kw)
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 34f857a..24b026e 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -43,6 +43,8 @@ def event_from_message(message):
class Event(object):
+ type = 'event'
+
status_map = {
"TENTATIVE": kolabformat.StatusTentative,
"CONFIRMED": kolabformat.StatusConfirmed,
@@ -612,9 +614,9 @@ class Event(object):
def set_created(self, _datetime=None):
if _datetime == None:
- _datetime = datetime.datetime.now()
+ _datetime = datetime.datetime.utcnow()
- self.event.setCreated(xmlutils.to_cdatetime(_datetime, False))
+ self.event.setCreated(xmlutils.to_cdatetime(_datetime, False, True))
def set_description(self, description):
self.event.setDescription(str(description))
@@ -624,7 +626,7 @@ class Event(object):
self.event.setComment(str(comment))
def set_dtstamp(self, _datetime):
- self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
+ self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True))
def set_end(self, _datetime):
valid_datetime = False
@@ -771,12 +773,12 @@ class Event(object):
if _datetime == None:
valid_datetime = True
- _datetime = datetime.datetime.now()
+ _datetime = datetime.datetime.utcnow()
if not valid_datetime:
raise InvalidEventDateError, _("Event start needs datetime.date or datetime.datetime instance")
- self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
+ self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True))
def set_location(self, location):
self.event.setLocation(str(location))
@@ -938,7 +940,7 @@ class Event(object):
msg['Date'] = formatdate(localtime=True)
msg.add_header('X-Kolab-MIME-Version', '3.0')
- msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.event')
+ msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.' + self.type)
text = utils.multiline_message("""
This is a Kolab Groupware object. To view this object you
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
index 28a7b4d..b04b233 100644
--- a/pykolab/xml/todo.py
+++ b/pykolab/xml/todo.py
@@ -34,6 +34,7 @@ def todo_from_message(message):
# FIXME: extend a generic pykolab.xml.Xcal class instead of Event
class Todo(Event):
+ type = 'task'
def __init__(self, from_ical="", from_string=""):
self._attendees = []
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index bcaa480..aa05e11 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -62,10 +62,18 @@ def from_cdatetime(_cdatetime, with_timezone=True):
return datetime.datetime(year, month, day, hour, minute, second)
-def to_cdatetime(_datetime, with_timezone=True):
+def to_cdatetime(_datetime, with_timezone=True, as_utc=False):
"""
Convert a datetime.dateime object into a kolabformat.cDateTime instance
"""
+ # convert date into UTC timezone
+ if as_utc and hasattr(_datetime, "tzinfo"):
+ if _datetime.tzinfo is not None:
+ _datetime = _datetime.astimezone(pytz.utc)
+ else:
+ datetime = _datetime.replace(tzinfo=pytz.utc)
+ with_timezone = False
+
(
year,
month,
@@ -97,4 +105,7 @@ def to_cdatetime(_datetime, with_timezone=True):
else:
_cdatetime.setTimezone(_datetime.tzinfo.__str__())
+ if as_utc:
+ _cdatetime.setUTC(True)
+
return _cdatetime
More information about the commits
mailing list