tests/functional wallace/module_invitationpolicy.py

Thomas Brüderli bruederli at kolabsys.com
Thu Aug 21 19:46:29 CEST 2014


 tests/functional/test_wallace/test_007_invitationpolicy.py |   39 ++++
 wallace/module_invitationpolicy.py                         |  110 +++++++------
 2 files changed, 99 insertions(+), 50 deletions(-)

New commits:
commit 44bde53ddb4eadd4fc3fd653fcefe48c39285d5a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 21 13:46:18 2014 -0400

    Apply ACT_UPDATE policy on iTip REQUESTs with no re-scheduling (i.e. unchanged sequence number) (#3447)

diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index dffc9df..8feeff0 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -387,13 +387,15 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return uid
 
-    def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None):
+    def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None, folder=None):
         if start is None:
             start = datetime.datetime.now(pytz.timezone("Europe/Berlin"))
         if user is None:
             user = self.john
         if attendees is None:
             attendees = [self.jane]
+        if folder is None:
+            folder = user['kolabcalendarfolder']
 
         end = start + datetime.timedelta(hours=4)
 
@@ -419,7 +421,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         imap = IMAP()
         imap.connect()
 
-        mailbox = imap.folder_quote(user['kolabcalendarfolder'])
+        mailbox = imap.folder_quote(folder)
         imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
         imap.imap.m.select(mailbox)
 
@@ -904,7 +906,34 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
 
 
-    def test_011_task_assignment_accept(self):
+    def test_011_manual_schedule_auto_update(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        # create an event in john's calendar as it was manually accepted
+        start = datetime.datetime(2014,9,2, 11,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_calendar_event(start, user=self.jane, sequence=1, folder=self.john['kolabcalendarfolder'])
+
+        # send update with the same sequence: no re-scheduling
+        templ = itip_invitation.replace("RSVP=TRUE", "RSVP=FALSE").replace("Doe, John", self.jane['displayname']).replace("john.doe at example.org", self.jane['mail'])
+        self.send_itip_update(self.john['mail'], uid, start, summary="test updated", sequence=1, partstat='ACCEPTED', template=templ)
+
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test updated")
+        self.assertEqual(event.get_attendee(self.john['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+        # this should also trigger an update notification
+        notification = self.check_message_received(_('"%s" has been updated') % ('test updated'), self.jane['mail'], mailbox=self.john['mailbox'])
+        self.assertIsInstance(notification, email.message.Message)
+
+        # send outdated update: should not be saved
+        self.send_itip_update(self.john['mail'], uid, start, summary="old test", sequence=0, partstat='NEEDS-ACTION', template=templ)
+        notification = self.check_message_received(_('"%s" has been updated') % ('old test'), self.jane['mail'], mailbox=self.john['mailbox'])
+        self.assertEqual(notification, None)
+
+
+    def test_020_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)
 
@@ -928,7 +957,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(todo.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
 
 
-    def test_012_task_assignment_reply(self):
+    def test_021_task_assignment_reply(self):
         self.purge_mailbox(self.john['mailbox'])
 
         due = datetime.datetime(2014,9,12, 14,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
@@ -958,7 +987,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertIn(participant_status_label(partstat), notification_text)
 
 
-    def test_013_task_cancellation(self):
+    def test_022_task_cancellation(self):
         uid = self.send_itip_invitation(self.jane['mail'], summary='more work', template=itip_todo)
 
         time.sleep(10)
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index e753c38..9ba1490 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -403,6 +403,26 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             # TODO: delegate (but to whom?)
             return None
 
+    # auto-update changes if enabled for this user
+    elif policy & ACT_UPDATE and existing:
+        # compare sequence number to avoid outdated updates
+        if not itip_event['sequence'] == existing.get_sequence():
+            log.info(_("The iTip request sequence (%r) doesn't match the referred object version (%r). Ignoring.") % (
+                itip_event['sequence'], existing.get_sequence()
+            ))
+            return None
+
+        log.debug(_("Auto-updating %s %r on iTip REQUEST (no re-scheduling)") % (existing.type, existing.uid), level=8)
+        save_object = True
+
+        # retain task status and percent-complete properties from my old copy
+        if is_task:
+            itip_event['xml'].set_status(existing.get_status())
+            itip_event['xml'].set_percentcomplete(existing.get_percentcomplete())
+
+        if policy & COND_NOTIFY:
+            send_update_notification(itip_event['xml'], receiving_user, False)
+
     # if RSVP, send an iTip REPLY
     if rsvp or scheduling_required:
         # set attendee's CN from LDAP record if yet missing
@@ -424,10 +444,6 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             # policy doesn't match, pass on to next one
             return None
 
-    else:
-        log.debug(_("No RSVP for recipient %r requested") % (receiving_user['mail']), level=8)
-        # TODO: only update if policy & ACT_UPDATE ?
-
     if save_object:
         targetfolder = None
 
@@ -517,7 +533,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
             # update the organizer's copy of the object
             if update_object(existing, receiving_user):
                 if policy & COND_NOTIFY:
-                    send_reply_notification(existing, receiving_user)
+                    send_update_notification(existing, receiving_user, True)
 
                 # update all other attendee's copies
                 if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
@@ -931,7 +947,7 @@ def delete_object(existing):
     imap.imap.m.expunge()
 
 
-def send_reply_notification(object, receiving_user):
+def send_update_notification(object, receiving_user, reply=True):
     """
         Send a (consolidated) notification about the current participant status to organizer
     """
@@ -941,52 +957,56 @@ def send_reply_notification(object, receiving_user):
     from email.MIMEText import MIMEText
     from email.Utils import formatdate
 
-    log.debug(_("Compose participation status summary 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()
 
-    auto_replies_expected = 0
-    auto_replies_received = 0
-    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())
-        else:
-            partstats['PENDING'].append(attendee.get_displayname())
+    if reply:
+        log.debug(_("Compose participation status summary for %s %r to user %r") % (
+            object.type, object.uid, receiving_user['mail']
+        ), level=8)
 
-        # look-up kolabinvitationpolicy for this attendee
-        if attendee.get_cutype() == kolabformat.CutypeResource:
-            resource_dns = auth.find_resource(attendee.get_email())
-            if isinstance(resource_dns, list):
-                attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
+        auto_replies_expected = 0
+        auto_replies_received = 0
+        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())
             else:
-                attendee_dn = resource_dns
-        else:
-            attendee_dn = user_dn_from_email_address(attendee.get_email())
-
-        if attendee_dn:
-            attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
-            if is_auto_reply(attendee_rec, orgemail, object.type):
-                auto_replies_expected += 1
-                if not parstat == 'NEEDS-ACTION':
-                    auto_replies_received += 1
-
-    # skip notification until we got replies from all automatically responding attendees
-    if auto_replies_received < auto_replies_expected:
-        log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % (
-            auto_replies_received, auto_replies_expected
-        ), level=8)
-        return
+                partstats['PENDING'].append(attendee.get_displayname())
 
-    roundup = ''
-    for status,attendees in partstats.iteritems():
-        if len(attendees) > 0:
-            roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
+            # look-up kolabinvitationpolicy for this attendee
+            if attendee.get_cutype() == kolabformat.CutypeResource:
+                resource_dns = auth.find_resource(attendee.get_email())
+                if isinstance(resource_dns, list):
+                    attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
+                else:
+                    attendee_dn = resource_dns
+            else:
+                attendee_dn = user_dn_from_email_address(attendee.get_email())
+
+            if attendee_dn:
+                attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
+                if is_auto_reply(attendee_rec, orgemail, object.type):
+                    auto_replies_expected += 1
+                    if not parstat == 'NEEDS-ACTION':
+                        auto_replies_received += 1
+
+        # skip notification until we got replies from all automatically responding attendees
+        if auto_replies_received < auto_replies_expected:
+            log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % (
+                auto_replies_received, auto_replies_expected
+            ), level=8)
+            return
+
+        roundup = ''
+        for status,attendees in partstats.iteritems():
+            if len(attendees) > 0:
+                roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
+    else:
+        # TODO: compose a diff of changes to previous version
+        roundup = "\n" + _("Minor changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail)
 
     # compose different notification texts for events/tasks
     if object.type == 'task':




More information about the commits mailing list