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()