2 commits - pykolab/utils.py pykolab/xml tests/functional wallace/module_invitationpolicy.py

Thomas Brüderli bruederli at kolabsys.com
Mon Mar 16 13:52:15 CET 2015


 pykolab/utils.py                                           |    6 
 pykolab/xml/event.py                                       |   17 ++
 tests/functional/test_wallace/test_007_invitationpolicy.py |   51 +++++++
 wallace/module_invitationpolicy.py                         |   84 ++++++++-----
 4 files changed, 125 insertions(+), 33 deletions(-)

New commits:
commit 9e267beaea760608393960850f59fca7aed47522
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 11 11:44:44 2015 -0400

    New *_CANCEL_DELETE policy to remove cancelled objects rather than flagging as cancelled (#4306)

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 at example.org',
             'kolabtasksfolder': 'user/jane.manager/Tasks at example.org',
             'kolabconfidentialcalendar': 'user/jane.manager/Calendar/Confidential at 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 at example.org',
             'kolabcalendarfolder': 'user/lucy.meyer/Calendar at example.org',
             'kolabtasksfolder': 'user/lucy.meyer/Tasks at 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')


commit e0e64a93a67d03bef200a10417d4334198247aa4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 11 10:36:32 2015 -0400

    The email.MIMEText class will take care of line wrapping; no need to do it twice

diff --git a/pykolab/utils.py b/pykolab/utils.py
index f91ed8d..a9537dd 100644
--- a/pykolab/utils.py
+++ b/pykolab/utils.py
@@ -303,11 +303,7 @@ def multiline_message(message):
     return "\n%s\n" % ("\n".join(lines))
 
 def stripped_message(message):
-    lines = []
-    for line in message.strip().split("\n"):
-        lines.append(multiline_message(line).strip())
-
-    return "\n%s\n" % ("\n".join(lines))
+    return "\n" + message.strip() + "\n"
 
 def str2unicode(s, encoding='utf-8'):
     if isinstance(s, unicode):




More information about the commits mailing list