diff options
-rw-r--r-- | tests/functional/test_wallace/test_005_resource_invitation.py | 91 | ||||
-rw-r--r-- | wallace/module_resources.py | 60 |
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): |