6 commits - pykolab/itip pykolab/xml tests/functional tests/unit wallace/module_resources.py

Thomas Brüderli bruederli at kolabsys.com
Tue Aug 5 16:55:14 CEST 2014


 pykolab/itip/__init__.py                                      |    4 
 pykolab/xml/attendee.py                                       |    2 
 pykolab/xml/event.py                                          |   23 +-
 tests/functional/test_wallace/test_005_resource_invitation.py |   82 +++++++
 tests/functional/test_wallace/test_007_invitationpolicy.py    |    2 
 tests/unit/test-011-itip.py                                   |   10 
 wallace/module_resources.py                                   |  110 +++++++---
 7 files changed, 188 insertions(+), 45 deletions(-)

New commits:
commit a8555e3e8789fd02b7d5749ea4fc51b84e57285f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 5 00:53:26 2014 -0400

    Improve resource confirmation workflow:
    - Use base64 encoding for original event UIDs
    - Compare sequence number on resource owner replies
    - Added confirmation test scenario with reservation update and outdated replies

diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index c21f12c..61b9402 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -239,11 +239,12 @@ class TestResourceInvitation(unittest.TestCase):
         smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload))
         smtp.quit()
 
-    def send_itip_invitation(self, resource_email, start=None, allday=False, template=None):
+    def send_itip_invitation(self, resource_email, start=None, allday=False, template=None, uid=None):
         if start is None:
             start = datetime.datetime.now()
 
-        uid = str(uuid.uuid4())
+        if uid is None:
+            uid = str(uuid.uuid4())
 
         if allday:
             default_template = itip_allday
@@ -393,6 +394,7 @@ class TestResourceInvitation(unittest.TestCase):
         self.assertEqual(event.get_summary(), "test")
 
 
+    # @depends test_002_invite_resource
     def test_003_invite_resource_conflict(self):
         uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 12,0,0))
 
@@ -670,6 +672,7 @@ class TestResourceInvitation(unittest.TestCase):
 
         event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_status(True), 'CONFIRMED')
         self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED')
 
 
@@ -705,6 +708,74 @@ class TestResourceInvitation(unittest.TestCase):
         response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.room3['mail'])
         self.assertIsInstance(response, email.message.Message)
 
-        # tentative reservation was removed from resource calendar
+        # tentative reservation was set to cancelled
         event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
         self.assertEqual(event, None)
+        #self.assertEqual(event.get_status(True), 'CANCELLED')
+        #self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'DECLINED')
+
+
+    def test_015_owner_confirmation_update(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,8,19, 9,0,0), uid="http://a-totally.stupid/?uid")
+
+        # requester (john) gets a TENTATIVE confirmation
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        # check first confirmation message sent to resource owner (jane)
+        notify1 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notify1, email.message.Message)
+
+        itip_event1 = events_from_message(notify1)[0]
+        self.assertEqual(itip_event1['start'].hour, 9)
+
+        self.purge_mailbox(self.jane['mailbox'])
+        self.purge_mailbox(self.john['mailbox'])
+
+        # send update with new date (and sequence)
+        self.send_itip_update(self.room3['mail'], uid, datetime.datetime(2014,8,19, 16,0,0))
+
+        event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'TENTATIVE')
+
+        # check second confirmation message sent to resource owner (jane)
+        notify2 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notify2, email.message.Message)
+
+        itip_event2 = events_from_message(notify2)[0]
+        self.assertEqual(itip_event2['start'].hour, 16)
+
+        # resource owner declines the first reservation request
+        itip_reply = itip_event1['xml'].to_message_itip(self.jane['mail'],
+            method="REPLY",
+            participant_status='DECLINED',
+            message_text="Request declined",
+            subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('DECLINED'))
+        )
+        smtp = smtplib.SMTP('localhost', 10026)
+        smtp.sendmail(self.jane['mail'], str(itip_event1['organizer']), str(itip_reply))
+        smtp.quit()
+
+        time.sleep(5)
+
+        # resource owner accpets the second reservation request
+        itip_reply = itip_event2['xml'].to_message_itip(self.jane['mail'],
+            method="REPLY",
+            participant_status='ACCEPTED',
+            message_text="Request accepred",
+            subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('ACCEPTED'))
+        )
+        smtp = smtplib.SMTP('localhost', 10026)
+        smtp.sendmail(self.jane['mail'], str(itip_event2['organizer']), str(itip_reply))
+        smtp.quit()
+
+        # requester (john) now gets the ACCEPTED response
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED')
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 1af22ff..9bc808f 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -598,7 +598,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "cancelled")
-        self.assertEqual(event.get_status(), 'CANCELLED')
+        self.assertEqual(event.get_status(True), 'CANCELLED')
         self.assertTrue(event.get_transparency())
 
 
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index f7c9441..47259da 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -25,7 +25,7 @@ import random
 import tempfile
 import time
 from urlparse import urlparse
-import urllib
+import base64
 import uuid
 import re
 
@@ -204,9 +204,10 @@ def execute(*args, **kw):
 
         for recipient in recipients:
             # extract reference UID from recipients like resource+UID at domain.org
-            if re.match('.+\+[A-Za-z0-9%/_-]+@', recipient):
+            if re.match('.+\+[A-Za-z0-9=/-]+@', recipient):
                 (prefix, host) = recipient.split('@')
-                (local, reference_uid) = prefix.split('+')
+                (local, uid) = prefix.split('+')
+                reference_uid = base64.b64decode(uid, '-/')
                 recipient = local + '@' + host
 
             if not len(resource_record_from_email_address(recipient)) == 0:
@@ -268,6 +269,13 @@ def execute(*args, **kw):
                         log.error("Could not find envelope sender attendee: %r" % (e))
                         continue
 
+                    # compare sequence number to avoid outdated replies
+                    if not itip_event['sequence'] == event.get_sequence():
+                        log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Ignoring.") % (
+                            itip_event['sequence'], event.get_sequence()
+                        ))
+                        continue
+
                     # forward owner response comment
                     comment = itip_event['xml'].get_comment()
                     if comment:
@@ -276,10 +284,12 @@ def execute(*args, **kw):
                     itip_event_ = dict(xml=event, uid=event.get_uid())
 
                     if owner_reply == kolabformat.PartAccepted:
+                        event.set_status(kolabformat.StatusConfirmed)
                         accept_reservation_request(itip_event_, receiving_resource, confirmed=True)
                     elif owner_reply == kolabformat.PartDeclined:
                         decline_reservation_request(itip_event_, receiving_resource)
-                        # TODO: set partstat=DECLINED and status=CANCELLED instead of deleting?
+                        # TODO: set status=CANCELLED instead of deleting?
+                        # event.set_status(kolabformat.StatusCancelled)
                         delete_resource_event(reference_uid, receiving_resource)
                     else:
                         log.info("Invalid response (%r) recieved from resource owner for event %r" % (
@@ -288,6 +298,9 @@ def execute(*args, **kw):
                 else:
                     log.info(_("Event referenced by this REPLY (%r) not found in resource calendar") % (reference_uid))
 
+            else:
+                log.info(_("No event reference found in this REPLY. Ignoring."))
+
             # exit for-loop
             break
 
@@ -615,16 +628,13 @@ def accept_reservation_request(itip_event, resource, delegator=None, confirmed=F
 
     partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
 
+    itip_event['xml'].set_transparency(False);
     itip_event['xml'].set_attendee_participant_status(
         itip_event['xml'].get_attendee_by_email(resource['mail']),
         partstat
     )
 
-    # remove old copy of the reservation
-    if confirmed:
-        delete_resource_event(itip_event['uid'], resource)
-
-    saved = save_resource_event(itip_event, resource)
+    saved = save_resource_event(itip_event, resource, replace=confirmed)
 
     log.debug(
         _("Adding event to %r: %r") % (resource['kolabtargetfolder'], saved),
@@ -658,14 +668,20 @@ def decline_reservation_request(itip_event, resource):
         send_owner_notification(resource, owner, itip_event, True)
 
 
-def save_resource_event(itip_event, resource):
+def save_resource_event(itip_event, resource, replace=False):
     """
         Append the given event object to the resource's calendar
     """
     try:
         # Administrator login name comes from configuration.
         targetfolder = imap.folder_quote(resource['kolabtargetfolder'])
-        imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
+
+        # remove old copy of the reservation (also sets ACLs)
+        if replace:
+            delete_resource_event(itip_event['uid'], resource)
+        else:
+            imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
+
         result = imap.imap.m.append(
             targetfolder,
             None,
@@ -788,7 +804,7 @@ def resource_records_from_itip_events(itip_events, recipient_email=None):
     log.debug(_("Raw set of resources: %r") % (resources_raw), level=9)
 
     # consider organizer (in REPLY messages), too
-    organizers_raw = [re.sub('\+[A-Za-z0-9%/_-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
+    organizers_raw = [re.sub('\+[A-Za-z0-9=/-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
 
     log.debug(_("Raw set of organizers: %r") % (organizers_raw), level=8)
 
@@ -1159,7 +1175,7 @@ def send_owner_confirmation(resource, owner, itip_event):
     # generate new UID and set the resource as organizer
     (mail, domain) = resource['mail'].split('@')
     event.set_uid(str(uuid.uuid4()))
-    event.set_organizer(mail + '+' + urllib.quote(uid) + '@' + domain, resource['cn'])
+    event.set_organizer(mail + '+' + base64.b64encode(uid, '-/') + '@' + domain, resource['cn'])
     itip_event['uid'] = event.get_uid()
 
     # add resource owner as (the sole) attendee


commit 78b688519c8b73d66ab1f4fba74ab39acdf9552f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 5 00:22:22 2014 -0400

    Respect transparency property for conflict detection; fix tests for translated iTip message contents

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index c30421a..ddcb392 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -144,7 +144,9 @@ def check_event_conflict(kolab_event, itip_event):
     if kolab_event.uid == itip_event['uid']:
         return conflict
 
-    # TODO: don't consider conflict if event has TRANSP:TRANSPARENT
+    # don't consider conflict if event has TRANSP:TRANSPARENT
+    if kolab_event.get_transparency():
+        return conflict
 
     _es = to_dt(kolab_event.get_start())
     _ee = to_dt(kolab_event.get_ical_dtend())  # use iCal style end date: next day for all-day events
diff --git a/tests/unit/test-011-itip.py b/tests/unit/test-011-itip.py
index a120fd2..a08d05f 100644
--- a/tests/unit/test-011-itip.py
+++ b/tests/unit/test-011-itip.py
@@ -5,6 +5,8 @@ import kolabformat
 
 from pykolab import itip
 from pykolab.xml import Event
+from pykolab.xml import participant_status_label
+from pykolab.translate import _
 
 from icalendar import Calendar
 from email import message
@@ -363,6 +365,9 @@ class TestITip(unittest.TestCase):
 
         self.assertTrue(itip.check_event_conflict(allday, itip_event), "Conflicting allday event")
 
+        allday.set_transparency(True)
+        self.assertFalse(itip.check_event_conflict(allday, itip_event), "No conflict if event is set to transparent")
+
         event2 = Event()
         event2.set_start(datetime.datetime(2012,7,13, 10,0,0, tzinfo=pytz.timezone("US/Central")))
         event2.set_end(datetime.datetime(2012,7,13, 11,0,0, tzinfo=pytz.timezone("US/Central")))
@@ -398,9 +403,10 @@ class TestITip(unittest.TestCase):
         self.assertEqual(self.smtplog[0][0], 'resource-collection-car at example.org', "From attendee")
         self.assertEqual(self.smtplog[0][1], 'john.doe at example.org', "To organizer")
 
+        _accepted = participant_status_label('ACCEPTED')
         message = message_from_string(self.smtplog[0][2])
-        self.assertEqual(message.get('Subject'), 'Invitation for test was ACCEPTED')
+        self.assertEqual(message.get('Subject'), _("Invitation for %(summary)s was %(status)s") % { 'summary':'test', 'status':_accepted })
 
         text = str(message.get_payload(0));
         self.assertIn('SUMMARY=test', text)
-        self.assertIn('STATUS=ACCEPTED', text)
+        self.assertIn('STATUS=' + _accepted, text)


commit e636a436365e2025e50ed7eb16105f134412bd62
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 5 00:21:06 2014 -0400

    Remove duplicate set_status() method; get translated event status value on request

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index a9db73e..72cbfeb 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -550,11 +550,12 @@ class Event(object):
     def get_start(self):
         return xmlutils.from_cdatetime(self.event.start(), True)
 
-    def get_status(self):
+    def get_status(self, translated=False):
         status = self.event.status()
-        for key in self.status_map.keys():
-            if self.status_map[key] == status:
-                return key
+        if translated:
+            return self._translate_value(status, self.status_map) if status else None
+
+        return status
 
     def get_summary(self):
         return self.event.summary()
@@ -592,14 +593,6 @@ class Event(object):
 
         self.event.setAttendees(self._attendees)
 
-    def set_status(self, status):
-        if status in self.status_map.keys():
-            self.event.setStatus(self.status_map[status])
-        elif status in self.status_map.values():
-            self.event.setStatus(status)
-        else:
-            raise ValueError, _("Invalid status %r") % (status)
-
     def set_classification(self, classification):
         if classification in self.classification_map.keys():
             self.event.setClassification(self.classification_map[classification])


commit 91887a0e6cecf53273fb6de85608bea0b0b5581a
Merge: a71a8d2 84fd219
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 21:59:32 2014 -0400

    Merge branch 'master' of ssh://git.kolab.org/git/pykolab



commit a71a8d2729c7fb5a9fc31170cb762cb100aed7ba
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 21:59:24 2014 -0400

    Inherit kolabinvitationpolicy attributes from resource collection; forward comments from owner confirmation replies

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 7921280..4a30622 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -172,7 +172,7 @@ class Attendee(kolabformat.Attendee):
     def get_displayname(self):
         name = self.contactreference.get_name()
         email = self.contactreference.get_email()
-        return "%s <%s>" % (name, email) if name is not None else email
+        return "%s <%s>" % (name, email) if not name == "" else email
 
     def get_participant_status(self, translated=False):
         partstat = self.partStat()
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 096fba8..c21f12c 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -34,6 +34,7 @@ SUMMARY:test
 DESCRIPTION:test
 ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
 ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:%s
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Somebody Else:mailto:somebody at else.com
 TRANSP:OPAQUE
 END:VEVENT
 END:VCALENDAR
@@ -221,9 +222,11 @@ class TestResourceInvitation(unittest.TestCase):
 
         self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
         self.room2 = funcs.resource_add("confroom", "Conference Room B-222")
-        self.room3 = funcs.resource_add("confroom", "CEOs Office 303", owner=self.jane['dn'], kolabinvitationpolicy='ACT_MANUAL')
         self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
 
+        self.room3 = funcs.resource_add("confroom", "CEOs Office 303")
+        self.viprooms = funcs.resource_add("collection", "VIP Rooms", [ self.room3['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_MANUAL')
+
         time.sleep(1)
         from tests.functional.synchronize import synchronize_once
         synchronize_once()
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 2f93c6f..f7c9441 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -268,6 +268,11 @@ def execute(*args, **kw):
                         log.error("Could not find envelope sender attendee: %r" % (e))
                         continue
 
+                    # forward owner response comment
+                    comment = itip_event['xml'].get_comment()
+                    if comment:
+                        event.set_comment(str(comment))
+
                     itip_event_ = dict(xml=event, uid=event.get_uid())
 
                     if owner_reply == kolabformat.PartAccepted:
@@ -598,11 +603,15 @@ def accept_reservation_request(itip_event, resource, delegator=None, confirmed=F
     owner = get_resource_owner(resource)
     confirmation_required = False
 
-    if not confirmed and resource.has_key('kolabinvitationpolicy'):
-        for policy in resource['kolabinvitationpolicy']:
-            if policy & ACT_MANUAL and owner['mail']:
-                confirmation_required = True
-                break
+    if not confirmed:
+        invitationpolicy = get_resource_invitationpolicy(resource)
+        log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=9)
+
+        if invitationpolicy is not None:
+            for policy in invitationpolicy:
+                if policy & ACT_MANUAL and owner['mail']:
+                    confirmation_required = True
+                    break
 
     partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
 
@@ -963,6 +972,37 @@ def get_resource_owner(resource):
     return None
 
 
+def get_resource_invitationpolicy(resource):
+    """
+        Get this resource's kolabinvitationpolicy configuration
+    """
+    global auth
+
+    if not resource.has_key('kolabinvitationpolicy') or resource['kolabinvitationpolicy'] is None:
+        if not auth:
+            auth = Auth()
+            auth.connect()
+
+        # get kolabinvitationpolicy attribute from collection
+        collections = auth.search_entry_by_attribute('uniquemember', resource['dn'])
+        if not isinstance(collections, list):
+            collections = [ (collections['dn'],collections) ]
+
+        log.debug("Check collections %r for kolabinvitationpolicy attributes" % (collections), level=9)
+
+        for dn,collection in collections:
+            # ldap.search_entry_by_attribute() doesn't return the attributes lower-cased
+            if collection.has_key('kolabInvitationPolicy'):
+                collection['kolabinvitationpolicy'] = collection['kolabInvitationPolicy']
+
+            if collection.has_key('kolabinvitationpolicy'):
+                parse_kolabinvitationpolicy(collection)
+                resource['kolabinvitationpolicy'] = collection['kolabinvitationpolicy']
+                break
+
+    return resource['kolabinvitationpolicy'] if resource.has_key('kolabinvitationpolicy') else None
+
+
 def send_response(from_address, itip_events, owner=None):
     """
         Send the given iCal events as a valid iTip response to the organizer.
@@ -1033,8 +1073,10 @@ def send_owner_notification(resource, owner, itip_event, success=True):
     notify = False
     status = itip_event['xml'].get_attendee_by_email(resource['mail']).get_participant_status(True)
 
-    if resource.has_key('kolabinvitationpolicy'):
-        for policy in resource['kolabinvitationpolicy']:
+    invitationpolicy = get_resource_invitationpolicy(resource)
+
+    if invitationpolicy is not None:
+        for policy in invitationpolicy:
             # TODO: distingish ACCEPTED / DECLINED status notifications?
             if policy & COND_NOTIFY and owner['mail']:
                 notify = True
@@ -1109,9 +1151,10 @@ def send_owner_confirmation(resource, owner, itip_event):
         receive the reply from the owner.
     """
 
-    event = itip_event['xml']
     uid = itip_event['uid']
+    event = itip_event['xml']
     organizer = event.get_organizer()
+    event_attendees = [a.get_displayname() for a in event.get_attendees() if not a.get_cutype() == kolabformat.CutypeResource]
 
     # generate new UID and set the resource as organizer
     (mail, domain) = resource['mail'].split('@')
@@ -1119,11 +1162,12 @@ def send_owner_confirmation(resource, owner, itip_event):
     event.set_organizer(mail + '+' + urllib.quote(uid) + '@' + domain, resource['cn'])
     itip_event['uid'] = event.get_uid()
 
-    # add resource owner as attendee
+    # add resource owner as (the sole) attendee
+    event._attendees = []
     event.add_attendee(owner['mail'], owner['cn'], rsvp=True, role=kolabformat.Required, participant_status=kolabformat.PartNeedsAction)
 
     # flag this iTip message as confirmation type
-    event.add_custom_property('X-Wallace-MessageType', 'CONFIRMATION')
+    event.add_custom_property('X-Kolab-InvitationType', 'CONFIRMATION')
 
     log.debug(
         _("Clone invitation for owner confirmation: %r from %r") % (
@@ -1140,6 +1184,7 @@ def send_owner_confirmation(resource, owner, itip_event):
 
         Subject: %(summary)s.
         Date: %(date)s
+        Participants: %(attendees)s
 
         *** This is an automated message, please don't reply by email. ***
     """)% {
@@ -1147,7 +1192,8 @@ def send_owner_confirmation(resource, owner, itip_event):
         'orgname': organizer.name(),
         'orgemail': organizer.email(),
         'summary': event.get_summary(),
-        'date': event.get_date_text()
+        'date': event.get_date_text(),
+        'attendees': ",\n+ ".join(event_attendees)
     }
 
     pykolab.itip.send_request(owner['mail'], itip_event, message_text,


commit acbede72e43a4f38a1cb31e920a06dcdd13be2d2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 20:26:48 2014 -0400

    Treat the comment property as list for iCal export

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 076eb39..a9db73e 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -521,6 +521,12 @@ class Event(object):
     def get_ical_sequence(self):
         return str(self.event.sequence()) if self.event.sequence() else None
 
+    def get_ical_comment(self):
+        comment = self.get_comment()
+        if comment is not None:
+            return [ comment ]
+        return None
+
     def get_location(self):
         return self.event.location()
 




More information about the commits mailing list