diff options
author | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-09 03:35:53 -0400 |
---|---|---|
committer | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-09 03:36:21 -0400 |
commit | 5038b40a73f111293abff38a82f6bc1764d164ba (patch) | |
tree | bfa09ff741fa3f04ccb48bc54e8c6bf123bf4a85 | |
parent | ebcc6748bf105fda1183bce3242260b867271809 (diff) | |
download | pykolab-5038b40a73f111293abff38a82f6bc1764d164ba.tar.gz |
Send owner notifications for resource bookings (#3167)
-rw-r--r-- | pykolab/xml/event.py | 10 | ||||
-rw-r--r-- | tests/functional/resource_func.py | 4 | ||||
-rw-r--r-- | tests/functional/test_wallace/test_005_resource_add.py | 17 | ||||
-rw-r--r-- | tests/functional/test_wallace/test_005_resource_invitation.py | 37 | ||||
-rw-r--r-- | wallace/module_resources.py | 122 |
5 files changed, 166 insertions, 24 deletions
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py index 65eb818..de9e4d9 100644 --- a/pykolab/xml/event.py +++ b/pykolab/xml/event.py @@ -299,6 +299,16 @@ class Event(object): dt = self.get_start() + duration return dt + def get_date_text(self, date_format='%Y-%m-%d', time_format='%H:%M %Z'): + start = self.get_start() + end = self.get_end() + if start.date() == end.date(): + end_format = time_format + else: + end_format = date_format + " " + time_format + + return "%s - %s" % (start.strftime(date_format + " " + time_format), end.strftime(end_format)) + def get_exception_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates()) diff --git a/tests/functional/resource_func.py b/tests/functional/resource_func.py index 43aca96..ac80360 100644 --- a/tests/functional/resource_func.py +++ b/tests/functional/resource_func.py @@ -4,7 +4,7 @@ from pykolab import wap_client conf = pykolab.getConf() -def resource_add(type, cn, members=None, owner=None): +def resource_add(type, cn, members=None, owner=None, **kw): if type == None or type == '': raise Exception @@ -18,6 +18,8 @@ def resource_add(type, cn, members=None, owner=None): 'owner': owner } + resource_details.update(kw) + result = wap_client.authenticate(conf.get('ldap', 'bind_dn'), conf.get('ldap', 'bind_pw'), conf.get('kolab', 'primary_domain')) type_id = 0 diff --git a/tests/functional/test_wallace/test_005_resource_add.py b/tests/functional/test_wallace/test_005_resource_add.py index 2de60fb..fc7f3ed 100644 --- a/tests/functional/test_wallace/test_005_resource_add.py +++ b/tests/functional/test_wallace/test_005_resource_add.py @@ -29,8 +29,8 @@ class TestResourceAdd(unittest.TestCase): funcs.purge_resources() self.audi = funcs.resource_add("car", "Audi A4") self.passat = funcs.resource_add("car", "VW Passat") - self.boxter = funcs.resource_add("car", "Porsche Boxter S") - self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ]) + self.boxter = funcs.resource_add("car", "Porsche Boxter S", kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY') + self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ], kolabinvitationpolicy='ACT_ACCEPT') from tests.functional.synchronize import synchronize_once synchronize_once() @@ -56,3 +56,16 @@ class TestResourceAdd(unittest.TestCase): attrs = auth.get_entry_attributes(None, self.cars['dn'], ['*']) self.assertIn('groupofuniquenames', attrs['objectclass']) self.assertEqual(len(attrs['uniquemember']), 3) + self.assertEqual(attrs['kolabinvitationpolicy'], 'ACT_ACCEPT') + + def test_003_get_resource_records(self): + resource_dns = module_resources.resource_record_from_email_address(self.cars['mail']) + self.assertEqual(resource_dns[0], self.cars['dn']) + + resources = module_resources.get_resource_records(resource_dns) + self.assertEqual(len(resources), 4) + + # check for (inherited) kolabinvitationpolicy values (bitmasks) + self.assertEqual(resources[self.cars['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT]) + self.assertEqual(resources[self.audi['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT]) + self.assertEqual(resources[self.boxter['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT_AND_NOTIFY]) diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py index d9f2d41..79ceaf2 100644 --- a/tests/functional/test_wallace/test_005_resource_invitation.py +++ b/tests/functional/test_wallace/test_005_resource_invitation.py @@ -214,9 +214,9 @@ class TestResourceInvitation(unittest.TestCase): self.boxter = funcs.resource_add("car", "Porsche Boxter S") self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ]) - self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn']) + 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.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn']) + self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY') time.sleep(1) from tests.functional.synchronize import synchronize_once @@ -353,12 +353,10 @@ class TestResourceInvitation(unittest.TestCase): def find_resource_by_email(self, email): resource = None - if (email.find(self.audi['mail']) >= 0): - resource = self.audi - if (email.find(self.passat['mail']) >= 0): - resource = self.passat - if (email.find(self.boxter['mail']) >= 0): - resource = self.boxter + for r in [self.audi, self.passat, self.boxter, self.room1, self.room2]: + if (email.find(r['mail']) >= 0): + resource = r + break return resource @@ -593,20 +591,29 @@ class TestResourceInvitation(unittest.TestCase): self.assertIn(self.jane['displayname'], respose_text) - def TODO_test_012_owner_notification(self): + def test_012_owner_notification(self): self.purge_mailbox(self.john['mailbox']) self.purge_mailbox(self.jane['mailbox']) - self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,5,4, 13,0,0)) + self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,8,4, 13,0,0)) # check notification message sent to resource owner (jane) - notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail'], self.jane['mailbox']) + notify = self.check_message_received("Booking for %s has been ACCEPTED" % (self.room1['cn']), self.room1['mail'], self.jane['mailbox']) self.assertIsInstance(notify, email.message.Message) - self.assertEqual(notify['From'], self.room1['mail']) - self.assertEqual(notify['Cc'], self.jane['mail']) + + notification_text = str(notify.get_payload()) + self.assertIn(self.john['mail'], notification_text) + self.assertIn("ACCEPTED", notification_text) + + self.purge_mailbox(self.john['mailbox']) # check notification sent to collection owner (jane) - self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,5,4, 12,30,0)) + self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,8,4, 12,30,0)) + + # one of the collection members accepted the reservation + accepted = self.check_message_received("Reservation Request for test was ACCEPTED") + delegatee = self.find_resource_by_email(accepted['from']) - notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail'], self.jane['mailbox']) + notify = self.check_message_received("Booking for %s has been ACCEPTED" % (delegatee['cn']), delegatee['mail'], self.jane['mailbox']) self.assertIsInstance(notify, email.message.Message) + self.assertIn(self.john['mail'], notification_text) diff --git a/wallace/module_resources.py b/wallace/module_resources.py index 3864f7c..5e07552 100644 --- a/wallace/module_resources.py +++ b/wallace/module_resources.py @@ -46,6 +46,18 @@ from pykolab.itip import events_from_message from pykolab.itip import check_event_conflict from pykolab.translate import _ +# define some contstants used in the code below +COND_NOTIFY = 256 +ACT_MANUAL = 1 +ACT_ACCEPT = 2 +ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY + +policy_name_map = { + 'ACT_MANUAL': ACT_MANUAL, + 'ACT_ACCEPT': ACT_ACCEPT, + 'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY +} + log = pykolab.getLogger('pykolab.wallace') conf = pykolab.getConf() @@ -513,7 +525,11 @@ def accept_reservation_request(itip_event, resource, delegator=None): level=8 ) - send_response(delegator['mail'] if delegator else resource['mail'], itip_event, get_resource_owner(resource)) + owner = get_resource_owner(resource) + send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner) + + if owner: + send_owner_notification(resource, owner, itip_event, saved) def decline_reservation_request(itip_event, resource): @@ -527,8 +543,12 @@ def decline_reservation_request(itip_event, resource): "DECLINED" ) + owner = get_resource_owner(resource) send_response(resource['mail'], itip_event, get_resource_owner(resource)) + if owner: + send_owner_notification(resource, owner, itip_event, True) + def save_resource_event(itip_event, resource): """ @@ -749,21 +769,25 @@ def get_resource_records(resource_dns): # If it is not, ... resource_attrs = auth.get_entry_attributes(None, resource_dn, ['*']) resource_attrs['dn'] = resource_dn + parse_kolabinvitationpolicy(resource_attrs) + if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]: if resource_attrs.has_key('uniquemember'): resources[resource_dn] = resource_attrs for uniquemember in resource_attrs['uniquemember']: - resource_attrs = auth.get_entry_attributes( + member_attrs = auth.get_entry_attributes( None, uniquemember, ['*'] ) - if 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]: - resource_attrs['dn'] = uniquemember - resources[uniquemember] = resource_attrs + if 'kolabsharedfolder' in [x.lower() for x in member_attrs['objectclass']]: + member_attrs['dn'] = uniquemember + parse_kolabinvitationpolicy(member_attrs, resource_attrs) + + resources[uniquemember] = member_attrs resources[uniquemember]['memberof'] = resource_dn - if not resource_attrs.has_key('owner') and resources[resource_dn].has_key('owner'): + if not member_attrs.has_key('owner') and resources[resource_dn].has_key('owner'): resources[uniquemember]['owner'] = resources[resource_dn]['owner'] resource_dns.append(uniquemember) else: @@ -772,6 +796,16 @@ def get_resource_records(resource_dns): return resources +def parse_kolabinvitationpolicy(attrs, parent=None): + if attrs.has_key('kolabinvitationpolicy'): + if not isinstance(attrs['kolabinvitationpolicy'], list): + attrs['kolabinvitationpolicy'] = [attrs['kolabinvitationpolicy']] + attrs['kolabinvitationpolicy'] = [policy_name_map[p] for p in attrs['kolabinvitationpolicy'] if policy_name_map.has_key(p)] + + elif isinstance(parent, dict) and parent.has_key('kolabinvitationpolicy'): + attrs['kolabinvitationpolicy'] = parent['kolabinvitationpolicy'] + + def get_resource_collection(email_address): """ @@ -878,3 +912,79 @@ def reservation_response_text(status, owner): """) % (owner['cn'], owner['mail'], owner['telephoneNumber'] if owner.has_key('telephoneNumber') else '') return message_text + + +def send_owner_notification(resource, owner, itip_event, success=True): + """ + Send a reservation notification to the resource owner + """ + import smtplib + from pykolab import utils + from email.MIMEText import MIMEText + from email.Utils import formatdate + + 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']: + # TODO: distingish ACCEPTED / DECLINED status notifications? + if policy & COND_NOTIFY and owner['mail']: + notify = True + break + + if notify or not success: + log.debug( + _("Sending booking notification for event %r to %r from %r") % ( + itip_event['uid'], owner['mail'], resource['cn'] + ), + level=8 + ) + + message_text = owner_notification_text(resource, owner, itip_event['xml'], success) + + msg = MIMEText(utils.stripped_message(message_text)) + + msg['To'] = owner['mail'] + msg['From'] = resource['mail'] + msg['Date'] = formatdate(localtime=True) + msg['Subject'] = _('Booking for %s has been %s') % (resource['cn'], _(status) if success else _('failed')) + + smtp = smtplib.SMTP("localhost", 10027) + + if conf.debuglevel > 8: + smtp.set_debuglevel(True) + + try: + smtp.sendmail(resource['mail'], owner['mail'], msg.as_string()) + except Exception, e: + log.error(_("SMTP sendmail error: %r") % (e)) + + smtp.quit() + +def owner_notification_text(resource, owner, event, success): + organizer = event.get_organizer() + status = event.get_attendee_by_email(resource['mail']).get_participant_status(True) + + if success: + message_text = _(""" + The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s. + + *** This is an automated message, sent to you as the resource owner. *** + """) + else: + message_text = _(""" + A reservation request for %(resource)s could not be processed automatically. + Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s. + + *** This is an automated message, sent to you as the resource owner. *** + """) + + return message_text % { + 'resource': resource['cn'], + 'summary': event.get_summary(), + 'date': event.get_date_text(), + 'status': _(status), + 'orgname': organizer.name(), + 'orgemail': organizer.email() + } |