summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2015-03-11 15:44:44 (GMT)
committerThomas Bruederli <bruederli@kolabsys.com>2015-03-11 15:44:44 (GMT)
commit9e267beaea760608393960850f59fca7aed47522 (patch)
treea517a9fd72168a30961c93e22ce766c1b88a8ff0
parente0e64a93a67d03bef200a10417d4334198247aa4 (diff)
downloadpykolab-9e267beaea760608393960850f59fca7aed47522.tar.gz
New *_CANCEL_DELETE policy to remove cancelled objects rather than flagging as cancelled (#4306)
-rw-r--r--pykolab/xml/event.py17
-rw-r--r--tests/functional/test_wallace/test_007_invitationpolicy.py51
-rw-r--r--wallace/module_invitationpolicy.py84
3 files changed, 124 insertions, 28 deletions
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 4e36595..5080e43 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -208,6 +208,23 @@ class Event(object):
self.event.setExceptions(vexceptions)
+ def del_exception(self, exception):
+ recurrence_id = exception.get_recurrence_id()
+ if recurrence_id is None:
+ raise EventIntegrityError, "Recurrence exceptions require a Recurrence-ID property"
+
+ updated = False
+ vexceptions = self.event.exceptions()
+ for i, ex in enumerate(self._exceptions):
+ if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture:
+ del vexceptions[i]
+ del self._exceptions[i]
+ updated = True
+
+ if updated:
+ self.event.setExceptions(vexceptions)
+
+
def as_string_itip(self, method="REQUEST"):
cal = icalendar.Calendar()
cal.add(
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index a019949..f019a91 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -263,7 +263,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
'kolabcalendarfolder': 'user/jane.manager/Calendar@example.org',
'kolabtasksfolder': 'user/jane.manager/Tasks@example.org',
'kolabconfidentialcalendar': 'user/jane.manager/Calendar/Confidential@example.org',
- 'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','TASK_ACCEPT','ACT_UPDATE']
+ 'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','TASK_ACCEPT','TASK_UPDATE_AND_NOTIFY','ACT_UPDATE']
}
self.jack = {
@@ -296,7 +296,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
'mailbox': 'user/lucy.meyer@example.org',
'kolabcalendarfolder': 'user/lucy.meyer/Calendar@example.org',
'kolabtasksfolder': 'user/lucy.meyer/Tasks@example.org',
- 'kolabinvitationpolicy': ['ALL_SAVE_AND_FORWARD','ACT_UPDATE_AND_NOTIFY']
+ 'kolabinvitationpolicy': ['ALL_SAVE_AND_FORWARD','ACT_CANCEL_DELETE_AND_NOTIFY','ACT_UPDATE_AND_NOTIFY']
}
self.bill = {
@@ -885,6 +885,22 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.assertTrue(event.get_transparency())
+ def test_007_invitation_cancel_and_delete(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ uid = self.send_itip_invitation(self.lucy['mail'], summary="cancel-delete")
+ event = self.check_user_calendar_event(self.lucy['kolabcalendarfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+
+ self.send_itip_cancel(self.lucy['mail'], uid, summary="cancel-delete")
+
+ response = self.check_message_received(_('"%s" has been cancelled') % ('cancel-delete'), self.john['mail'], mailbox=self.lucy['mailbox'])
+ self.assertIsInstance(response, email.message.Message)
+
+ # verify event was removed from the user's calendar
+ self.assertEqual(self.check_user_calendar_event(self.lucy['kolabcalendarfolder'], uid), None)
+
+
def test_008_inivtation_reply_notify(self):
self.purge_mailbox(self.john['mailbox'])
@@ -1231,6 +1247,37 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
response = self.check_message_received(self.itip_reply_subject % { 'summary':'new booking', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
+ def test_017_cancel_delete_single_occurrence(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ start = datetime.datetime(2015,3,24, 13,0,0, tzinfo=pytz.timezone("Europe/Zurich"))
+ uid = self.send_itip_invitation(self.lucy['mail'], summary="recurring cancel-delete", start=start, template=itip_recurring)
+
+ event = self.check_user_calendar_event(self.lucy['kolabcalendarfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+
+ # send update to a single instance with the same sequence: no re-scheduling
+ exdate = start + datetime.timedelta(days=14)
+ exstart = exdate + datetime.timedelta(hours=5)
+ self.send_itip_update(self.lucy['mail'], uid, exstart, summary="recurring rescheduled", sequence=1, partstat='NEEDS-ACTION', instance=exdate)
+
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.lucy['kolabcalendarfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(len(event.get_exceptions()), 1)
+
+ # send cancellation for exception
+ self.send_itip_cancel(self.lucy['mail'], uid, summary="recurring rescheduled", instance=exdate)
+
+ response = self.check_message_received(_('"%s" has been cancelled') % ('recurring rescheduled'), self.john['mail'], mailbox=self.lucy['mailbox'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_user_calendar_event(self.lucy['kolabcalendarfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(len(event.get_exception_dates()), 1)
+ self.assertEqual(event.get_exception_dates()[0].strftime("%Y-%m-%d"), exdate.strftime("%Y-%m-%d"))
+ self.assertEqual(len(event.get_exceptions()), 0)
+
def test_017_cancel_thisandfuture(self):
self.purge_mailbox(self.john['mailbox'])
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index ee96634..3c5c381 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -57,20 +57,22 @@ ACT_ACCEPT = 2
ACT_DELEGATE = 4
ACT_REJECT = 8
ACT_UPDATE = 16
-ACT_SAVE_TO_FOLDER = 32
-
-COND_IF_AVAILABLE = 64
-COND_IF_CONFLICT = 128
-COND_TENTATIVE = 256
-COND_NOTIFY = 512
-COND_FORWARD = 4096
-COND_TYPE_EVENT = 1024
-COND_TYPE_TASK = 2048
+ACT_CANCEL_DELETE = 32
+ACT_SAVE_TO_FOLDER = 64
+
+COND_IF_AVAILABLE = 128
+COND_IF_CONFLICT = 256
+COND_TENTATIVE = 512
+COND_NOTIFY = 1024
+COND_FORWARD = 2048
+COND_TYPE_EVENT = 4096
+COND_TYPE_TASK = 8192
COND_TYPE_ALL = COND_TYPE_EVENT + COND_TYPE_TASK
ACT_TENTATIVE = ACT_ACCEPT + COND_TENTATIVE
ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY
ACT_SAVE_AND_FORWARD = ACT_SAVE_TO_FOLDER + COND_FORWARD
+ACT_CANCEL_DELETE_AND_NOTIFY = ACT_CANCEL_DELETE + COND_NOTIFY
FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type'
@@ -87,6 +89,8 @@ policy_name_map = {
'ALL_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
'ALL_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_ALL,
'ALL_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_ALL,
+ 'ALL_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_ALL,
+ 'ALL_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL,
# event related policy values
'EVENT_MANUAL': ACT_MANUAL + COND_TYPE_EVENT,
'EVENT_ACCEPT': ACT_ACCEPT + COND_TYPE_EVENT,
@@ -101,6 +105,8 @@ policy_name_map = {
'EVENT_REJECT_IF_CONFLICT': ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
'EVENT_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
'EVENT_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT,
+ 'EVENT_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_EVENT,
+ 'EVENT_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_EVENT,
# task related policy values
'TASK_MANUAL': ACT_MANUAL + COND_TYPE_TASK,
'TASK_ACCEPT': ACT_ACCEPT + COND_TYPE_TASK,
@@ -110,6 +116,8 @@ policy_name_map = {
'TASK_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_TASK,
'TASK_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_TASK,
'TASK_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_TASK,
+ 'TASK_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_TASK,
+ 'TASK_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_TASK,
# legacy values
'ACT_MANUAL': ACT_MANUAL + COND_TYPE_ALL,
'ACT_ACCEPT': ACT_ACCEPT + COND_TYPE_ALL,
@@ -122,6 +130,8 @@ policy_name_map = {
'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_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_ALL,
+ 'ACT_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL,
'ACT_SAVE_TO_CALENDAR': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
'ACT_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT,
}
@@ -597,10 +607,11 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei
log.info(_("Pass cancellation for manual processing"))
return MESSAGE_FORWARD
- # auto-update the local copy with STATUS=CANCELLED
- if policy & ACT_UPDATE:
+ # auto-update the local copy
+ if policy & ACT_UPDATE or policy & ACT_CANCEL_DELETE:
# find existing object in user's folders
(existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True)
+ remove_object = policy & ACT_CANCEL_DELETE
if existing:
# on this-and-future cancel requests, set the recurrence until date on the master event
@@ -610,13 +621,32 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei
rrule.set_until(existing.get_start() + datetime.timedelta(days=-1))
master.set_recurrence(rrule)
existing.set_recurrence_id(existing.get_recurrence_id(), True)
+ remove_object = False
+
+ # delete the local copy
+ if remove_object:
+ # remove exception and register an exdate to the main event
+ if master:
+ log.debug(_("Remove cancelled %s instance %s from %r") % (existing.type, itip_event['recurrence-id'], existing.uid), level=8)
+ master.add_exception_date(existing.get_start())
+ master.del_exception(existing)
+ success = update_object(master, receiving_user)
+
+ # delete main event
+ else:
+ success = delete_object(existing)
- existing.set_status('CANCELLED')
- existing.set_transparency(True)
- if update_object(existing, receiving_user, master):
+ # update the local copy with STATUS=CANCELLED
+ else:
+ log.debug(_("Update cancelled %s %r with STATUS=CANCELLED") % (existing.type, existing.uid), level=8)
+ existing.set_status('CANCELLED')
+ existing.set_transparency(True)
+ success = update_object(existing, receiving_user, master)
+
+ if success:
# send cancellation notification
- if policy & ACT_UPDATE_AND_NOTIFY:
- send_cancel_notification(existing, receiving_user)
+ if policy & COND_NOTIFY:
+ send_cancel_notification(existing, receiving_user, remove_object)
return MESSAGE_PROCESSED
@@ -1177,7 +1207,7 @@ def send_update_notification(object, receiving_user, old=None, reply=True):
smtp.quit()
-def send_cancel_notification(object, receiving_user):
+def send_cancel_notification(object, receiving_user, deleted=False):
"""
Send a notification about event/task cancellation
"""
@@ -1199,24 +1229,26 @@ def send_cancel_notification(object, receiving_user):
# 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.
- """) % {
+ message_text = _("The assignment for '%(summary)s' has been cancelled by %(organizer)s.") % {
'summary': object.get_summary(),
'organizer': orgname if orgname else orgemail
}
+ if deleted:
+ message_text += " " + _("The copy in your tasklist as been removed accordingly.")
+ else:
+ message_text += " " + _("The copy in your tasklist as been marked as cancelled accordingly.")
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.
- """) % {
+ message_text = _("The event '%(summary)s' at %(start)s has been cancelled by %(organizer)s.") % {
'summary': object.get_summary(),
'start': xmlutils.property_to_string('start', object.get_start()),
'organizer': orgname if orgname else orgemail
}
+ if deleted:
+ message_text += " " + _("The copy in your calendar as been removed accordingly.")
+ else:
+ message_text += " " + _("The copy in your calendar as been marked as cancelled accordingly.")
- message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
+ message_text += "\n\n" + _("*** This is an automated message. Please do not reply. ***")
# compose mime message
msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8')