summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2014-02-19 23:20:58 -0500
committerThomas Bruederli <bruederli@kolabsys.com>2014-02-19 23:20:58 -0500
commit439cea0c64e461546b57e572476fc1b20ac53d54 (patch)
tree4dffeb51d28eede9ee9d12362cfb41de24335bc2
parent054161ee8b09c58719e02f2189e8ac5112860751 (diff)
downloadpykolab-439cea0c64e461546b57e572476fc1b20ac53d54.tar.gz
Fix and test invitation of resource collections and delegation to a (free) collection member
-rw-r--r--tests/functional/test_wallace/test_005_resource_invitation.py91
-rw-r--r--wallace/module_resources.py60
2 files changed, 118 insertions, 33 deletions
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 085ad3b..5bf9767 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -2,6 +2,8 @@ import time
import pykolab
import smtplib
import email
+import datetime
+import uuid
from pykolab import wap_client
from pykolab.auth import Auth
@@ -43,10 +45,10 @@ PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
-UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+UID:%s
DTSTAMP:20120713T1254140
-DTSTART;TZID=Europe/London:20140713T100000
-DTEND;TZID=Europe/London:20140713T160000
+DTSTART;TZID=Europe/London:%s
+DTEND;TZID=Europe/London:%s
SUMMARY:test
DESCRIPTION:test
ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
@@ -76,7 +78,8 @@ class TestResourceInvitation(unittest.TestCase):
self.john = {
'displayname': 'John Doe',
'mail': 'john.doe@example.org',
- 'sender': 'John Doe <john.doe@example.org>'
+ 'sender': 'John Doe <john.doe@example.org>',
+ 'mailbox': 'user/john.doe@example.org'
}
from tests.functional.user_add import user_add
@@ -99,14 +102,28 @@ class TestResourceInvitation(unittest.TestCase):
smtp = smtplib.SMTP('localhost', 10026)
smtp.sendmail(from_addr, to_addr, msg_source)
- def send_itip_invitation(self, resource_email):
- self.send_message(itip_invitation % (resource_email, resource_email), resource_email)
+ def send_itip_invitation(self, resource_email, start=None):
+ if start is None:
+ start = datetime.datetime.now()
- def check_message_received(self, subject):
+ uid = str(uuid.uuid4())
+ end = start + datetime.timedelta(hours=4)
+ self.send_message(itip_invitation % (
+ resource_email,
+ uid,
+ start.strftime('%Y%m%dT%H%M%S'),
+ end.strftime('%Y%m%dT%H%M%S'),
+ resource_email
+ ),
+ resource_email)
+
+ return uid
+
+ def check_message_received(self, subject, from_addr=None):
imap = IMAP()
imap.connect()
- imap.set_acl("user/john.doe@example.org", "cyrus-admin", "lrs")
- imap.imap.m.select("user/john.doe@example.org")
+ imap.set_acl(self.john['mailbox'], "cyrus-admin", "lrs")
+ imap.imap.m.select(self.john['mailbox'])
found = None
max_tries = 20
@@ -114,7 +131,7 @@ class TestResourceInvitation(unittest.TestCase):
while not found and max_tries > 0:
max_tries -= 1
- typ, data = imap.imap.m.search(None, 'ALL')
+ typ, data = imap.imap.m.search(None, '(UNDELETED HEADER FROM "%s")' % (from_addr) if from_addr else 'UNDELETED')
for num in data[0].split():
typ, msg = imap.imap.m.fetch(num, '(RFC822)')
message = message_from_string(msg[0][1])
@@ -141,9 +158,12 @@ class TestResourceInvitation(unittest.TestCase):
typ, data = imap.imap.m.fetch(num, '(RFC822)')
event_message = message_from_string(data[0][1])
+ # return matching UID or first event found
+ if uid and event_message['subject'] != uid:
+ continue
+
for part in event_message.walk():
- # return matching UID or first event found
- if (not uid or event_message['subject'] == uid) and part.get_content_type() == "application/calendar+xml":
+ if part.get_content_type() == "application/calendar+xml":
payload = part.get_payload(decode=True)
found = pykolab.xml.event_from_string(payload)
break
@@ -155,6 +175,19 @@ class TestResourceInvitation(unittest.TestCase):
return found
+ def purge_mailbox(self, mailbox):
+ imap = IMAP()
+ imap.connect()
+ imap.set_acl(mailbox, "cyrus-admin", "lrwcdest")
+ imap.imap.m.select(u'"'+mailbox+'"')
+
+ typ, data = imap.imap.m.search(None, 'ALL')
+ for num in data[0].split():
+ imap.imap.m.store(num, '+FLAGS', '\\Deleted')
+
+ imap.imap.m.expunge()
+ imap.disconnect()
+
def test_001_resource_from_email_address(self):
resource = module_resources.resource_record_from_email_address(self.audi['mail'])
@@ -167,11 +200,39 @@ class TestResourceInvitation(unittest.TestCase):
def test_002_invite_resource(self):
- self.send_itip_invitation(self.audi['mail'])
+ uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 10,0,0))
- response = self.check_message_received("Meeting Request ACCEPTED")
+ response = self.check_message_received("Meeting Request ACCEPTED", self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
- event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], '626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271')
+ event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
self.assertIsInstance(event, pykolab.xml.Event)
self.assertEqual(event.get_summary(), "test")
+
+
+ def test_003_invite_resource_conflict(self):
+ uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 12,0,0))
+
+ response = self.check_message_received("Meeting Request DECLINED", self.audi['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ self.assertEqual(self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid), None)
+
+
+ def test_004_invite_resource_collection(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,7,13, 12,0,0))
+
+ # one of the collection members accepted the reservation
+ accept = self.check_message_received("Meeting Request ACCEPTED")
+ self.assertIsInstance(accept, email.message.Message)
+ self.assertIn(accept['from'], [ self.passat['mail'], self.boxter['mail'] ])
+
+ # check booking in the delegatee's resource calendar
+ delegatee = self.passat if accept['from'] == self.passat['mail'] else self.boxter
+ self.assertIsInstance(self.check_resource_calendar_event(delegatee['kolabtargetfolder'], uid), pykolab.xml.Event)
+
+ # resource collection respons with a DELEGATED message
+ response = self.check_message_received("Meeting Request DELEGATED", self.cars['mail'])
+ self.assertIsInstance(response, email.message.Message)
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index e76df24..eaa547b 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -187,19 +187,19 @@ def execute(*args, **kw):
# A simple list of merely resource entry IDs that hold any relevance to the
# iTip events
- resource_records = resource_records_from_itip_events(itip_events, resource_recipient)
+ resource_dns = resource_records_from_itip_events(itip_events, resource_recipient)
# Get the resource details, which includes details on the IMAP folder
resources = {}
- for resource_record in list(set(resource_records)):
+ for resource_dn in list(set(resource_dns)):
# Get the attributes for the record
# See if it is a resource collection
# If it is, expand to individual resources
# If it is not, ...
- resource_attrs = auth.get_entry_attributes(None, resource_record, ['*'])
+ resource_attrs = auth.get_entry_attributes(None, resource_dn, ['*'])
if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
if resource_attrs.has_key('uniquemember'):
- resources[resource_record] = resource_attrs
+ resources[resource_dn] = resource_attrs
for uniquemember in resource_attrs['uniquemember']:
resource_attrs = auth.get_entry_attributes(
None,
@@ -209,11 +209,12 @@ def execute(*args, **kw):
if 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
resources[uniquemember] = resource_attrs
- resources[uniquemember]['memberof'] = resource_record
+ resources[uniquemember]['memberof'] = resource_dn
+ resource_dns.append(uniquemember)
else:
- resources[resource_record] = resource_attrs
+ resources[resource_dn] = resource_attrs
- log.debug(_("Resources: %r") % (resources), level=8)
+ log.debug(_("Resources: %r, %r") % (resource_dns, resources), level=8)
# For each resource, determine if any of the events in question is in
# conflict.
@@ -239,21 +240,39 @@ def execute(*args, **kw):
done = False
- for resource in resources.keys():
+ for resource in resource_dns:
log.debug(_("Polling for resource %r") % (resource), level=9)
- if done:
- break
-
if not resources.has_key(resource):
log.debug(_("Resource %r has been popped from the list") % (resource), level=9)
continue
if not resources[resource].has_key('conflicting_events'):
log.debug(_("Resource is a collection"), level=9)
+
+ # check if there are non-conflicting collection members
+ conflicting_members = [x for x in resources[resource]['uniquemember'] if resources[x]['conflict']]
+
+ # found at least one non-conflicting member, remove the conflicting ones and continue
+ if len(conflicting_members) < len(resources[resource]['uniquemember']):
+ for member in conflicting_members:
+ resources[resource]['uniquemember'] = [x for x in resources[resource]['uniquemember'] if x != member]
+ del resources[member]
+
+ log.debug(_("Removed conflicting resources from %r: (%r) => %r") % (
+ resource, conflicting_members, resources[resource]['uniquemember']
+ ), level=9)
+
+ else:
+ # TODO: shuffle existing bookings of collection members in order
+ # to make one availale for the requested time
+ pass
+
continue
if len(resources[resource]['conflicting_events']) > 0:
+ log.debug(_("Conflicting events: %r for resource %r") % (resources[resource]['conflicting_events'], resource), level=9)
+
# This is the event being conflicted with!
for itip_event in itip_events:
# Now we have the event that was conflicting
@@ -268,9 +287,6 @@ def execute(*args, **kw):
if resources[resource].has_key('memberof'):
original_resource = resources[resources[resource]['memberof']]
- # TODO: shuffle existing bookings of collection members in order
- # to make one availale for the requested time
-
if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
decline_reservation_request(itip_event, original_resource)
done = True
@@ -279,6 +295,7 @@ def execute(*args, **kw):
# No conflicts, go accept
for itip_event in itip_events:
if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
+ log.debug(_("Accept invitation for individual resource %r / %r") % (resource, resources[resource]['mail']), level=9)
accept_reservation_request(itip_event, resources[resource], imap)
done = True
@@ -292,6 +309,8 @@ def execute(*args, **kw):
# Randomly selects a target resource from the resource collection.
_target_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]]
+ log.debug(_("Delegate invitation for resource collection %r to %r") % (original_resource['mail'], _target_resource['mail']), level=9)
+
if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
#
# Delegate:
@@ -301,9 +320,14 @@ def execute(*args, **kw):
#
itip_event['xml'].delegate(original_resource['mail'], _target_resource['mail'])
- accept_reservation_request(itip_event, _target_resource, imap)
+ accept_reservation_request(itip_event, _target_resource, imap, delegator=original_resource)
done = True
+ if done:
+ break
+
+ # for resource in resource_dns:
+
auth.disconnect()
del auth
@@ -391,7 +415,7 @@ def read_resource_calendar(resource_rec, itip_events, imap):
return resource_rec['conflict']
-def accept_reservation_request(itip_event, resource, imap):
+def accept_reservation_request(itip_event, resource, imap, delegator=None):
"""
Accepts the given iTip event by booking it into the resource's
calendar. Then set the attendee status of the given resource to
@@ -418,7 +442,7 @@ def accept_reservation_request(itip_event, resource, imap):
itip_event['xml'].to_message().as_string()
)
- send_response(resource['mail'], itip_event)
+ send_response(delegator['mail'] if delegator else resource['mail'], itip_event)
def decline_reservation_request(itip_event, resource):
@@ -432,7 +456,7 @@ def decline_reservation_request(itip_event, resource):
"DECLINED"
)
- send_response(resource, itip_event)
+ send_response(resource['mail'], itip_event)
def itip_events_from_message(message):