Commits

Andrej Golcov  committed 2dcbb7c Draft

adding draft code for bhrelations - towards bep-0006. There are a few missing parts in bhrelations source.

git-svn-id: https://svn.apache.org/repos/asf/bloodhound/trunk@146888213f79535-47bb-0310-9956-ffa450edef68

  • Participants
  • Parent commits 8f8fd74

Comments (0)

Files changed (14)

File bloodhound_relations/README

   - [http:trac.edgewall.org Trac]  ,,Since version 
     ''' 1.0.1 ''',, .
 
-
 == Configuration ==
 
 In order to enable [wiki:/En/Devel/BloodhoundTicketRelationsPlugin BloodhoundTicketRelationsPlugin] plugin,
 [query:status=new|assigned|reopened&keywords=~bep-0006 here].
 If you have any issues, please create a [/newticket?keywords=bep-0006 new ticket].
 
+== 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).
+The plugin trac_ticket_links directory represents an updated copy of the combined vendor branch
+located on https://svn.apache.org/repos/asf/bloodhound/vendor/trac-ticket-links.
+The combined vendor branch represents a source tree merged from the several original
+vendor branches. For more information on the original vendor branches, see //svn.apache.org/repos/asf/bloodhound/vendor/README

File bloodhound_relations/bhrelations/api.py

 #  specific language governing permissions and limitations
 #  under the License.
 
+

File bloodhound_relations/bhrelations/tests/bhrelations_links.py

+#!/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.
+
+"""
+    This module enables running of original ticket-links unit-test in
+    bhrelations environment.
+"""
+from trac_ticket_links.ticket.tests.links import TicketTestCase
+
+class BloodhoundRelationsTicketTestCase(TicketTestCase):
+
+    def setUp(self):
+        super(BloodhoundRelationsTicketTestCase, self).setUp()
+#        self.env.enable.append = 'trac_ticket_links.*'

File bloodhound_relations/bhrelations/trac/ticket/__init__.py

-#!/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.
-

File bloodhound_relations/bhrelations/trac/ticket/links.py

-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2010 Edgewall Software
-# All rights reserved.
-#
-# This software is licensed as described in the file COPYING, which
-# you should have received as part of this distribution. The terms
-# are also available at http://trac.edgewall.org/wiki/TracLicense.
-#
-# This software consists of voluntary contributions made by many
-# individuals. For the exact contribution history, see the revision
-# history and logs, available at http://trac.edgewall.org/log/.
-#
-# Author: Joachim Hoessler <hoessler@gmail.com>
-
-from trac.resource import ResourceNotFound
-from trac.ticket.api import (ITicketLinkController, ITicketManipulator,
-                             TicketSystem)
-from trac.ticket.model import Ticket
-from copy import copy
-from trac.core import Component, implements
-from trac.util import unique
-
-class LinksProvider(Component):
-    """Link controller that provides links as specified in the [ticket-links]
-    section in the trac.ini configuration file.
-    """
-    
-    implements(ITicketLinkController, ITicketManipulator)
-    
-    PARENT_END = 'parent'
-    
-    def __init__(self):
-        self._links, self._labels, \
-        self._validators, self._blockers, \
-        self._copy_fields = self._get_links_config()
-
-    def get_ends(self):
-        return self._links
-    
-    def render_end(self, end):
-        return self._labels[end]
-    
-    def is_blocker(self, end):
-        return self._blockers[end]
-    
-    def get_copy_fields(self, end):
-        if end in self._copy_fields:
-            return self._copy_fields[end]
-        else:
-            return TicketSystem(self.env).default_copy_fields
-    
-    def get_validator(self, end):
-        return self._validators.get(end)
-        
-    def prepare_ticket(self, req, ticket, fields, actions):
-        pass
-        
-    def validate_ticket(self, req, ticket):
-        action = req.args.get('action')
-        ticket_system = TicketSystem(self.env)
-        
-        for end in ticket_system.link_ends_map:
-            check = self.validate_links_exist(ticket, end)
-            if check:
-                yield None, check
-                continue
-            
-            validator_name = self.get_validator(end)
-            if validator_name == 'no_cycle':
-                validator = self.validate_no_cycle
-            elif validator_name == 'parent_child' and end == self.PARENT_END:
-                validator = self.validate_parent
-            else:
-                validator = self.validate_any
-            
-            check = validator(ticket, end)
-            if check:
-                yield None, check
-            
-            if action == 'resolve' and self.is_blocker(end):
-                blockers = self.find_blockers(ticket, end, [])
-                if blockers:
-                    blockers_str = ', '.join('#%s' % x 
-                                             for x in unique(blockers))
-                    msg = ("Cannot resolve this ticket because it is "
-                           "blocked by '%s' tickets [%s]" 
-                           % (end,  blockers_str))
-                    yield None, msg
-    
-    def validate_links_exist(self, ticket, end):
-        ticket_system = TicketSystem(self.env)
-        links = ticket_system.parse_links(ticket[end])
-        bad_links = []
-        for link in links:
-            try:
-                tkt = Ticket(self.env, link)
-            except ResourceNotFound:
-                bad_links.append(link)
-        if bad_links:
-            return ("Tickets linked in '%s' do not exist: [%s]" 
-                    % (end, ', '.join('#%s' % link for link in bad_links)))
-          
-    def validate_no_cycle(self, ticket, end):
-        cycle = self.find_cycle(ticket, end, [])
-        if cycle != None:
-            cycle_str = ['#%s'%id for id in cycle]
-            return 'Cycle in ''%s'': %s' % (self.render_end(end),
-                                            ' -> '.join(cycle_str))
-
-    def validate_parent(self, ticket, end):
-        cycle_validation = self.validate_no_cycle(ticket, end)
-        if cycle_validation: 
-            return cycle_validation
-        
-        ticket_system = TicketSystem(self.env)
-        links = ticket_system.parse_links(ticket[end])
-        
-        multiple_parents = (end == self.PARENT_END and len(links) > 1)
-        if multiple_parents:
-            parents_str = ', '.join('#%s' % id for id in links)
-            return "Multiple links in '%s': #%s -> [%s]" \
-                   % (self.render_end(end), ticket.id, parents_str)
-    
-    def validate_any(self, ticket, end):
-        return None
-    
-    def _get_links_config(self):
-        links = []
-        labels = {}
-        validators = {}
-        blockers = {}
-        copy_fields = {}
-        
-        config = self.config['ticket-links']
-        for name in [option for option, _ in config.options()
-                     if '.' not in option]:
-            ends = [e.strip() for e in config.get(name).split(',')]
-            if not ends:
-                continue
-            end1 = ends[0]
-            end2 = None
-            if len(ends) > 1:
-                end2 = ends[1]
-            links.append((end1, end2))
-            
-            label1 = config.get(end1 + '.label') or end1.capitalize()
-            labels[end1] = label1
-            if end2:
-                label2 = config.get(end2 + '.label') or end2.capitalize()
-                labels[end2] = label2
-            
-            validator = config.get(name + '.validator')
-            if validator:
-                validators[end1] = validator
-                if end2:
-                    validators[end2] = validator
-                
-            blockers[end1] = config.getbool(end1 + '.blocks', default=False)
-            if end2:
-                blockers[end2] = config.getbool(end2 + '.blocks', default=False)
-            
-            # <end>.copy_fields may be absent or intentionally set empty.
-            # config.getlist() will return [] in either case, so check that
-            # the key is present before assigning the value
-            for end in [end1, end2]:
-                if end:
-                    cf_key = '%s.copy_fields' % end
-                    if cf_key in config:
-                        copy_fields[end] = config.getlist(cf_key)
-            
-        return links, labels, validators, blockers, copy_fields
-    
-    def find_blockers(self, ticket, field, blockers):
-        ticket_system = TicketSystem(self.env)
-        links = ticket_system.parse_links(ticket[field])
-        for link in links:
-            linked_ticket = Ticket(self.env, link)
-            if linked_ticket['status'] != 'closed':
-                blockers.append(link)
-            else:
-                self.find_blockers(linked_ticket, field, blockers)
-        return blockers
-        
-    def find_cycle(self, ticket, field, path):
-        if ticket.id in path:
-            path.append(ticket.id)
-            return path
-
-        path.append(ticket.id)
-
-        ticket_system = TicketSystem(self.env)
-        links = ticket_system.parse_links(ticket[field])
-        for link in links:
-            linked_ticket= Ticket(self.env, link)
-            cycle = self.find_cycle(linked_ticket, field, copy(path))
-            if  cycle != None:
-                return cycle
-        return None
-                

File bloodhound_relations/bhrelations/trac/ticket/tests/__init__.py

-#!/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.
-

File bloodhound_relations/bhrelations/trac/ticket/tests/links.py

-from trac.ticket.model import Ticket
-from trac.test import EnvironmentStub, Mock
-from trac.ticket.links import LinksProvider
-from trac.ticket.api import TicketSystem
-from trac.ticket.query import Query
-from trac.util.datefmt import utc
-from copy import copy
-import unittest
-
-class TicketTestCase(unittest.TestCase):
-    def setUp(self):
-        self.env = EnvironmentStub(default_data=True)
-        self.env.config.set('ticket-links', 'dependency', 'dependson,dependent')
-        self.env.config.set('ticket-links', 'dependency.validator', 'no_cycle')
-        self.env.config.set('ticket-links', 'parent_children', 
-                                                            'parent,children')
-        self.env.config.set('ticket-links', 'parent_children.validator',
-                                                            'parent_child')
-        self.env.config.set('ticket-links', 'children.blocks', 'true')
-        self.env.config.set('ticket-links', 'children.label', 'Overridden')
-        self.env.config.set('ticket-links', 'parent.copy_fields',
-                                                            'summary, foo')
-        self.env.config.set('ticket-links', 'oneway', 'refersto')
-        self.req = Mock(href=self.env.href, authname='anonymous', tz=utc,
-                        args=dict(action='dummy'))
-
-    def _insert_ticket(self, summary, **kw):
-        """Helper for inserting a ticket into the database"""
-        ticket = Ticket(self.env)
-        for k,v in kw.items():
-            ticket[k] = v
-        return ticket.insert()
-
-    def _create_a_ticket(self):
-        ticket = Ticket(self.env)
-        ticket['reporter'] = 'santa'
-        ticket['summary'] = 'Foo'
-        ticket['foo'] = 'This is a custom field'
-        return ticket
-
-    # TicketSystem tests
-    
-    def test_get_ends(self):
-        links_provider = LinksProvider(self.env)
-        self.assertEquals(set(links_provider.get_ends()),
-                          set([('dependson', 'dependent'), 
-                               ('parent', 'children'), ('refersto', None)])
-                          )
-    
-    def test_render_end(self):
-        links_provider = LinksProvider(self.env)
-        self.assertEquals(links_provider.render_end('refersto'), 'Refersto')
-        self.assertEquals(links_provider.render_end('children'), 'Overridden')
-    
-    def test_is_blocker(self):
-        links_provider = LinksProvider(self.env)
-        self.assertFalse(links_provider.is_blocker('parent'))
-        self.assertTrue(links_provider.is_blocker('children'))
-        
-    def test_link_ends_map(self):
-        ticket_system = TicketSystem(self.env)
-        self.assertEquals(ticket_system.link_ends_map,
-                          {'dependson': 'dependent', 'dependent': 'dependson',
-                           'parent': 'children', 'children': 'parent',
-                           'refersto': None})
-    
-    def test_parse_links(self):
-        ticket_system = TicketSystem(self.env)
-        self.assertEquals([1, 2, 42], ticket_system.parse_links('1 2 42'))
-        self.assertEquals([1, 2, 42], ticket_system.parse_links('#1 #2 #42'))
-        self.assertEquals([1, 2, 42], ticket_system.parse_links('1, 2, 42'))
-        self.assertEquals([1, 2, 42], ticket_system.parse_links('#1, #2, #42'))
-        self.assertEquals([1, 2, 42], ticket_system.parse_links('#1 #2 #42 #1'))
-        
-    def test_get_ticket_fields(self):
-        ticket_system = TicketSystem(self.env)
-        fields = ticket_system.get_ticket_fields()
-        link_fields = [f['name'] for f in fields if f.get('link')]
-        self.assertEquals(5, len(link_fields))
-
-    def test_update_links(self):
-        ticket = self._create_a_ticket()
-        ticket.insert()
-        ticket = self._create_a_ticket()
-        ticket['dependson'] = '#1, #2'
-        ticket.insert()
-
-        # Check if ticket link in #1 has been updated
-        ticket = Ticket(self.env, 1)
-        self.assertEqual(1, ticket.id)
-        self.assertEqual('#2', ticket['dependent'])
-        
-        # Remove link from #2 to #1
-        ticket = Ticket(self.env, 2)
-        ticket['dependson'] = '#2'
-        ticket.save_changes("me", "testing")
-
-        # Check if ticket link in #1 has been updated
-        ticket = Ticket(self.env, 1)
-        self.assertEqual(1, ticket.id)
-        self.assertEqual('', ticket['dependent'])
-        
-    def test_populate_from_linked_field(self):
-        ticket = self._create_a_ticket()
-        ticket.insert()
-        ticket = Ticket(self.env)
-        ticket.populate_from(1, link_field_name='children')
-        self.assertEqual('Foo', ticket['summary'])
-        self.assertEqual('#1', ticket['children'])
-        
-    def test_save_retrieve_links(self):
-        ticket = self._create_a_ticket()
-        ticket.insert()
-        ticket = self._create_a_ticket()
-        ticket['dependson'] = '#1, #2'
-        ticket.insert()
-
-        # Check if ticket link in #1 has been updated
-        ticket = Ticket(self.env, 1)
-        self.assertEqual(1, ticket.id)
-        self.assertEqual('#2', ticket['dependent'])
-
-    def test_query_by_result(self):
-        ticket = self._create_a_ticket()
-        ticket.insert()
-        ticket = self._create_a_ticket()
-        ticket['dependson'] = '#1'
-        ticket.insert()
-        query = Query.from_string(self.env, 'dependson=1', order='id')
-        sql, args = query.get_sql()
-        tickets = query.execute(self.req)
-        self.assertEqual(len(tickets), 1)
-        self.assertEqual(tickets[0]['id'], 2)
-    
-    def test_query_by_result2(self):
-        ticket = self._create_a_ticket()
-        ticket.insert()
-        ticket = self._create_a_ticket()
-        ticket['dependson'] = '#1'
-        ticket.insert()
-        query = Query.from_string(self.env, 'dependent=2', order='id')
-        sql, args = query.get_sql()
-        tickets = query.execute(self.req)
-        self.assertEqual(len(tickets), 1)
-        self.assertEqual(tickets[0]['id'], 1)
-    
-    def test_validator_links_exists(self):
-        ticket1 = self._create_a_ticket()
-        ticket1.insert()
-        ticket2 = self._create_a_ticket()
-        ticket2['dependson'] = '#1'
-        links_provider = LinksProvider(self.env)
-        issues = links_provider.validate_ticket(self.req, ticket2)
-        self.assertEquals(sum(1 for _ in issues), 0)
-        ticket2['dependson'] = '#404'
-        issues = links_provider.validate_ticket(self.req, ticket2)
-        self.assertEquals(sum(1 for _ in issues), 1)
-        
-    def test_validator_no_cyle(self):
-        ticket1 = self._create_a_ticket()
-        ticket1.insert()
-        ticket2 = self._create_a_ticket()
-        ticket2['dependson'] = '#1'
-        links_provider = LinksProvider(self.env)
-        issues = links_provider.validate_ticket(self.req, ticket2)
-        self.assertEquals(sum(1 for _ in issues), 0)
-        ticket2.insert()
-        ticket1['dependson'] = '#2'
-        issues = links_provider.validate_ticket(self.req, ticket1)
-        self.assertEquals(sum(1 for _ in issues), 1)
-
-    def test_validator_parent_child(self):
-        ticket1 = self._create_a_ticket()
-        ticket1.insert()
-        ticket2 = self._create_a_ticket()
-        ticket2['parent'] = '#1'
-        links_provider = LinksProvider(self.env)
-        issues = links_provider.validate_ticket(self.req, ticket2)
-        self.assertEquals(sum(1 for _ in issues), 0)
-        ticket2.insert()
-        ticket1['parent'] = '#2'
-        issues = links_provider.validate_ticket(self.req, ticket1)
-        self.assertEquals(sum(1 for _ in issues), 1)
-        ticket1['parent'] = ''
-        ticket3 = self._create_a_ticket()
-        ticket3['parent'] = '#1, #2'
-        issues = links_provider.validate_ticket(self.req, ticket3)
-        self.assertEquals(sum(1 for _ in issues), 1)
-    
-    def test_validator_blocker(self):
-        ticket1 = self._create_a_ticket()
-        ticket1['status'] = 'new'
-        ticket1.insert()
-        ticket2 = self._create_a_ticket()
-        ticket2['children'] = '#1'
-        links_provider = LinksProvider(self.env)
-        self.assertEquals([1], links_provider.find_blockers(ticket2, 'children',
-                                                                     []))
-        req = copy(self.req) 
-        req.args['action'] = 'resolve'
-        issues = links_provider.validate_ticket(req, ticket2)
-        self.assertEquals(sum(1 for _ in issues), 1)
-        
-
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(TicketTestCase, 'test'))
-    return suite
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='suite')

File bloodhound_relations/trac_ticket_links/__init__.py

+#!/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.
+

File bloodhound_relations/trac_ticket_links/ticket/__init__.py

+#!/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.
+

File bloodhound_relations/trac_ticket_links/ticket/links.py

+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+#
+# Author: Joachim Hoessler <hoessler@gmail.com>
+from trac.resource import ResourceNotFound
+from trac.ticket.api import (ITicketManipulator)
+from trac.ticket.model import Ticket
+from trac.config import ListOption
+from copy import copy
+from trac.core import Component, implements
+from bhrelations.ticket_links_other import (ITicketLinkController, unique,
+                                            TicketLinksSystem)
+
+class LinksProvider(Component):
+    """Link controller that provides links as specified in the [ticket-links]
+    section in the trac.ini configuration file.
+    """
+
+    implements(ITicketLinkController, ITicketManipulator)
+    
+    PARENT_END = 'parent'
+    
+    def __init__(self):
+        self._links, self._labels, \
+        self._validators, self._blockers, \
+        self._copy_fields = self._get_links_config()
+
+    def get_ends(self):
+        return self._links
+    
+    def render_end(self, end):
+        return self._labels[end]
+    
+    def is_blocker(self, end):
+        return self._blockers[end]
+    
+    def get_copy_fields(self, end):
+        if end in self._copy_fields:
+            return self._copy_fields[end]
+        else:
+            return TicketLinksSystem(self.env).default_copy_fields
+    
+    def get_validator(self, end):
+        return self._validators.get(end)
+        
+    def prepare_ticket(self, req, ticket, fields, actions):
+        pass
+        
+    def validate_ticket(self, req, ticket):
+        action = req.args.get('action')
+        ticket_system = TicketLinksSystem(self.env)
+        
+        for end in ticket_system.link_ends_map:
+            check = self.validate_links_exist(ticket, end)
+            if check:
+                yield None, check
+                continue
+            
+            validator_name = self.get_validator(end)
+            if validator_name == 'no_cycle':
+                validator = self.validate_no_cycle
+            elif validator_name == 'parent_child' and end == self.PARENT_END:
+                validator = self.validate_parent
+            else:
+                validator = self.validate_any
+            
+            check = validator(ticket, end)
+            if check:
+                yield None, check
+            
+            if action == 'resolve' and self.is_blocker(end):
+                blockers = self.find_blockers(ticket, end, [])
+                if blockers:
+                    blockers_str = ', '.join('#%s' % x 
+                                             for x in unique(blockers))
+                    msg = ("Cannot resolve this ticket because it is "
+                           "blocked by '%s' tickets [%s]" 
+                           % (end,  blockers_str))
+                    yield None, msg
+    
+    def validate_links_exist(self, ticket, end):
+        ticket_system = TicketLinksSystem(self.env)
+        links = ticket_system.parse_links(ticket[end])
+        bad_links = []
+        for link in links:
+            try:
+                tkt = Ticket(self.env, link)
+            except ResourceNotFound:
+                bad_links.append(link)
+        if bad_links:
+            return ("Tickets linked in '%s' do not exist: [%s]" 
+                    % (end, ', '.join('#%s' % link for link in bad_links)))
+          
+    def validate_no_cycle(self, ticket, end):
+        cycle = self.find_cycle(ticket, end, [])
+        if cycle != None:
+            cycle_str = ['#%s'%id for id in cycle]
+            return 'Cycle in ''%s'': %s' % (self.render_end(end),
+                                            ' -> '.join(cycle_str))
+
+    def validate_parent(self, ticket, end):
+        cycle_validation = self.validate_no_cycle(ticket, end)
+        if cycle_validation: 
+            return cycle_validation
+        
+        ticket_system = TicketLinksSystem(self.env)
+        links = ticket_system.parse_links(ticket[end])
+        
+        multiple_parents = (end == self.PARENT_END and len(links) > 1)
+        if multiple_parents:
+            parents_str = ', '.join('#%s' % id for id in links)
+            return "Multiple links in '%s': #%s -> [%s]" \
+                   % (self.render_end(end), ticket.id, parents_str)
+    
+    def validate_any(self, ticket, end):
+        return None
+    
+    def _get_links_config(self):
+        links = []
+        labels = {}
+        validators = {}
+        blockers = {}
+        copy_fields = {}
+        
+        config = self.config['ticket-links']
+        for name in [option for option, _ in config.options()
+                     if '.' not in option]:
+            ends = [e.strip() for e in config.get(name).split(',')]
+            if not ends:
+                continue
+            end1 = ends[0]
+            end2 = None
+            if len(ends) > 1:
+                end2 = ends[1]
+            links.append((end1, end2))
+            
+            label1 = config.get(end1 + '.label') or end1.capitalize()
+            labels[end1] = label1
+            if end2:
+                label2 = config.get(end2 + '.label') or end2.capitalize()
+                labels[end2] = label2
+            
+            validator = config.get(name + '.validator')
+            if validator:
+                validators[end1] = validator
+                if end2:
+                    validators[end2] = validator
+                
+            blockers[end1] = config.getbool(end1 + '.blocks', default=False)
+            if end2:
+                blockers[end2] = config.getbool(end2 + '.blocks', default=False)
+            
+            # <end>.copy_fields may be absent or intentionally set empty.
+            # config.getlist() will return [] in either case, so check that
+            # the key is present before assigning the value
+            for end in [end1, end2]:
+                if end:
+                    cf_key = '%s.copy_fields' % end
+                    if cf_key in config:
+                        copy_fields[end] = config.getlist(cf_key)
+            
+        return links, labels, validators, blockers, copy_fields
+    
+    def find_blockers(self, ticket, field, blockers):
+        ticket_system = TicketLinksSystem(self.env)
+        links = ticket_system.parse_links(ticket[field])
+        for link in links:
+            linked_ticket = Ticket(self.env, link)
+            if linked_ticket['status'] != 'closed':
+                blockers.append(link)
+            else:
+                self.find_blockers(linked_ticket, field, blockers)
+        return blockers
+        
+    def find_cycle(self, ticket, field, path):
+        if ticket.id in path:
+            path.append(ticket.id)
+            return path
+
+        path.append(ticket.id)
+
+        ticket_system = TicketLinksSystem(self.env)
+        links = ticket_system.parse_links(ticket[field])
+        for link in links:
+            linked_ticket= Ticket(self.env, link)
+            cycle = self.find_cycle(linked_ticket, field, copy(path))
+            if  cycle != None:
+                return cycle
+        return None
+                

File bloodhound_relations/trac_ticket_links/ticket/tests/__init__.py

+#!/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.
+

File bloodhound_relations/trac_ticket_links/ticket/tests/links.py

+from bhrelations.ticket_links_other import TicketLinksSystem, TicketLinksModel
+from trac.ticket.model import Ticket
+from trac.test import EnvironmentStub, Mock
+from trac_ticket_links.ticket.links import LinksProvider
+from trac.ticket.api import TicketSystem
+from trac.ticket.query import Query
+from trac.util.datefmt import utc
+from copy import copy
+import unittest
+
+class TicketTestCase(unittest.TestCase):
+    def setUp(self):
+        self.env = EnvironmentStub(
+            default_data=True,
+            enable=['trac_ticket_links.*', 'bhrelations.*']
+        )
+        self.env.config.set('ticket-links', 'dependency', 'dependson,dependent')
+        self.env.config.set('ticket-links', 'dependency.validator', 'no_cycle')
+        self.env.config.set('ticket-links', 'parent_children',
+                                                            'parent,children')
+        self.env.config.set('ticket-links', 'parent_children.validator',
+                                                            'parent_child')
+        self.env.config.set('ticket-links', 'children.blocks', 'true')
+        self.env.config.set('ticket-links', 'children.label', 'Overridden')
+        self.env.config.set('ticket-links', 'parent.copy_fields',
+                                                            'summary, foo')
+        self.env.config.set('ticket-links', 'oneway', 'refersto')
+        self.req = Mock(href=self.env.href, authname='anonymous', tz=utc,
+                        args=dict(action='dummy'))
+        self.ticket_links_system = TicketLinksSystem(self.env)
+        self.ticket_links_model = TicketLinksModel(self.env)
+
+    def _insert_ticket(self, summary, **kw):
+        """Helper for inserting a ticket into the database"""
+        ticket = Ticket(self.env)
+        for k,v in kw.items():
+            ticket[k] = v
+        return ticket.insert()
+
+    def _create_a_ticket(self):
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'santa'
+        ticket['summary'] = 'Foo'
+        ticket['foo'] = 'This is a custom field'
+        return ticket
+
+    # TicketSystem tests
+    
+    def test_get_ends(self):
+        links_provider = LinksProvider(self.env)
+        self.assertEquals(set(links_provider.get_ends()),
+                          set([('dependson', 'dependent'), 
+                               ('parent', 'children'), ('refersto', None)])
+                          )
+    
+    def test_render_end(self):
+        links_provider = LinksProvider(self.env)
+        self.assertEquals(links_provider.render_end('refersto'), 'Refersto')
+        self.assertEquals(links_provider.render_end('children'), 'Overridden')
+    
+    def test_is_blocker(self):
+        links_provider = LinksProvider(self.env)
+        self.assertFalse(links_provider.is_blocker('parent'))
+        self.assertTrue(links_provider.is_blocker('children'))
+        
+    def test_link_ends_map(self):
+        ticket_system = self.ticket_links_system
+        self.assertEquals(ticket_system.link_ends_map,
+                          {'dependson': 'dependent', 'dependent': 'dependson',
+                           'parent': 'children', 'children': 'parent',
+                           'refersto': None})
+    
+    def test_parse_links(self):
+        ticket_system = self.ticket_links_system
+        self.assertEquals([1, 2, 42], ticket_system.parse_links('1 2 42'))
+        self.assertEquals([1, 2, 42], ticket_system.parse_links('#1 #2 #42'))
+        self.assertEquals([1, 2, 42], ticket_system.parse_links('1, 2, 42'))
+        self.assertEquals([1, 2, 42], ticket_system.parse_links('#1, #2, #42'))
+        self.assertEquals([1, 2, 42], ticket_system.parse_links('#1 #2 #42 #1'))
+        
+    def test_get_ticket_fields(self):
+        ticket_system = TicketSystem(self.env)
+        fields = ticket_system.get_ticket_fields()
+        link_fields = [f['name'] for f in fields if f.get('link')]
+        self.assertEquals(5, len(link_fields))
+
+    def test_update_links(self):
+        ticket = self._create_a_ticket()
+        ticket.insert()
+        ticket = self._create_a_ticket()
+        ticket['dependson'] = '#1, #2'
+        ticket.insert()
+
+        # Check if ticket link in #1 has been updated
+        ticket = Ticket(self.env, 1)
+        self.assertEqual(1, ticket.id)
+        self.assertEqual('#2', ticket['dependent'])
+        
+        # Remove link from #2 to #1
+        ticket = Ticket(self.env, 2)
+        ticket['dependson'] = '#2'
+        ticket.save_changes("me", "testing")
+
+        # Check if ticket link in #1 has been updated
+        ticket = Ticket(self.env, 1)
+        self.assertEqual(1, ticket.id)
+        self.assertEqual('', ticket['dependent'])
+        
+    def test_populate_from_linked_field(self):
+        ticket = self._create_a_ticket()
+        ticket.insert()
+        ticket = Ticket(self.env)
+#        ticket.populate_from(1, link_field_name='children')
+        self.ticket_links_model.populate_from(
+            ticket, 1, link_field_name='children')
+        self.assertEqual('Foo', ticket['summary'])
+        self.assertEqual('#1', ticket['children'])
+        
+    def test_save_retrieve_links(self):
+        ticket = self._create_a_ticket()
+        ticket.insert()
+        ticket = self._create_a_ticket()
+        ticket['dependson'] = '#1, #2'
+        ticket.insert()
+
+        # Check if ticket link in #1 has been updated
+        ticket = Ticket(self.env, 1)
+        self.assertEqual(1, ticket.id)
+        self.assertEqual('#2', ticket['dependent'])
+
+    def test_query_by_result(self):
+        ticket = self._create_a_ticket()
+        ticket.insert()
+        ticket = self._create_a_ticket()
+        ticket['dependson'] = '#1'
+        ticket.insert()
+        query = Query.from_string(self.env, 'dependson=1', order='id')
+        sql, args = query.get_sql()
+        tickets = query.execute(self.req)
+        self.assertEqual(len(tickets), 1)
+        self.assertEqual(tickets[0]['id'], 2)
+    
+    def test_query_by_result2(self):
+        ticket = self._create_a_ticket()
+        ticket.insert()
+        ticket = self._create_a_ticket()
+        ticket['dependson'] = '#1'
+        ticket.insert()
+        query = Query.from_string(self.env, 'dependent=2', order='id')
+        sql, args = query.get_sql()
+        tickets = query.execute(self.req)
+        self.assertEqual(len(tickets), 1)
+        self.assertEqual(tickets[0]['id'], 1)
+    
+    def test_validator_links_exists(self):
+        ticket1 = self._create_a_ticket()
+        ticket1.insert()
+        ticket2 = self._create_a_ticket()
+        ticket2['dependson'] = '#1'
+        links_provider = LinksProvider(self.env)
+        issues = links_provider.validate_ticket(self.req, ticket2)
+        self.assertEquals(sum(1 for _ in issues), 0)
+        ticket2['dependson'] = '#404'
+        issues = links_provider.validate_ticket(self.req, ticket2)
+        self.assertEquals(sum(1 for _ in issues), 1)
+        
+    def test_validator_no_cyle(self):
+        ticket1 = self._create_a_ticket()
+        ticket1.insert()
+        ticket2 = self._create_a_ticket()
+        ticket2['dependson'] = '#1'
+        links_provider = LinksProvider(self.env)
+        issues = links_provider.validate_ticket(self.req, ticket2)
+        self.assertEquals(sum(1 for _ in issues), 0)
+        ticket2.insert()
+        ticket1['dependson'] = '#2'
+        issues = links_provider.validate_ticket(self.req, ticket1)
+        self.assertEquals(sum(1 for _ in issues), 1)
+
+    def test_validator_parent_child(self):
+        ticket1 = self._create_a_ticket()
+        ticket1.insert()
+        ticket2 = self._create_a_ticket()
+        ticket2['parent'] = '#1'
+        links_provider = LinksProvider(self.env)
+        issues = links_provider.validate_ticket(self.req, ticket2)
+        self.assertEquals(sum(1 for _ in issues), 0)
+        ticket2.insert()
+        ticket1['parent'] = '#2'
+        issues = links_provider.validate_ticket(self.req, ticket1)
+        self.assertEquals(sum(1 for _ in issues), 1)
+        ticket1['parent'] = ''
+        ticket3 = self._create_a_ticket()
+        ticket3['parent'] = '#1, #2'
+        issues = links_provider.validate_ticket(self.req, ticket3)
+        self.assertEquals(sum(1 for _ in issues), 1)
+    
+    def test_validator_blocker(self):
+        ticket1 = self._create_a_ticket()
+        ticket1['status'] = 'new'
+        ticket1.insert()
+        ticket2 = self._create_a_ticket()
+        ticket2['children'] = '#1'
+        links_provider = LinksProvider(self.env)
+        self.assertEquals([1], links_provider.find_blockers(ticket2, 'children',
+                                                                     []))
+        req = copy(self.req) 
+        req.args['action'] = 'resolve'
+        issues = links_provider.validate_ticket(req, ticket2)
+        self.assertEquals(sum(1 for _ in issues), 1)
+        
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TicketTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')

File installer/requirements-dev.txt

 -e ../bloodhound_theme
 TracPermRedirect
 -e ../bloodhound_search
--e ../bloodhound_relations
+#-e ../bloodhound_relations