You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by as...@apache.org on 2013/05/14 15:06:47 UTC
svn commit: r1482323 - in
/bloodhound/trunk/bloodhound_relations/bhrelations: api.py notification.py
templates/relation_notify_email.txt tests/notification.py
Author: astaric
Date: Tue May 14 13:06:46 2013
New Revision: 1482323
URL: http://svn.apache.org/r1482323
Log:
Added notifications for ticket relations. Towards #542.
Added:
bloodhound/trunk/bloodhound_relations/bhrelations/notification.py
bloodhound/trunk/bloodhound_relations/bhrelations/templates/relation_notify_email.txt
bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py
Modified:
bloodhound/trunk/bloodhound_relations/bhrelations/api.py
Modified: bloodhound/trunk/bloodhound_relations/bhrelations/api.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/api.py?rev=1482323&r1=1482322&r2=1482323&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/api.py (original)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/api.py Tue May 14 13:06:46 2013
@@ -18,6 +18,7 @@
# specific language governing permissions and limitations
# under the License.
from datetime import datetime
+from pkg_resources import resource_filename
from bhrelations import db_default
from bhrelations.model import Relation
from multiproduct.api import ISupportMultiProductEnvironment
@@ -29,6 +30,7 @@ from trac.resource import (ResourceSyste
get_resource_shortname, Neighborhood)
from trac.ticket import Ticket, ITicketManipulator, ITicketChangeListener
from trac.util.datefmt import utc, to_utimestamp
+from trac.web.chrome import ITemplateProvider
PLUGIN_NAME = 'Bloodhound Relations Plugin'
@@ -83,7 +85,8 @@ class IRelationChangingListener(Interfac
class EnvironmentSetup(Component):
- implements(IEnvironmentSetupParticipant, ISupportMultiProductEnvironment)
+ implements(IEnvironmentSetupParticipant, ISupportMultiProductEnvironment,
+ ITemplateProvider)
def environment_created(self):
self.upgrade_environment(self.env.db_transaction)
@@ -142,6 +145,11 @@ class EnvironmentSetup(Component):
for statement in db_connector.to_sql(table):
db(statement)
+ # ITemplateProviderMethods
+ def get_templates_dirs(self):
+ """provide the plugin templates"""
+ return [resource_filename(__name__, 'templates')]
+
class RelationsSystem(Component):
PARENT_RELATION_TYPE = 'parent'
@@ -204,6 +212,9 @@ class RelationsSystem(Component):
for listener in self.changing_listeners:
listener.adding_relation(relation)
+ from bhrelations.notification import RelationNotifyEmail
+ RelationNotifyEmail(self.env).notify(relation)
+
def delete(self, relation_id, when=None):
if when is None:
when = datetime.now(utc)
@@ -226,6 +237,9 @@ class RelationsSystem(Component):
for listener in self.changing_listeners:
listener.deleting_relation(cloned_relation, when)
+ from bhrelations.notification import RelationNotifyEmail
+ RelationNotifyEmail(self.env).notify(cloned_relation, deleted=when)
+
def delete_resource_relations(self, resource_instance):
sql = "DELETE FROM " + Relation.get_table_name() + \
" WHERE source=%s OR destination=%s"
Added: bloodhound/trunk/bloodhound_relations/bhrelations/notification.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/notification.py?rev=1482323&view=auto
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/notification.py (added)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/notification.py Tue May 14 13:06:46 2013
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from trac.notification import NotifyEmail
+from trac.ticket.notification import (get_ticket_notification_recipients,
+ TicketNotifyEmail)
+from trac.util.datefmt import from_utimestamp
+from trac.web.chrome import Chrome
+
+from bhrelations.api import ResourceIdSerializer, TicketRelationsSpecifics
+
+
+class RelationNotifyEmail(TicketNotifyEmail):
+ template_name = "relation_notify_email.txt"
+
+ imitate_ticket_notification = False
+
+ def notify(self, relation, deleted=False):
+ self.relation = relation
+ source = ResourceIdSerializer.get_resource_by_id(relation.source)
+ if source.realm == 'ticket':
+ self.imitate_ticket_notification = True
+ helper = TicketRelationsSpecifics(self.env)
+ t = helper._create_ticket_by_full_id(source)
+ self.template = Chrome(self.env).load_template(
+ TicketNotifyEmail.template_name, method='text')
+ if deleted:
+ modtime = deleted
+ else:
+ modtime = from_utimestamp(relation.time)
+ TicketNotifyEmail.notify(self, t, newticket=False, modtime=modtime)
+ else:
+ self._generic_notify(relation, deleted)
+
+ def _generic_notify(self, relation, deleted):
+ self.data.update(dict(
+ created=not deleted,
+ relation=relation,
+ ))
+ NotifyEmail.notify(self, '', '', '')
+
+ def send(self, torcpts, ccrcpts):
+ if self.imitate_ticket_notification:
+ TicketNotifyEmail.send(self, torcpts, ccrcpts)
+ else:
+ NotifyEmail.send(self, torcpts, ccrcpts)
+
+ def get_recipients(self, relid):
+ relation = self.relation
+ source, destination = map(ResourceIdSerializer.get_resource_by_id,
+ (relation.source, relation.destination))
+ to, cc = [], []
+ for resource in (source, destination):
+ if resource.realm == 'ticket':
+ (torecipients, ccrecipients, reporter, owner) = \
+ get_ticket_notification_recipients(self.env, self.config,
+ resource.id, [])
+ to.extend(torecipients)
+ cc.extend(ccrecipients)
+ return to, cc
Added: bloodhound/trunk/bloodhound_relations/bhrelations/templates/relation_notify_email.txt
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/templates/relation_notify_email.txt?rev=1482323&view=auto
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/templates/relation_notify_email.txt (added)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/templates/relation_notify_email.txt Tue May 14 13:06:46 2013
@@ -0,0 +1,8 @@
+{% choose created %}\
+ {% when True %}\${_('Relation %(type)s %(source)s -> %(destination)s created.', type=relation.type, source=relation.source, destination=relation.destination)}{% end %}\
+ {% otherwise %}${_('Relation %(type)s %(source)s -> %(destination)s deleted.', type=relation.type, source=relation.source, destination=relation.destination)}{% end %}\
+{% end %}
+
+--
+$project.name <${project.url or abs_href()}>
+$project.descr
Added: bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py?rev=1482323&view=auto
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py (added)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py Tue May 14 13:06:46 2013
@@ -0,0 +1,158 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import unittest
+from trac.tests.notification import SMTPServerStore, SMTPThreadedServer
+from trac.ticket.tests.notification import (
+ SMTP_TEST_PORT, smtp_address, parse_smtp_message)
+from bhrelations.notification import RelationNotifyEmail
+from bhrelations.tests.api import BaseApiApiTestCase
+
+
+class NotificationTestCase(BaseApiApiTestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.smtpd = CustomSMTPThreadedServer(SMTP_TEST_PORT)
+ cls.smtpd.start()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.smtpd.stop()
+
+ def setUp(self):
+ super(NotificationTestCase, self).setUp()
+ self.env.config.set('notification', 'smtp_enabled', 'true')
+ self.env.config.set('notification', 'always_notify_owner', 'true')
+ self.env.config.set('notification', 'always_notify_reporter', 'true')
+ self.env.config.set('notification', 'smtp_always_cc',
+ 'joe.user@example.net, joe.bar@example.net')
+ self.env.config.set('notification', 'use_public_cc', 'true')
+ self.env.config.set('notification', 'smtp_port', str(SMTP_TEST_PORT))
+ self.env.config.set('notification', 'smtp_server', 'localhost')
+ self.notifier = RelationNotifyEmail(self.env)
+
+ def tearDown(self):
+ super(NotificationTestCase, self).tearDown()
+ self.smtpd.cleanup()
+
+ def test_recipients_of_both_related_tickets_get_notified(self):
+ """To/Cc recipients"""
+ ticket = self._insert_and_load_ticket(
+ 'Foo',
+ reporter= '"Joe User" < joe.user@example.org >',
+ owner='joe.user@example.net',
+ cc='joe.user@example.com, joe.bar@example.org, '
+ 'joe.bar@example.net'
+ )
+ ticket2 = self._insert_and_load_ticket(
+ 'Bar',
+ reporter='"Bob User" < bob.user@example.org >',
+ owner='bob.user@example.net',
+ cc='bob.user@example.com, bob.bar@example.org, '
+ 'bob.bar@example.net')
+ relation = self.relations_system.add(
+ ticket, ticket2, "dependent")
+
+ rn = RelationNotifyEmail(self.env)
+ rn.notify(relation)
+
+ recipients = self.smtpd.get_recipients()
+ # checks there is no duplicate in the recipient list
+ rcpts = []
+ for r in recipients:
+ self.failIf(r in rcpts)
+ rcpts.append(r)
+ # checks that all cc recipients have been notified
+ cc_list = self.env.config.get('notification', 'smtp_always_cc')
+ cc_list = "%s, %s, %s" % (cc_list, ticket['cc'], ticket2['cc'])
+ for r in cc_list.replace(',', ' ').split():
+ self.failIf(r not in recipients)
+ # checks that both owners have been notified
+ self.failIf(smtp_address(ticket['owner']) not in recipients)
+ self.failIf(smtp_address(ticket2['owner']) not in recipients)
+ # checks that both reporters have been notified
+ self.failIf(smtp_address(ticket['reporter']) not in recipients)
+ self.failIf(smtp_address(ticket2['reporter']) not in recipients)
+
+ def test_no_recipient_results_in_no_notification(self):
+ self.env.config.set('notification', 'smtp_always_cc', '')
+ ticket = self._insert_and_load_ticket('Foo', reporter='anonymous')
+ ticket2 = self._insert_and_load_ticket('Bar', reporter='anonymous')
+
+ self.relations_system.add(ticket, ticket2, "dependent")
+
+ sender = self.smtpd.get_sender()
+ recipients = self.smtpd.get_recipients()
+ message = self.smtpd.get_message()
+ # checks that no message has been sent
+ self.failIf(recipients)
+ self.failIf(sender)
+ self.failIf(message)
+
+ def test_one_email_per_relation(self):
+ ticket = self._insert_and_load_ticket('Foo', reporter='anonymous')
+ ticket2 = self._insert_and_load_ticket('Bar', reporter='anonymous')
+
+ relation = self.relations_system.add(ticket, ticket2, "dependent")
+
+ relations = self.env.db_direct_query(
+ "SELECT * FROM bloodhound_relations")
+ self.assertEqual(len(relations), 2)
+ self.assertEqual(self.smtpd.messages_received(), 1)
+
+ self.smtpd.cleanup()
+
+ self.relations_system.delete(relation.get_relation_id())
+ relations = self.env.db_direct_query(
+ "SELECT * FROM bloodhound_relations")
+ self.assertEqual(len(relations), 0)
+ self.assertEqual(self.smtpd.messages_received(), 1)
+
+
+class CustomSMTPServerStore(SMTPServerStore):
+ """SMTPServerStore that can count received messages"""
+ def __init__(self):
+ SMTPServerStore.__init__(self)
+ self.messages = 0
+
+ def helo(self, args):
+ SMTPServerStore.helo(self, args)
+ self.messages += 1
+
+
+class CustomSMTPThreadedServer(SMTPThreadedServer):
+ def __init__(self, port):
+ SMTPThreadedServer.__init__(self, port)
+ self.store = CustomSMTPServerStore()
+
+ def cleanup(self):
+ SMTPThreadedServer.cleanup(self)
+ self.store.messages = 0
+
+ def messages_received(self):
+ return self.store.messages
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(unittest.makeSuite(NotificationTestCase, 'test'))
+ return test_suite
+
+if __name__ == '__main__':
+ unittest.main()