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