Commits

Anonymous committed f97c97c Draft

Merged from trunk.

Comments (0)

Files changed (62)

+0.8
+ * i18n internationalization.
+ * Multiproduct support for CommitTicketUpdater.
+ * Quick Create Ticket form sets product based on context and forwards
+   values to the ticket form when "More fields" is selected.
+ * Fixed caching issue that prevented product-scope trac-admin commands
+   from taking effect immediately.
+ * Improved presentation of the Products page.
+ * Improvements to ticket timeline (comments section).
+ * BloodhoundSearch now supports Whoosh >= 2.5.1 (upgraded from 2.4.1).
+ * Fixed errors in BloodhoundRelations validation logic.
+ * Bootstrap template for the Roadmap and Edit Product views.
+ * Bloodhound installer support for MySQL database.
+ * Numerous other important fixes and minor enhancements.
+
+
+ * Not fixed for this release
+  * Cache is not properly refreshed for resources including wiki pages,
+    milestones, components and permissions; leading to stale data being
+    displayed after INSERTs and DELETEs (#613, #614, #620, #681, #719, #748).
+  * Products cannot be deleted (#517).
+  * Duplicate relation is not added when batch modifying tickets (#761).
+  * TicketDeleter component can't be used with BloodhoundTheme (#427).
+  * No product-scope permission checks on the global dashboard (#572).
+  * Inconsistencies in query views when running PostgreSQL (#730).
+
 0.7
  * Duplicate ticket relation is integrated to the ticket workflow.
  * Several fixes for multiproduct installations with products that map to subdomains.

bloodhound_dashboard/bhdashboard/widgets/templates/widget_cloud.html

     </span>
   </py:def>
 
-  <py:if test="heading is not None">
-  <h3 style="display: inline;">$heading</h3>
-  <br/>
-  </py:if>
+  <h3 py:if="heading is not None and items" style="display: inline;">$heading</h3>
   <py:if test="_view != view">
     <xi:include href="widget_alert.html"
         py:with="msglabel = 'Warning'; msgbody = 'Unknown view ' + view" />

bloodhound_dashboard/setup.py

                """
 extra = {}
 try:
-    from trac.util.dist import get_l10n_js_cmdclass
-    cmdclass = get_l10n_js_cmdclass()
+    from trac.util.dist import get_l10n_cmdclass
+    cmdclass = get_l10n_cmdclass()
     if cmdclass:
         extra['cmdclass'] = cmdclass
         extractors = [
     url = "https://bloodhound.apache.org/",
     requires = ['trac'],
     tests_require = ['dutest>=0.2.4', 'TracXMLRPC'],
-    install_requires = [
-        'setuptools>=0.6b1',
-        'Trac>=0.11',
-    ],
     package_dir = dict([p, i[0]] for p, i in PKG_INFO.iteritems()),
     packages = PKG_INFO.keys(),
     package_data = dict([p, i[1]] for p, i in PKG_INFO.iteritems()),

bloodhound_multiproduct/multiproduct/hooks.py

 
 # these imports monkey patch classes required to enable
 # multi product support
-import multiproduct.env
-import multiproduct.dbcursor
-import multiproduct.versioncontrol
-import multiproduct.ticket.query
-import multiproduct.ticket.batch
 
 import re
 
-from trac.core import TracError
 from trac.hooks import EnvironmentFactoryBase, RequestFactoryBase
-from trac.perm import PermissionCache
 from trac.web.href import Href
 from trac.web.main import RequestWithSession
 
+import multiproduct.env
+import multiproduct.dbcursor
+import multiproduct.ticket.batch
+import multiproduct.ticket.query
+import multiproduct.versioncontrol
+
 PRODUCT_RE = re.compile(r'^/products(?:/(?P<pid>[^/]*)(?P<pathinfo>.*))?')
 
+
 class MultiProductEnvironmentFactory(EnvironmentFactoryBase):
     def open_environment(self, environ, env_path, global_env, use_cache=False):
         environ.setdefault('SCRIPT_NAME', '')  # bh:ticket:594
 
 
 class ProductizedHref(Href):
-    PATHS_NO_TRANSFORM = ['chrome',
-                          'login',
-                          'logout',
-                          'prefs',
-                          'products',
-                          'verify_email',
-                          'reset_password',
-                          'register',
-                          ]
-    STATIC_PREFIXES = ['js/',
-                       'css/',
-                       'img/',
-                       ]
+    PATHS_NO_TRANSFORM = ['chrome', 'login', 'logout', 'prefs', 'products',
+                          'register',  'reset_password', 'verify_email']
+    STATIC_PREFIXES = ['css/', 'img/', 'js/']
 
     def __init__(self, global_href, base):
         self.super = super(ProductizedHref, self)
 class ProductRequestFactory(RequestFactoryBase):
     def create_request(self, env, environ, start_response):
         return ProductRequestWithSession(env, environ, start_response) \
-            if env else RequestWithSession(environ, start_response)
+               if env else RequestWithSession(environ, start_response)

bloodhound_multiproduct/multiproduct/product_admin.py

                       'description':description,
                       'owner':owner,
                       }
+        data = {}
 
         # Detail view?
         if product:
             default = self.config.get('ticket', 'default_product')
             if req.method == 'POST':
                 # Add Product
-                if req.args.get('add') and req.args.get('prefix'):
+                if req.args.get('add'):
                     req.perm.require('PRODUCT_CREATE')
-                    if not owner:
-                        add_warning(req, _('All fields are required!'))
-                        req.redirect(req.href.admin(cat, page))
-
-                    try:
-                        prod = Product(self.env, keys)
-                    except ResourceNotFound:
-                        prod = Product(self.env)
-                        prod.update_field_dict(keys)
-                        prod.update_field_dict(field_data)
-                        prod.insert()
-                        add_notice(req, _('The product "%(id)s" has been added.',
-                                          id=prefix))
-                        req.redirect(req.href.admin(cat, page))
+                    if not (prefix and name and owner):
+                        if not prefix:
+                            add_warning(req, _("You must provide a prefix "
+                                               "for the product."))
+                        if not name:
+                            add_warning(req, _("You must provide a name "
+                                               "for the product."))
+                        if not owner:
+                            add_warning(req, _("You must provide an owner "
+                                               "for the product."))
+                        data['prefix'] = prefix
+                        data['name'] = name
+                        data['owner'] = owner
                     else:
-                        if prod.prefix is None:
-                            raise TracError(_('Invalid product id.'))
-                        raise TracError(_("Product %(id)s already exists.",
-                                          id=prefix))
+                        try:
+                            prod = Product(self.env, keys)
+                        except ResourceNotFound:
+                            prod = Product(self.env)
+                            prod.update_field_dict(keys)
+                            prod.update_field_dict(field_data)
+                            prod.insert()
+                            add_notice(req, _('The product "%(id)s" has been '
+                                              'added.', id=prefix))
+                            req.redirect(req.href.admin(cat, page))
+                        else:
+                            if prod.prefix is None:
+                                raise TracError(_('Invalid product id.'))
+                            raise TracError(_("Product %(id)s already exists.",
+                                              id=prefix))
 
                 # Remove product
                 elif req.args.get('remove'):
                     _save_config(self.config, req, self.log)
                     req.redirect(req.href.admin(cat, page))
 
-            products = Product.select(self.env)
-            data = {'view': 'list',
-                    'products': products,
-                    'default': default}
+            data['view'] = 'list'
+            data['products'] = Product.select(self.env)
+            data['default'] = default
         if self.config.getbool('ticket', 'restrict_owner'):
             perm = PermissionSystem(self.env)
             def valid_owner(username):

bloodhound_multiproduct/multiproduct/templates/admin_products.html

           <fieldset>
             <legend>Add Product:</legend>
             <div class="field">
-              <label>Prefix:<br /><input type="text" name="prefix" /></label>
+              <label>Prefix:<br /><input type="text" name="prefix" value="$prefix" /></label>
             </div>
             <div class="field">
-              <label>Name:<br /><input type="text" name="name" /></label>
+              <label>Name:<br /><input type="text" name="name" value="$name" /></label>
             </div>
-            ${owner_field()}
+            ${owner_field(owner)}
             <div class="buttons">
               <input type="submit" name="add" value="${_('Add')}"/>
             </div>

bloodhound_multiproduct/multiproduct/ticket/query.py

         return self.cols
 
     def _get_ticket_href(self, prefix, tid):
-        env = lookup_product_env(self.env, prefix)
-        href = resolve_product_href(env, self.env)
-        return href.ticket(tid)
+        try:
+            env = lookup_product_env(self.env, prefix)
+        except LookupError:
+            return '#invalid-product-' + prefix
+        else:
+            href = resolve_product_href(env, self.env)
+            return href.ticket(tid)
 
     def get_href(self, href, id=None, order=None, desc=None, format=None,
                  max=None, page=None):

bloodhound_multiproduct/setup.py

 
 extra = {}
 try:
-    from trac.util.dist import get_l10n_js_cmdclass
-    cmdclass = get_l10n_js_cmdclass()
+    from trac.util.dist import get_l10n_cmdclass
+    cmdclass = get_l10n_cmdclass()
     if cmdclass:
         extra['cmdclass'] = cmdclass
         extractors = [

bloodhound_multiproduct/tests/admin/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Tests for Apache(TM) Bloodhound's administration in product environments"""
-
+import unittest
 from tests import TestLoader
 
+
 def test_suite():
-    return TestLoader().discover_package(__name__, pattern='*.py')
+    return TestLoader().discover_package(__package__, pattern='*.py')
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

bloodhound_multiproduct/tests/db/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
+import unittest
 from tests import TestLoader
 
+
 def test_suite():
-    return TestLoader().discover_package(__name__, pattern='*.py')
+    return TestLoader().discover_package(__package__, pattern='*.py')
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

bloodhound_multiproduct/tests/mimeview/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Tests for Apache(TM) Bloodhound's MIME API in product environments"""
-
+import unittest
 from tests import TestLoader
 
+
 def test_suite():
-    return TestLoader().discover_package(__name__, pattern='*.py')
+    return TestLoader().discover_package(__package__, pattern='*.py')
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

bloodhound_multiproduct/tests/ticket/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Tests for Apache(TM) Bloodhound's tickets in product environments"""
-
+import unittest
 from tests import TestLoader
 
+
 def test_suite():
-    return TestLoader().discover_package(__name__, pattern='*.py')
+    return TestLoader().discover_package(__package__, pattern='*.py')
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

bloodhound_multiproduct/tests/versioncontrol/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Tests for Apache(TM) Bloodhound's version control in product environments"""
-
+import unittest
 from tests import TestLoader
 
+
 def test_suite():
-    return TestLoader().discover_package(__name__, pattern='*.py')
+    return TestLoader().discover_package(__package__, pattern='*.py')
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

bloodhound_multiproduct/tests/web/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Tests for Apache(TM) Bloodhound web API in product environments"""
-
+import unittest
 from tests import TestLoader
 
+
 def test_suite():
-    return TestLoader().discover_package(__name__, pattern='*.py')
+    return TestLoader().discover_package(__package__, pattern='*.py')
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

bloodhound_multiproduct/tests/wiki/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-"""Tests for Apache(TM) Bloodhound's wiki subsystem in product environments"""
-
+import unittest
 from tests import TestLoader
 
+
 def test_suite():
-    return TestLoader().discover_package(__name__, pattern='*.py')
+    return TestLoader().discover_package(__package__, pattern='*.py')
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

bloodhound_relations/README

 
 == The Trac ticket-links branch
 Bloodhound Relations plugin contains the code from the Trac ticket-links branch, which
-is licensed under the same license as Trac (http://trac.edgewall.org/wiki/TracLicense).
+is licensed under the same license as Trac (http://trac.edgewall.org/wiki/TracLicense).

bloodhound_relations/TESTING_README

 
-= Testing Bloodhound Search plugin =
+= Testing Bloodhound Relations plugin =
 
 == Overview ==
 

bloodhound_relations/bhrelations/api.py

 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
 import itertools
-
 import re
 from datetime import datetime
 from pkg_resources import resource_filename
+
+from trac.config import OrderedExtensionsOption, Option
+from trac.core import Component, ExtensionPoint, Interface, TracError, \
+                      implements
+from trac.db import DatabaseManager
+from trac.env import IEnvironmentSetupParticipant
+from trac.resource import Neighborhood, Resource, ResourceNotFound, \
+                          ResourceSystem, get_resource_shortname
+from trac.ticket.api import ITicketChangeListener, ITicketManipulator, \
+                            TicketSystem
+from trac.ticket.model import Ticket
+from trac.util.datefmt import to_utimestamp, utc
+from trac.web.chrome import ITemplateProvider
+
+from multiproduct.api import ISupportMultiProductEnvironment
+from multiproduct.model import Product
+from multiproduct.env import ProductEnvironment
+
 from bhrelations import db_default
 from bhrelations.model import Relation
 from bhrelations.utils import unique
 from bhrelations.utils.translation import _, add_domain
-from multiproduct.api import ISupportMultiProductEnvironment
-from multiproduct.model import Product
-from multiproduct.env import ProductEnvironment
 
-from trac.config import OrderedExtensionsOption, Option
-from trac.core import (Component, implements, TracError, Interface,
-                       ExtensionPoint)
-from trac.env import IEnvironmentSetupParticipant
-from trac.db import DatabaseManager
-from trac.resource import (ResourceSystem, Resource, ResourceNotFound,
-                           get_resource_shortname, Neighborhood)
-from trac.ticket import Ticket, ITicketManipulator, ITicketChangeListener
-from trac.ticket.api import TicketSystem
-from trac.util.datefmt import utc, to_utimestamp
-from trac.web.chrome import ITemplateProvider
 
 PLUGIN_NAME = 'Bloodhound Relations Plugin'
 RELATIONS_CONFIG_NAME = 'bhrelations_links'
     implements(IEnvironmentSetupParticipant, ISupportMultiProductEnvironment,
                ITemplateProvider)
 
+    # IEnvironmentSetupParticipant methods
+
     def environment_created(self):
         self.upgrade_environment(self.env.db_transaction)
 
 
         db_version = db_default.DB_VERSION
         if db_installed_version > db_version:
-            raise TracError('''Current db version (%d) newer than supported by
-            this version of the %s (%d).''' % (db_installed_version,
-                                               PLUGIN_NAME,
-                                               db_version))
+            raise TracError("""Current db version (%d) newer than supported
+                            by this version of the %s (%d)."""
+                            % (db_installed_version, PLUGIN_NAME, db_version))
         needs_upgrade = db_installed_version < db_version or \
                         not list(self.config.options(RELATIONS_CONFIG_NAME))
         return needs_upgrade
 
     def _get_version(self, db):
         """Finds the current version of the bloodhound database schema"""
-        rows = db("""
-            SELECT value FROM system WHERE name = %s
-            """, (db_default.DB_SYSTEM_KEY,))
+        rows = db("""SELECT value FROM system WHERE name = %s
+                  """, (db_default.DB_SYSTEM_KEY,))
         return int(rows[0][0]) if rows else -1
 
     def _update_db_version(self, db, version):
         old_version = self._get_version(db)
         if old_version != -1:
-            self.log.info(
-                "Updating %s database schema from version %d to %d",
-                PLUGIN_NAME, old_version, version)
+            self.log.info("Updating %s database schema from version %d to %d",
+                          PLUGIN_NAME, old_version, version)
             db("""UPDATE system SET value=%s
-                      WHERE name=%s""", (version, db_default.DB_SYSTEM_KEY))
+                  WHERE name=%s""", (version, db_default.DB_SYSTEM_KEY))
         else:
-            self.log.info(
-                "Initial %s database schema set to version %d",
-                PLUGIN_NAME, version)
-            db("""
-                INSERT INTO system (name, value) VALUES ('%s','%s')
+            self.log.info("Initial %s database schema set to version %d",
+                          PLUGIN_NAME, version)
+            db("""INSERT INTO system (name, value) VALUES ('%s','%s')
                 """ % (db_default.DB_SYSTEM_KEY, version))
         return version
 
                 db(statement)
 
     # ITemplateProviderMethods
+
     def get_templates_dirs(self):
         """provide the plugin templates"""
         return [resource_filename(__name__, 'templates')]
         return self._select_relations(resource_full_id)
 
     def _select_relations(
-            self, source, resource_type=None):
+            self, source=None, resource_type=None, destination=None):
         #todo: add optional paging for possible umbrella tickets with
         #a lot of child tickets
-        where = dict(source=source)
+        where = dict()
+        if source:
+            where["source"] = source
         if resource_type:
             where["type"] = resource_type
             order_by = ["destination"]
         else:
             order_by = ["type", "destination"]
+        if destination:
+            where["destination"] = destination
         return Relation.select(
             self.env,
             where=where,
         * resource_full_id: fully qualified resource id in format
         "product:ticket:123". In case of global environment it is ":ticket:123"
         """
-        nbhprefix, realm, resource_id = cls.split_full_id(resource_full_id)
-        if nbhprefix:
-            neighborhood = Neighborhood('product', nbhprefix)
+        nbh_prefix, realm, resource_id = cls.split_full_id(resource_full_id)
+        if nbh_prefix:
+            neighborhood = Neighborhood('product', nbh_prefix)
             return neighborhood.child(realm, id=resource_id)
         else:
             return Resource(realm, id=resource_id)
         if ticket.id is None:
             raise ValueError("Cannot get resource id for ticket "
                              "that does not exist yet.")
-        nbhprefix = ticket["product"]
+        nbh_prefix = ticket["product"]
 
         resource_full_id = cls.RESOURCE_ID_DELIMITER.join(
-            (nbhprefix, resource.realm, unicode(resource.id))
+            (nbh_prefix, resource.realm, unicode(resource.id))
         )
         return resource_full_id
 
     def ticket_changed(self, ticket, comment, author, old_values):
         if self._closed_as_duplicate(ticket) and \
                 self.rls.duplicate_relation_type and \
-                hasattr(ticket, 'duplicate'): # workaround for comment:5:ticket:710
+                hasattr(ticket, 'duplicate'):  # workaround for comment:5:ticket:710
             try:
                 self.rls.add(ticket, ticket.duplicate,
                              self.rls.duplicate_relation_type,
         operations = self._get_operations_for_action(req, ticket, action)
         if 'set_resolution' in operations:
             for relation in [r for r in self.rls.get_relations(ticket)
-                             if r['type'] == self.rls.CHILDREN_RELATION_TYPE]:
+                             if r['type'] == self.rls.PARENT_RELATION_TYPE]:
                 child_ticket = \
                     self._create_ticket_by_full_id(relation['destination'])
                 if child_ticket['status'] != 'closed':
                     for p in Product.select(self.env):
                         if p.prefix != self.env.product.prefix:
                             # TODO: check for PRODUCT_VIEW permissions
-                            penv = ProductEnvironment(self.env.parent, p.prefix)
+                            penv = ProductEnvironment(self.env.parent,
+                                                      p.prefix)
                             try:
                                 ticket = Ticket(penv, tid)
                             except ResourceNotFound:
 
     def _get_env_for_resource(self, resource):
         if hasattr(resource, "neighborhood"):
-            env = ResourceSystem(self.env).load_component_manager(
-                resource.neighborhood)
+            env = ResourceSystem(self.env). \
+                    load_component_manager(resource.neighborhood)
         else:
             env = self.env
         return env
                 )
 
     def _get_ticket_id_and_product(self, resource_full_id):
-        nbhprefix, realm, resource_id = ResourceIdSerializer.split_full_id(
-            resource_full_id)
+        nbh_prefix, realm, resource_id = \
+            ResourceIdSerializer.split_full_id(resource_full_id)
         ticket_id = None
         if realm == "ticket":
             ticket_id = int(resource_id)
-        return ticket_id, nbhprefix
+        return ticket_id, nbh_prefix
 
     def _add_ticket_change_record(
             self, db, relation, relation_system, is_delete, when_ts):

bloodhound_relations/bhrelations/db_default.py

 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
 from bhrelations.model import Relation
 
 DB_SYSTEM_KEY = 'bhrelations'

bloodhound_relations/bhrelations/model.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-from bhdashboard.model import ModelBase
 from trac.resource import Resource
 from trac.util.datefmt import to_utimestamp, from_utimestamp
 
+from bhdashboard.model import ModelBase
+
 
 class Relation(ModelBase):
     """The Relation table"""

bloodhound_relations/bhrelations/notification.py

 #  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.ticket.notification import TicketNotifyEmail, \
+                                     get_ticket_notification_recipients
 from trac.util.datefmt import from_utimestamp
 from trac.web.chrome import Chrome
 
         to, cc = [], []
         for resource in (source, destination):
             if resource.realm == 'ticket':
-                (torecipients, ccrecipients, reporter, owner) = \
+                torecipients, ccrecipients, reporter, owner = \
                     get_ticket_notification_recipients(self.env, self.config,
                     resource.id, [])
                 to.extend(torecipients)

bloodhound_relations/bhrelations/templates/relations_manage.html

           <div class="control-group">
             <label class="control-label" for="comment">Comment</label>
             <div class="controls">
-              <textarea name="comment" rows="3" class="span4">${relation.comment}</textarea>
+              <textarea id="comment" name="comment" rows="3" class="span4">${relation.comment}</textarea>
             </div>
           </div>
 
     </div>
   </body>
 </html>
-

bloodhound_relations/bhrelations/tests/__init__.py

 #  specific language governing permissions and limitations
 #  under the License.
 
-# import sys
-# if sys.version < (2, 7):
-#     import unittest2 as unittest
-# else:
-#     import unittest
 import unittest
 
 from bhrelations.tests import api, notification, search, validation, web_ui
     test_suite.addTest(web_ui.suite())
     return test_suite
 
+
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
 else:

bloodhound_relations/bhrelations/tests/api.py

 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
+import unittest
 from datetime import datetime
-import unittest
+
+from trac.core import TracError
+from trac.ticket.model import Ticket
+from trac.util.datefmt import utc
+
+from multiproduct.env import ProductEnvironment
+
 from bhrelations.api import TicketRelationsSpecifics
 from bhrelations.tests.mocks import TestRelationChangingListener
 from bhrelations.validation import ValidationError
-from bhrelations.tests.base import BaseRelationsTestCase
-from multiproduct.env import ProductEnvironment
-from trac.ticket.model import Ticket
-from trac.core import TracError
-from trac.util.datefmt import utc
+from bhrelations.tests.base import BaseRelationsTestCase, BLOCKED_BY, \
+                                   BLOCKS, CHILD, DEPENDENCY_OF, DEPENDS_ON, \
+                                   DUPLICATE_OF, MULTIPRODUCT_REL, PARENT, \
+                                   REFERS_TO
 
 
 class ApiTestCase(BaseRelationsTestCase):
     def test_can_add_two_ways_relations(self):
         #arrange
         ticket = self._insert_and_load_ticket("A1")
-        dependent = self._insert_and_load_ticket("A2")
+        ticket2 = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(
-            ticket, dependent, "dependent")
+        self.add_relation(ticket, DEPENDENCY_OF, ticket2)
         #assert
-        relations = relations_system.get_relations(ticket)
-        self.assertEqual("dependent", relations[0]["type"])
-        self.assertEqual(unicode(dependent.id), relations[0]["destination"].id)
+        relations = self.get_relations(ticket)
+        self.assertEqual(DEPENDENCY_OF, relations[0]["type"])
+        self.assertEqual(unicode(ticket2.id), relations[0]["destination"].id)
 
-        relations = relations_system.get_relations(dependent)
-        self.assertEqual("dependson", relations[0]["type"])
+        relations = self.get_relations(ticket2)
+        self.assertEqual(DEPENDS_ON, relations[0]["type"])
         self.assertEqual(unicode(ticket.id), relations[0]["destination"].id)
 
     def test_can_add_single_way_relations(self):
         #arrange
         ticket = self._insert_and_load_ticket("A1")
-        referred = self._insert_and_load_ticket("A2")
+        ticket2 = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(ticket, referred, "refersto")
+        self.add_relation(ticket, REFERS_TO, ticket2)
         #assert
-        relations = relations_system.get_relations(ticket)
-        self.assertEqual("refersto", relations[0]["type"])
-        self.assertEqual(unicode(referred.id), relations[0]["destination"].id)
+        relations = self.get_relations(ticket)
+        self.assertEqual(1, len(relations))
+        self.assertEqual(REFERS_TO, relations[0]["type"])
+        self.assertEqual(unicode(ticket2.id), relations[0]["destination"].id)
 
-        relations = relations_system.get_relations(referred)
-        self.assertEqual(0, len(relations))
+        self.assertEqual(0, len(self.get_relations(ticket2)))
 
     def test_can_add_multiple_relations(self):
         #arrange
         ticket = self._insert_and_load_ticket("A1")
-        dependent1 = self._insert_and_load_ticket("A2")
-        dependent2 = self._insert_and_load_ticket("A3")
+        ticket2 = self._insert_and_load_ticket("A2")
+        ticket3 = self._insert_and_load_ticket("A3")
         #act
-        relations_system = self.relations_system
-        relations_system.add(
-            ticket, dependent1, "dependent")
-        relations_system.add(
-            ticket, dependent2, "dependent")
+        self.add_relation(ticket, DEPENDS_ON, ticket2)
+        self.add_relation(ticket, DEPENDS_ON, ticket3)
         #assert
-        relations = relations_system.get_relations(ticket)
-        self.assertEqual(2, len(relations))
+        self.assertEqual(2, len(self.get_relations(ticket)))
+        self.assertEqual(1, len(self.get_relations(ticket2)))
+        self.assertEqual(1, len(self.get_relations(ticket3)))
 
     def test_will_not_create_more_than_one_identical_relations(self):
         #arrange
         ticket = self._insert_and_load_ticket("A1")
-        dependent1 = self._insert_and_load_ticket("A2")
+        ticket2 = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(
-            ticket, dependent1, "dependent")
+        self.add_relation(ticket, DEPENDS_ON, ticket2)
         self.assertRaisesRegexp(
             TracError,
             "already exists",
-            relations_system.add,
-            ticket, dependent1, "dependent")
+            self.add_relation,
+            ticket, DEPENDS_ON, ticket2
+        )
 
     def test_will_not_create_more_than_one_identical_relations_db_level(self):
         sql = """INSERT INTO bloodhound_relations (source, destination, type)
                     VALUES (%s, %s, %s)"""
         with self.env.db_transaction as db:
-            db(sql, ["1", "2", "dependson"])
+            db(sql, ["1", "2", DEPENDS_ON])
             self.assertRaises(
                 self.env.db_exc.IntegrityError,
                 db,
                 sql,
-                ["1", "2", "dependson"])
+                ["1", "2", DEPENDS_ON]
+            )
 
     def test_can_add_one_way_relations(self):
         #arrange
         ticket = self._insert_and_load_ticket("A1")
-        referred_ticket = self._insert_and_load_ticket("A2")
+        ticket2 = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(
-            ticket, referred_ticket, "refersto")
+        self.add_relation(ticket, REFERS_TO, ticket2)
         #assert
-        relations = relations_system.get_relations(ticket)
-        self.assertEqual("refersto", relations[0]["type"])
-        self.assertEqual(unicode(referred_ticket.id),
+        relations = self.get_relations(ticket)
+        self.assertEqual(REFERS_TO, relations[0]["type"])
+        self.assertEqual(unicode(ticket2.id),
                          relations[0]["destination"].id)
 
-        relations = relations_system.get_relations(referred_ticket)
-        self.assertEqual(0, len(relations))
+        self.assertEqual(0, len(self.get_relations(ticket2)))
 
     def test_can_delete_two_ways_relation(self):
         #arrange
         ticket = self._insert_and_load_ticket("A1")
-        dependent_ticket = self._insert_and_load_ticket("A2")
-        relations_system = self.relations_system
-        relations_system.add(
-            ticket, dependent_ticket, "dependson")
-        relations = relations_system.get_relations(ticket)
+        ticket2 = self._insert_and_load_ticket("A2")
+        self.add_relation(ticket, DEPENDS_ON, ticket2)
+
+        relations = self.get_relations(ticket)
         self.assertEqual(1, len(relations))
+        self.assertEqual(1, len(self.get_relations(ticket2)))
+
         #act
-        relation_to_delete = relations[0]
-        relations_system.delete(relation_to_delete["relation_id"])
+        self.delete_relation(relations[0])
         #assert
-        relations = relations_system.get_relations(ticket)
-        self.assertEqual(0, len(relations))
+        self.assertEqual(0, len(self.get_relations(ticket)))
+        self.assertEqual(0, len(self.get_relations(ticket2)))
 
     def test_can_delete_single_way_relation(self):
         #arrange
         ticket = self._insert_and_load_ticket("A1")
-        referred = self._insert_and_load_ticket("A2")
+        ticket2 = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(ticket, referred, "refersto")
+        self.add_relation(ticket, REFERS_TO, ticket2)
 
-        ticket = self._insert_and_load_ticket("A1")
-        dependent_ticket = self._insert_and_load_ticket("A2")
-        relations_system = self.relations_system
-        relations_system.add(
-            ticket, dependent_ticket, "dependson")
-        relations = relations_system.get_relations(ticket)
-
+        relations = self.get_relations(ticket)
         self.assertEqual(1, len(relations))
-        reverted_relations = relations_system.get_relations(dependent_ticket)
-        self.assertEqual(1, len(reverted_relations))
+        self.assertEqual(0, len(self.get_relations(ticket2)))
         #act
-        # self._debug_select()
-        relation_to_delete = relations[0]
-        relations_system.delete(relation_to_delete["relation_id"])
+        self.delete_relation(relations[0])
         #assert
-        relations = relations_system.get_relations(ticket)
-        self.assertEqual(0, len(relations))
-        reverted_relations = relations_system.get_relations(dependent_ticket)
-        self.assertEqual(0, len(reverted_relations))
+        self.assertEqual(0, len(self.get_relations(ticket)))
 
     def test_can_not_add_cycled_immediate_relations(self):
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(ticket1, ticket2, "dependson")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
 
         try:
-            relations_system.add(ticket2, ticket1, "dependson")
+            self.add_relation(ticket2, DEPENDS_ON, ticket1)
             self.fail("Should throw an exception")
         except ValidationError as ex:
             self.assertSequenceEqual(
         ticket2 = self._insert_and_load_ticket("A2")
         ticket3 = self._insert_and_load_ticket("A3")
         #act
-        relations_system = self.relations_system
-        relations_system.add(ticket1, ticket2, "dependson")
-        relations_system.add(ticket1, ticket3, "dependson")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
+        self.add_relation(ticket1, DEPENDS_ON, ticket3)
+
+        self.assertEqual(2, len(self.get_relations(ticket1)))
 
     def test_can_not_add_cycled_in_different_direction(self):
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(ticket1, ticket2, "dependson")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
         self.assertRaises(
             ValidationError,
-            relations_system.add,
-            ticket1,
-            ticket2,
-            "dependent")
+            self.add_relation,
+            ticket1, DEPENDENCY_OF, ticket2
+        )
 
     def test_can_not_add_cycled_relations(self):
         #arrange
         ticket2 = self._insert_and_load_ticket("A2")
         ticket3 = self._insert_and_load_ticket("A3")
         #act
-        relations_system = self.relations_system
-        relations_system.add(ticket1, ticket2, "dependson")
-        relations_system.add(ticket2, ticket3, "dependson")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
+        self.add_relation(ticket2, DEPENDS_ON, ticket3)
         self.assertRaises(
             ValidationError,
-            relations_system.add,
-            ticket3,
-            ticket1,
-            "dependson")
+            self.add_relation,
+            ticket3, DEPENDS_ON, ticket1
+        )
 
-    def test_can_not_add_more_than_one_parents(self):
+    def test_can_not_add_more_than_one_parent(self):
         #arrange
         child = self._insert_and_load_ticket("A1")
         parent1 = self._insert_and_load_ticket("A2")
         parent2 = self._insert_and_load_ticket("A3")
         #act
-        relations_system = self.relations_system
-        relations_system.add(child, parent1, "parent")
+        self.add_relation(parent1, PARENT, child)
         self.assertRaises(
             ValidationError,
-            relations_system.add,
-            child,
-            parent2,
-            "parent")
+            self.add_relation,
+            parent2, PARENT, child
+        )
 
-    def test_can_not_add_more_than_one_parents_via_children(self):
-        #arrange
-        child = self._insert_and_load_ticket("A1")
-        parent1 = self._insert_and_load_ticket("A2")
-        parent2 = self._insert_and_load_ticket("A3")
-        #act
-        relations_system = self.relations_system
-        relations_system.add(parent1, child, "children")
         self.assertRaises(
             ValidationError,
-            relations_system.add,
-            parent2,
-            child,
-            "children")
+            self.add_relation,
+            child, CHILD, parent2
+        )
+
+    def test_can_add_more_than_one_child(self):
+        parent = self._insert_and_load_ticket("A1")
+        child1 = self._insert_and_load_ticket("A2")
+        child2 = self._insert_and_load_ticket("A3")
+
+        self.add_relation(parent, PARENT, child1)
+        self.add_relation(parent, PARENT, child2)
+        self.assertEqual(2, len(self.get_relations(parent)))
 
     def test_ticket_can_be_resolved(self):
         #arrange
-        child = self._insert_and_load_ticket("A1")
-        parent1 = self._insert_and_load_ticket("A2")
-        parent2 = self._insert_and_load_ticket("A3")
+        parent = self._insert_and_load_ticket("A1")
+        child = self._insert_and_load_ticket("A2")
         #act
-        relations_system = self.relations_system
-        relations_system.add(parent1, child, "children")
-        self.assertRaises(
-            ValidationError,
-            relations_system.add,
-            parent2,
-            child,
-            "children")
+        self.add_relation(parent, PARENT, child)
+
+        self.req.args['action'] = 'resolve'
+        warnings = \
+            TicketRelationsSpecifics(self.env).validate_ticket(self.req, child)
+        self.assertEqual(0, len(list(warnings)))
 
     def test_can_save_and_load_relation_time(self):
         #arrange
         ticket2 = self._insert_and_load_ticket("A2")
         #act
         time = datetime.now(utc)
-        self.relations_system.add(ticket1, ticket2, "dependent", when=time)
-        relations = self.relations_system.get_relations(ticket1)
+        self.add_relation(ticket1, DEPENDS_ON, ticket2, when=time)
+        relations = self.get_relations(ticket1)
         #assert
         self.assertEqual(time, relations[0]["when"])
 
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        self.relations_system.add(ticket1, ticket2, "dependson")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
         #act
         self.req.args["action"] = 'resolve'
         warnings = TicketRelationsSpecifics(self.env).validate_ticket(
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2", status="closed")
-        self.relations_system.add(ticket1, ticket2, "dependson")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
         #act
         self.req.args["action"] = 'resolve'
         warnings = TicketRelationsSpecifics(self.env).validate_ticket(
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        relations_system = self.relations_system
-        relations_system.add(ticket1, ticket2, "dependent")
-        self.assertEqual(1, len(relations_system.get_relations(ticket2)))
+
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
+        self.assertEqual(1, len(self.get_relations(ticket2)))
         #act
         ticket1.delete()
         #assert
-        self.assertEqual(0, len(relations_system.get_relations(ticket2)))
+        self.assertEqual(0, len(self.get_relations(ticket2)))
 
     def test_that_no_error_when_deleting_ticket_without_relations(self):
         #arrange
         ticket1.delete()
 
     def test_can_add_multi_product_relations(self):
-        #arrange
+
         ticket1 = self._insert_and_load_ticket("A1")
-
         product2 = "tp2"
         self._load_product_from_data(self.global_env, product2)
         p2_env = ProductEnvironment(self.global_env, product2)
         ticket2 = self._insert_and_load_ticket_with_env(p2_env, "A2")
-        relations_system = self.relations_system
-        #act
-        relations_system.add(ticket1, ticket2, "mprel")
-        #assert
-        self.assertEqual(1, len(relations_system.get_relations(ticket1)))
-        self.assertEqual(1, len(relations_system.get_relations(ticket2)))
+
+        self.add_relation(ticket1, MULTIPRODUCT_REL, ticket2)
+
+        self.assertEqual(1, len(self.get_relations(ticket1)))
+        self.assertEqual(1, len(self.get_relations(ticket2)))
 
     def _debug_select(self):
         """
     def test_parent_relation_is_incompatible_with_two_way_relations(self):
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        self.relations_system.add(ticket1, ticket2, "dependent")
+        self.add_relation(ticket2, DEPENDS_ON, ticket1)
 
         self.assertRaises(
             ValidationError,
-            self.relations_system.add,
-            ticket1,
-            ticket2,
-            "parent")
+            self.add_relation,
+            ticket1, PARENT, ticket2
+        )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add,
-            ticket1,
-            ticket2,
-            "children")
+            self.add_relation,
+            ticket1, CHILD, ticket2
+        )
 
     def test_parent_relation_is_incompatible_with_one_way_relations(self):
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        self.relations_system.add(ticket1, ticket2, "refersto")
+        self.add_relation(ticket1, REFERS_TO, ticket2)
 
         self.assertRaises(
             ValidationError,
-            self.relations_system.add,
-            ticket1,
-            ticket2,
-            "parent")
+            self.add_relation,
+            ticket1, PARENT, ticket2
+        )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add,
-            ticket1,
-            ticket2,
-            "children")
+            self.add_relation,
+            ticket1, CHILD, ticket2
+        )
 
     def test_parent_must_be_in_same_product(self):
         ticket1 = self._insert_and_load_ticket("A1")
 
         self.assertRaises(
             ValidationError,
-            self.relations_system.add,
-            ticket1, ticket2, "parent"
+            self.add_relation,
+            ticket1, PARENT, ticket2
         )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add,
-            ticket1, ticket2, "children"
+            self.add_relation,
+            ticket1, CHILD, ticket2
         )
 
     def test_cannot_create_other_relations_between_descendants(self):
         t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, "12345")
-        self.relations_system.add(t4, t2, "parent")  #    t1 -> t2
-        self.relations_system.add(t3, t2, "parent")  #         /  \
-        self.relations_system.add(t2, t1, "parent")  #       t3    t4
+        self.add_relation(t1, PARENT, t2)   #    t1 -> t2
+        self.add_relation(t2, PARENT, t3)   #         /  \
+        self.add_relation(t2, PARENT, t4)   #       t3    t4
 
         self.assertRaises(
             ValidationError,
-            self.relations_system.add, t1, t2, "dependent"
+            self.add_relation,
+            t2, DEPENDS_ON, t1
         )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add, t2, t1, "dependent"
+            self.add_relation,
+            t1, DEPENDS_ON, t2
         )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add, t1, t4, "dependent"
+            self.add_relation,
+            t4, DEPENDS_ON, t1
         )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add, t3, t1, "dependent"
+            self.add_relation,
+            t1, DEPENDS_ON, t3
         )
         try:
-            self.relations_system.add(t1, t5, "dependent")
-            self.relations_system.add(t3, t4, "dependent")
+            self.add_relation(t1, DEPENDS_ON, t5)
+            self.add_relation(t3, DEPENDS_ON, t4)
         except ValidationError:
             self.fail("Could not add valid relation.")
 
     def test_cannot_add_parent_if_this_would_cause_invalid_relations(self):
         t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, "12345")
-        self.relations_system.add(t4, t2, "parent")  #    t1 -> t2
-        self.relations_system.add(t3, t2, "parent")  #         /  \
-        self.relations_system.add(t2, t1, "parent")  #       t3    t4    t5
-        self.relations_system.add(t2, t5, "dependent")
+        self.add_relation(t1, PARENT, t2)   #    t1 -> t2
+        self.add_relation(t2, PARENT, t3)   #         /  \
+        self.add_relation(t2, PARENT, t4)   #       t3    t4    t5
+        self.add_relation(t2, DEPENDS_ON, t5)
 
         self.assertRaises(
             ValidationError,
-            self.relations_system.add, t5, t2, "parent"
+            self.add_relation,
+            t2, PARENT, t5
         )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add, t5, t3, "parent"
+            self.add_relation,
+            t3, PARENT, t5
         )
         self.assertRaises(
             ValidationError,
-            self.relations_system.add, t1, t5, "parent"
+            self.add_relation,
+            t5, PARENT, t1,
         )
         try:
-            self.relations_system.add(t5, t1, "parent")
+            self.add_relation(t1, PARENT, t5)
         except ValidationError:
             self.fail("Could not add valid relation.")
 
     def test_cannot_close_ticket_with_open_children(self):
-        t1 = self._insert_and_load_ticket("1")                  #     t1
-        t2 = self._insert_and_load_ticket("2", status='closed') #   /  | \
-        t3 = self._insert_and_load_ticket("3")                  #  t2 t3 t4
+        t1 = self._insert_and_load_ticket("1")                    #     t1
+        t2 = self._insert_and_load_ticket("2", status='closed')   #   /  |  \
+        t3 = self._insert_and_load_ticket("3")                    #  t2 t3  t4
         t4 = self._insert_and_load_ticket("4")
-        self.relations_system.add(t2, t1, "parent")
-        self.relations_system.add(t3, t1, "parent")
-        self.relations_system.add(t4, t1, "parent")
+        self.add_relation(t1, PARENT, t2)
+        self.add_relation(t1, PARENT, t3)
+        self.add_relation(t1, PARENT, t4)
 
         # A warning is be returned for each open ticket
         self.req.args["action"] = 'resolve'
 
         self.assertRaises(
             ValidationError,
-            self.relations_system.add,
-            t1,
-            t2,
-            "duplicateof",
+            self.add_relation,
+            t1, DUPLICATE_OF, t2
         )
-        self.relations_system.add(t2, t1, "duplicateof")
+        self.add_relation(t2, DUPLICATE_OF, t1)
 
     def test_detects_blocker_cycles(self):
         t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, "12345")
-        self.relations_system.add(t1, t2, "blocks")
-        self.relations_system.add(t3, t2, "dependson")
-        self.relations_system.add(t4, t3, "blockedby")
-        self.relations_system.add(t4, t5, "dependent")
+        self.add_relation(t1, BLOCKS, t2)
+        self.add_relation(t3, DEPENDS_ON, t2)
+        self.add_relation(t4, BLOCKED_BY, t3)
+        self.add_relation(t4, DEPENDENCY_OF, t5)
 
-        self.assertRaises(ValidationError,
-                          self.relations_system.add, t2, t1, "blocks")
-        self.assertRaises(ValidationError,
-                          self.relations_system.add, t3, t1, "dependent")
-        self.assertRaises(ValidationError,
-                          self.relations_system.add, t1, t2, "blockedby")
-        self.assertRaises(ValidationError,
-                          self.relations_system.add, t1, t5, "dependson")
+        self.assertRaises(
+            ValidationError,
+            self.add_relation,
+            t2, BLOCKS, t1
+        )
+        self.assertRaises(
+            ValidationError,
+            self.add_relation,
+            t3, DEPENDENCY_OF, t1
+        )
+        self.assertRaises(
+            ValidationError,
+            self.add_relation,
+            t1, BLOCKED_BY, t2
+        )
+        self.assertRaises(
+            ValidationError,
+            self.add_relation,
+            t1, DEPENDS_ON, t5
+        )
 
-        self.relations_system.add(t1, t2, "dependent")
-        self.relations_system.add(t2, t3, "blocks")
-        self.relations_system.add(t4, t3, "dependson")
-        self.relations_system.add(t5, t4, "blockedby")
+        self.add_relation(t1, DEPENDENCY_OF, t2)
+        self.add_relation(t2, BLOCKS, t3)
+        self.add_relation(t4, DEPENDS_ON, t3)
+        self.add_relation(t5, BLOCKED_BY, t4)
 
-        self.relations_system.add(t1, t2, "refersto")
-        self.relations_system.add(t2, t1, "refersto")
+        self.add_relation(t1, REFERS_TO, t2)
+        self.add_relation(t2, REFERS_TO, t1)
 
     def test_can_find_ticket_by_id_from_same_env(self):
         """ Can find ticket given #id"""
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        relations_system = self.relations_system
         test_changing_listener = self.env[TestRelationChangingListener]
         #act
-        relations_system.add(ticket1, ticket2, "dependent")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
         #assert
         self.assertEqual("adding_relation", test_changing_listener.action)
         relation = test_changing_listener.relation
-        self.assertEqual("dependent", relation.type)
+        self.assertEqual(DEPENDS_ON, relation.type)
 
     def test_can_sent_deleting_event(self):
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        relations_system = self.relations_system
         test_changing_listener = self.env[TestRelationChangingListener]
-        relations_system.add(ticket1, ticket2, "dependent")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
         #act
-        relations = relations_system.get_relations(ticket1)
-        relation_to_delete = relations[0]
-        relations_system.delete(relation_to_delete["relation_id"])
+        relations = self.get_relations(ticket1)
+        self.delete_relation(relations[0])
         #assert
         self.assertEqual("deleting_relation", test_changing_listener.action)
         relation = test_changing_listener.relation
-        self.assertEqual("dependent", relation.type)
+        self.assertEqual(DEPENDS_ON, relation.type)
 
 
 class TicketChangeRecordUpdaterTestCase(BaseRelationsTestCase):
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        relations_system = self.relations_system
         #act
-        relations_system.add(ticket1, ticket2, "dependent")
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
         #assert
         change_log1 = Ticket(self.env, ticket1.id).get_changelog()
         self.assertEquals(1, len(change_log1))
         #arrange
         ticket1 = self._insert_and_load_ticket("A1")
         ticket2 = self._insert_and_load_ticket("A2")
-        relations_system = self.relations_system
-        relations_system.add(ticket1, ticket2, "dependent")
-        relations = relations_system.get_relations(ticket1)
+
+        self.add_relation(ticket1, DEPENDS_ON, ticket2)
+        relations = self.get_relations(ticket1)
         #act
-        relation_to_delete = relations[0]
-        relations_system.delete(relation_to_delete["relation_id"])
+        self.delete_relation(relations[0])
         #assert
         change_log1 = Ticket(self.env, ticket1.id).get_changelog()
         self.assertEquals(2, len(change_log1))
 
         if ticket_id:
             sql = """SELECT time, author, field, oldvalue, newvalue
-                    FROM ticket_change WHERE ticket=%s"""
+                     FROM ticket_change WHERE ticket=%s"""
             print "db_transaction select by ticket_id result:"
             with self.env.db_transaction:
                 for row in self.env.db_query(sql, (ticket_id, )):
 
 def suite():
     test_suite = unittest.TestSuite()
-    test_suite.addTest(unittest.makeSuite(ApiTestCase, 'test'))
-    test_suite.addTest(unittest.makeSuite(
-        RelationChangingListenerTestCase, 'test'))
-    test_suite.addTest(unittest.makeSuite(
-        TicketChangeRecordUpdaterTestCase, 'test'))
+    test_suite.addTest(unittest.makeSuite(ApiTestCase))
+    test_suite.addTest(unittest.makeSuite(RelationChangingListenerTestCase))
+    test_suite.addTest(unittest.makeSuite(TicketChangeRecordUpdaterTestCase))
     return test_suite
 
 

bloodhound_relations/bhrelations/tests/base.py

 #  under the License.
 
 from tests.env import MultiproductTestCase
-from multiproduct.env import ProductEnvironment
-from bhrelations.api import RelationsSystem, EnvironmentSetup, \
-    RELATIONS_CONFIG_NAME
 from trac.test import EnvironmentStub, Mock, MockPerm
 from trac.ticket import Ticket
 from trac.util.datefmt import utc
 
+from multiproduct.env import ProductEnvironment
+
+from bhrelations.api import EnvironmentSetup, RelationsSystem, \
+                            RELATIONS_CONFIG_NAME
+
 try:
     from babel import Locale
-
     locale_en = Locale.parse('en_US')
 except ImportError:
     locale_en = None
 
 
+PARENT = "parent"
+CHILD = "child"
+REFERS_TO = "refersto"
+DEPENDS_ON = "dependson"
+DEPENDENCY_OF = "dependent"
+DUPLICATE_OF = "duplicateof"
+DUPLICATED_BY = "duplicatedby"
+BLOCKED_BY = "blockedby"
+BLOCKS = "blocks"
+MULTIPRODUCT_REL = "mprel"
+MULTIPRODUCT_BACKREL = "mpbackrel"
+
+
 class BaseRelationsTestCase(MultiproductTestCase):
     def setUp(self, enabled=()):
         env = EnvironmentStub(
         env.config.set('bhrelations', 'duplicate_relation',
                        'duplicateof')
         config_name = RELATIONS_CONFIG_NAME
-        env.config.set(config_name, 'dependency', 'dependson,dependent')
+        env.config.set(config_name, 'dependency',
+                       ','.join([DEPENDS_ON, DEPENDENCY_OF]))
         env.config.set(config_name, 'dependency.validators',
                        'NoCycles,SingleProduct')
         env.config.set(config_name, 'dependson.blocks', 'true')
-        env.config.set(config_name, 'parent_children', 'parent,children')
+        env.config.set(config_name, 'parent_children',
+                       ','.join([PARENT, CHILD]))
         env.config.set(config_name, 'parent_children.validators',
                        'OneToMany,SingleProduct,NoCycles')
         env.config.set(config_name, 'children.label', 'Overridden')
         env.config.set(config_name, 'parent.copy_fields',
                        'summary, foo')
         env.config.set(config_name, 'parent.exclusive', 'true')
-        env.config.set(config_name, 'multiproduct_relation', 'mprel,mpbackrel')
-        env.config.set(config_name, 'oneway', 'refersto')
-        env.config.set(config_name, 'duplicate', 'duplicateof,duplicatedby')
+        env.config.set(config_name, 'multiproduct_relation',
+                       ','.join([MULTIPRODUCT_REL, MULTIPRODUCT_BACKREL]))
+        env.config.set(config_name, 'oneway', REFERS_TO)
+        env.config.set(config_name, 'duplicate',
+                       ','.join([DUPLICATE_OF, DUPLICATED_BY]))
         env.config.set(config_name, 'duplicate.validators', 'ReferencesOlder')
         env.config.set(config_name, 'duplicateof.label', 'is a duplicate of')
         env.config.set(config_name, 'duplicatedby.label', 'duplicates')
-        env.config.set(config_name, 'blocker', 'blockedby,blocks')
+        env.config.set(config_name, 'blocker', ','.join([BLOCKED_BY, BLOCKS]))
         env.config.set(config_name, 'blockedby.blocks', 'true')
 
         self.global_env = env
 
     def _insert_and_load_ticket_with_env(self, env, summary, **kw):
         return Ticket(env, self._insert_ticket(env, summary, **kw))
+
+    def add_relation(self, source, reltype, destination, *args, **kwargs):
+        return self.relations_system.add(source, destination, reltype,
+                                         *args, **kwargs)
+
+    def get_relations(self, ticket):
+        return self.relations_system.get_relations(ticket)
+
+    def delete_relation(self, relation):
+        self.relations_system.delete(relation["relation_id"])

bloodhound_relations/bhrelations/tests/mocks.py

 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
+from trac.core import Component, implements
+
 from bhrelations.api import IRelationChangingListener
-from trac.core import Component, implements
 
 
 class TestRelationChangingListener(Component):

bloodhound_relations/bhrelations/tests/notification.py

 #  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.tests.base import BaseRelationsTestCase
+from trac.ticket.tests.notification import SMTP_TEST_PORT, smtp_address
+
+from bhrelations.tests.base import DEPENDENCY_OF, BaseRelationsTestCase
 from bhrelations.notification import RelationNotifyEmail
 
 
 class NotificationTestCase(BaseRelationsTestCase):
+
     @classmethod
     def setUpClass(cls):
         cls.smtpd = CustomSMTPThreadedServer(SMTP_TEST_PORT)
         """To/Cc recipients"""
         ticket = self._insert_and_load_ticket(
             'Foo',
-            reporter= '"Joe User" < joe.user@example.org >',
+            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'
             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")
+        relation = self.add_relation(ticket, DEPENDENCY_OF, ticket2)
 
         self.notifier.notify(relation)
 
         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")
+        relation = self.add_relation(ticket, DEPENDENCY_OF, ticket2)
         self.notifier.notify(relation)
 
         sender = self.smtpd.get_sender()
         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")
+        relation = self.add_relation(ticket, DEPENDENCY_OF, ticket2)
         self.notifier.notify(relation)
 
         relations = self.env.db_direct_query(
 
 def suite():
     test_suite = unittest.TestSuite()
-    test_suite.addTest(unittest.makeSuite(NotificationTestCase, 'test'))
+    test_suite.addTest(unittest.makeSuite(NotificationTestCase))
     return test_suite
 
+
 if __name__ == '__main__':
     unittest.main()

bloodhound_relations/bhrelations/tests/search.py

 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
 import shutil
 import tempfile
 import unittest
 
 # TODO: Figure how to get trac to load components from these modules
 import bhsearch.query_parser, bhsearch.search_resources.ticket_search, \
-    bhsearch.whoosh_backend
+       bhsearch.whoosh_backend
 import bhrelations.search
-from bhrelations.tests.base import BaseRelationsTestCase
+from bhrelations.tests.base import BaseRelationsTestCase, DEPENDENCY_OF
 
 
 class SearchIntegrationTestCase(BaseRelationsTestCase):
+
     def setUp(self):
         BaseRelationsTestCase.setUp(self, enabled=['bhsearch.*'])
         self.global_env.path = tempfile.mkdtemp('bhrelations-tempenv')
         t1 = self._insert_and_load_ticket("Foo")
         t2 = self._insert_and_load_ticket("Bar")
 
-        self.relations_system.add(t1, t2, 'dependent')
+        self.add_relation(t1, DEPENDENCY_OF, t2)
 
-        result = self.search_api.query('dependent:#2')
+        result = self.search_api.query('%s:#2' % DEPENDENCY_OF)
         self.assertEqual(result.hits, 1)
 
     def test_relations_are_indexed_on_deletion(self):
         t1 = self._insert_and_load_ticket("Foo")
         t2 = self._insert_and_load_ticket("Bar")
 
-        self.relations_system.add(t1, t2, 'dependent')
-        relations = self.relations_system.get_relations(t1)
+        self.add_relation(t1, DEPENDENCY_OF, t2)
+        relations = self.get_relations(t1)
         self.relations_system.delete(relations[0]["relation_id"])
 
-        result = self.search_api.query('dependent:#2')
+        result = self.search_api.query('%s:#2' % DEPENDENCY_OF)
         self.assertEqual(result.hits, 0)
 
     def test_different_types_of_queries(self):
         t1 = self._insert_and_load_ticket("Foo")
         t2 = self._insert_and_load_ticket("Bar")
 
-        self.relations_system.add(t1, t2, 'dependent')
+        self.add_relation(t1, DEPENDENCY_OF, t2)
 
-        self.assertEqual(self.search_api.query('dependent:#2').hits, 1)
-        self.assertEqual(self.search_api.query('dependent:#tp1-2').hits, 1)
+        self.assertEqual(self.search_api.query('%s:#2'
+                                               % DEPENDENCY_OF).hits, 1)
+        self.assertEqual(self.search_api.query('%s:#tp1-2'
+                                               % DEPENDENCY_OF).hits, 1)
 
 
 def suite():
     test_suite = unittest.TestSuite()
-    test_suite.addTest(unittest.makeSuite(SearchIntegrationTestCase, 'test'))
+    test_suite.addTest(unittest.makeSuite(SearchIntegrationTestCase))
     return test_suite
 
+
 if __name__ == '__main__':
     unittest.main()

bloodhound_relations/bhrelations/tests/validation.py

 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
 import unittest
 
+from bhrelations.tests.base import BaseRelationsTestCase
 from bhrelations.validation import Validator
-from bhrelations.tests.base import BaseRelationsTestCase
 
 
 class GraphFunctionsTestCase(BaseRelationsTestCase):
         self.validator = Validator(self.env)
 
     def test_find_path(self):
-        self.assertEqual(
-            self.validator._find_path(u'A', u'E', u'p'),
-            [u'A', u'C', u'E'])
-        self.assertEqual(
-            self.validator._find_path(u'A', u'G', u'p'),
-            [u'A', u'C', u'E', u'F', u'G'])
-        self.assertEqual(
-            self.validator._find_path(u'H', u'D', u'p'),
-            [u'H', u'C', u'D'])
-        self.assertEqual(
-            self.validator._find_path(u'E', u'A', u'p'),
-            None)
-        self.assertEqual(
-            self.validator._find_path(u'B', u'D', u'p'),
-            None)
+        self.assertEqual(self.validator._find_path(u'A', u'E', u'p'),
+                         [u'A', u'C', u'E'])
+        self.assertEqual(self.validator._find_path(u'A', u'G', u'p'),
+                         [u'A', u'C', u'E', u'F', u'G'])
+        self.assertEqual(self.validator._find_path(u'H', u'D', u'p'),
+                         [u'H', u'C', u'D'])
+        self.assertEqual(self.validator._find_path(u'E', u'A', u'p'), None)
+        self.assertEqual(self.validator._find_path(u'B', u'D', u'p'), None)
 
     def test_descendants(self):
-        self.assertEqual(
-            self.validator._descendants(u'B', u'p'),
-            set()
-        )
-        self.assertEqual(
-            self.validator._descendants(u'E', u'p'),
-            set([u'F', u'G'])
-        )
-        self.assertEqual(
-            self.validator._descendants(u'H', u'p'),
-            set([u'C', u'D', u'E', u'F', u'G'])
-        )
+        self.assertEqual(self.validator._descendants(u'B', u'p'), set())
+        self.assertEqual(self.validator._descendants(u'E', u'p'),
+                         set([u'F', u'G']))
+        self.assertEqual(self.validator._descendants(u'H', u'p'),
+                         set([u'C', u'D', u'E', u'F', u'G']))
 
     def test_ancestors(self):
-        self.assertEqual(
-            self.validator._ancestors(u'B', u'p'),
-            set([u'A'])
-        )
-        self.assertEqual(
-            self.validator._ancestors(u'E', u'p'),
-            set([u'A', u'C', u'H'])
-        )
-        self.assertEqual(
-            self.validator._ancestors(u'H', u'p'),
-            set()
-        )
+        self.assertEqual(self.validator._ancestors(u'B', u'p'), set([u'A']))
+        self.assertEqual(self.validator._ancestors(u'E', u'p'),
+                         set([u'A', u'C', u'H']))
+        self.assertEqual(self.validator._ancestors(u'H', u'p'), set())
 
 
 def suite():
     test_suite = unittest.TestSuite()
-    test_suite.addTest(unittest.makeSuite(GraphFunctionsTestCase, 'test'))
+    test_suite.addTest(unittest.makeSuite(GraphFunctionsTestCase))
     return test_suite
 
+
 if __name__ == '__main__':
     unittest.main()

bloodhound_relations/bhrelations/tests/web_ui.py

 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
 import unittest
-from bhrelations.api import ResourceIdSerializer
-from bhrelations.web_ui import RelationManagementModule
-from bhrelations.tests.base import BaseRelationsTestCase
 
-from multiproduct.ticket.web_ui import TicketModule
 from trac.ticket import Ticket
 from trac.util.datefmt import to_utimestamp
 from trac.web import RequestDone
 
+from bhrelations.api import ResourceIdSerializer
+from bhrelations.tests.base import DEPENDS_ON, DUPLICATE_OF, \
+                                   BaseRelationsTestCase
+from bhrelations.web_ui import RelationManagementModule
+
+from multiproduct.ticket.web_ui import TicketModule
+
 
 class RelationManagementModuleTestCase(BaseRelationsTestCase):
+
     def setUp(self):
         BaseRelationsTestCase.setUp(self)
         ticket_id = self._insert_ticket(self.env, "Foo")
         t2 = self._insert_ticket(self.env, "Bar")
         self.req.args['add'] = True
         self.req.args['dest_tid'] = str(t2)
-        self.req.args['reltype'] = 'dependson'
+        self.req.args['reltype'] = DEPENDS_ON