Author: andrej
Date: Mon Apr 29 21:44:59 2013
New Revision: 1477361

URL: http://svn.apache.org/r1477361
Log:
adding resolution blocking for ticketfor bhrelations - work in progress

Modified:
    bloodhound/trunk/bloodhound_multiproduct/multiproduct/dbcursor.py
    bloodhound/trunk/bloodhound_relations/bhrelations/api.py
    bloodhound/trunk/bloodhound_relations/bhrelations/model.py
    bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py

Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/dbcursor.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/multiproduct/dbcursor.py?rev=1477361&r1=1477360&r2=1477361&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/multiproduct/dbcursor.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/multiproduct/dbcursor.py Mon Apr 
29 21:44:59 2013
@@ -32,7 +32,7 @@ SKIP_TABLES = ['auth_cookie',
                'cache',
                'repository', 'revision', 'node_change',
                'bloodhound_product', 'bloodhound_productresourcemap', 
'bloodhound_productconfig',
-               'sqlite_master'
+               'sqlite_master', 'bloodhound_relations'
                ]
 TRANSLATE_TABLES = ['system',
                     'ticket', 'ticket_change', 'ticket_custom',

Modified: bloodhound/trunk/bloodhound_relations/bhrelations/api.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/api.py?rev=1477361&r1=1477360&r2=1477361&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/api.py (original)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/api.py Mon Apr 29 
21:44:59 2013
@@ -25,12 +25,16 @@ from trac.core import Component, impleme
 from trac.env import IEnvironmentSetupParticipant
 from trac.db import DatabaseManager
 from trac.resource import (manager_for_neighborhood, ResourceSystem, Resource,
-                           get_resource_shortname)
-from trac.ticket import Ticket
+                           get_resource_shortname, get_resource_description,
+                           get_resource_summary, get_relative_resource, 
Neighborhood)
+from trac.ticket import Ticket, ITicketManipulator
 
 PLUGIN_NAME = 'Bloodhound Relations Plugin'
 
-class ValidationError(TracError):
+class CycleValidationError(TracError):
+    pass
+
+class ParentValidationError(TracError):
     pass
 
 class EnvironmentSetup(Component):
@@ -97,9 +101,8 @@ class EnvironmentSetup(Component):
 
 class RelationsSystem(Component):
     PARENT_RELATION_TYPE = 'parent'
+    CHILDREN_RELATION_TYPE = 'children'
     RELATIONS_CONFIG_NAME = 'bhrelations_links'
-    RESOURCE_ID_DELIMITER = u":"
-    RELATION_ID_DELIMITER = u","
 
     def __init__(self):
         self._links, self._labels, \
@@ -122,9 +125,10 @@ class RelationsSystem(Component):
             relation_type,
             comment = None,
             ):
-        source = self.get_resource_id_from_instance(source_resource_instance)
-        destination = self.get_resource_id_from_instance(
-            destination_resource_instance)
+        source = ResourceIdSerializer.get_resource_id_from_instance(
+            self.env, source_resource_instance)
+        destination = ResourceIdSerializer.get_resource_id_from_instance(
+            self.env, destination_resource_instance)
         relation = Relation(self.env)
         relation.source = source
         relation.destination = destination
@@ -133,28 +137,23 @@ class RelationsSystem(Component):
         self.add_relation(relation)
 
     def add_relation(self, relation):
-        self.validate(relation)
         #TBD: add changes in source and destination ticket history
+        self.validate(relation)
         with self.env.db_transaction:
             relation.insert()
             other_end = self.link_ends_map[relation.type]
             if other_end:
                 reverted_relation = relation.clone_reverted(other_end)
+                # self.validate(relation)
                 reverted_relation.insert()
 
     def delete(
             self,
             relation_id,
         ):
-        source, destination, relation_type = self._parse_relation_id(
-            relation_id)
-        #TODO: some optimization can be introduced here to not load relations
+        #TODO: some optimization can be made here by not loading relations
         #before actual DELETE SQL
-        relation = Relation(self.env, keys=dict(
-            source=source,
-            destination=destination,
-            type=relation_type
-            ))
+        relation = Relation.load_by_relation_id(self.env, relation_id)
         self._delete_relation(relation)
 
     def _delete_relation(self, relation):
@@ -180,22 +179,24 @@ class RelationsSystem(Component):
 
     def get_relations(self, resource_instance):
         relation_list = []
-        for relation in self._select_relations_for_resource(resource_instance):
+        for relation in self._select_relations_for_resource_instance(
+                resource_instance):
             relation_list.append(dict(
-                relation_id = self._create_relation_id(relation),
+                relation_id = relation.get_relation_id(),
                 destination_id = relation.destination,
-                destination=self._create_resource_instance_by_full_id(
+                destination=ResourceIdSerializer.get_resource_by_id(
                     relation.destination),
                 type = relation.type,
                 comment = relation.comment
             ))
         return relation_list
 
-    def _select_relations_for_resource(self, resource, resource_type=None):
-        source = self.get_resource_id_from_instance(resource)
-        return self._select_relations_by_source(source, resource_type)
+    def _select_relations_for_resource_instance(self, resource):
+        resource_full_id = ResourceIdSerializer.get_resource_id_from_instance(
+            self.env, resource)
+        return self._select_relations(resource_full_id)
 
-    def _select_relations_by_source(
+    def _select_relations(
             self, source, resource_type=None):
         #todo: add optional paging for possible umbrella tickets with
         #a lot of child tickets
@@ -211,17 +212,6 @@ class RelationsSystem(Component):
             order_by=order_by
             )
 
-    def _create_relation_id(self, relation):
-        return self.RELATION_ID_DELIMITER.join((
-            relation.source,
-            relation.destination,
-            relation.type))
-
-    def _parse_relation_id(self, relation_id):
-        source, destination, relation_type = relation_id.split(
-            self.RELATION_ID_DELIMITER)
-        return source, destination, relation_type
-
     # Copied from trac/ticket/links.py, ticket-links-trunk branch
     def _get_links_config(self):
         links = []
@@ -269,80 +259,10 @@ class RelationsSystem(Component):
 
         return links, labels, validators, blockers, copy_fields
 
-    def get_resource_id_from_instance(self, resource_instance):
-        resource = resource_instance.resource
-        rsys = ResourceSystem(manager_for_neighborhood(
-            self.env, resource.neighborhood))
-        nbhprefix = rsys.neighborhood_prefix(resource.neighborhood)
-        resource_full_id = self.RESOURCE_ID_DELIMITER.join(
-            (nbhprefix, resource.realm, unicode(resource.id))
-        )
-        return resource_full_id
-
-    def _create_resource_instance_by_full_id(self, resource_full_id):
-        """
-        * 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 = self.split_full_id(resource_full_id)
-        return self._create_resource_by_ids(nbhprefix, realm, resource_id)
-
-    def _get_resource_by_id(self, resource_full_id):
-        """
-        * 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 = self.split_full_id(resource_full_id)
-        env = self._get_env_by_prefix(nbhprefix)
-        return Resource(env, realm, resource_id)
-
-
-    def _create_resource_by_ids(self, nbhprefix, realm, resource_id):
-        env = self._get_env_by_prefix(nbhprefix)
-        #todo: implement more generic resource factory mechanism
-        if realm == "ticket":
-            return Ticket(env, resource_id)
-        else:
-            raise TracError("Resource type %s is not supported by " +
-                            "Bloodhound Relations" % realm)
-
-    def _get_env_by_prefix(self, nbhprefix):
-        if nbhprefix:
-            env = ProductEnvironment(nbhprefix)
-        elif hasattr(self.env, "parent") and self.env.parent:
-            env = self.env.parent
-        else:
-            env = self.env
-        return env
-
-
     def validate(self, relation):
         validator = self._get_validator(relation.type)
-        result = validator(relation)
-        if result is not None:
-            raise ValidationError(result)
-
-    # Copied from trac/utils.py, ticket-links-trunk branch
-    def unique(self, seq):
-        """Yield unique elements from sequence of hashables, preserving order.
-        (New in 0.13)
-        """
-        seen = set()
-        return (x for x in seq if x not in seen and not seen.add(x))
-
-    def can_be_resolved(self, resource):
-        #todo: implement the method
-        # blockers = []
-        for relation in self._select_relations_for_resource(resource):
-            if self.is_blocker(relation.type):
-                blockers = self.find_blockers(relation)
-                if blockers:
-                    blockers_str = ', '.join('#%s' % x
-                                             for x in self.unique(blockers))
-                    msg = ("Cannot resolve this ticket because it is "
-                           "blocked by '%s' tickets [%s]"
-                           % (end,  blockers_str))
-                    yield None, msg
+        if validator:
+            validator(relation)
 
     def is_blocker(self, relation_type):
         return self._blockers[relation_type]
@@ -352,11 +272,10 @@ class RelationsSystem(Component):
         validator_name = self._validators.get(relation_type)
         if validator_name == 'no_cycle':
             validator = self._validate_no_cycle
-        elif validator_name == 'parent_child' and \
-                        relation_type == self.PARENT_RELATION_TYPE:
+        elif validator_name == 'parent_child':
             validator = self._validate_parent
         else:
-            validator = self._validate_any
+            validator = None
         return validator
 
     def _validate_no_cycle(self, relation):
@@ -364,36 +283,40 @@ class RelationsSystem(Component):
         if cycle != None:
             cycle_str = [self._get_resource_name_from_id(resource_id)
                          for resource_id in cycle]
-            return 'Cycle in ''%s'': %s' % (
+            error =  'Cycle in ''%s'': %s' % (
                 self.render_relation_type(relation.type),
                 ' -> '.join(cycle_str))
+            error =  CycleValidationError(error)
+            error.failed_ids = cycle
+            raise error
 
     def _validate_parent(self, relation):
-        cycle_validation = self._validate_no_cycle(relation)
-        if cycle_validation:
-            return cycle_validation
+        self._validate_no_cycle(relation)
 
         if relation.type == self.PARENT_RELATION_TYPE:
-            parent_relations = self._select_relations_by_source(
-                relation.source, relation.type)
-            if len(parent_relations):
-                source_resource_name = self._get_resource_name_from_id(
-                    relation.source)
-                parent_ids_ins_string = ", ".join(
-                    [self._get_resource_name_from_id(relation.destination)
-                     for relation in parent_relations]
-                )
-                return "Multiple links in '%s': #%s -> [%s]" % (
-                    self.render_relation_type(relation.type),
-                    source_resource_name,
-                    parent_ids_ins_string)
-
-    def _get_resource_name_from_id(self, resource_id):
-        resource = self._get_resource_by_id(resource_id)
-        return get_resource_shortname(self.env, resource)
+            source = relation.source
+        elif relation.type == self.CHILDREN_RELATION_TYPE:
+            source = relation.destination
+        else:
+            return None
 
-    def _validate_any(self, relation):
-        return None
+        parent_relations = self._select_relations(
+            source, self.PARENT_RELATION_TYPE)
+        if len(parent_relations):
+            source_resource_name = self._get_resource_name_from_id(
+                relation.source)
+            parent_ids_ins_string = ", ".join(
+                [self._get_resource_name_from_id(relation.destination)
+                 for relation in parent_relations]
+            )
+            error = "Multiple links in '%s': %s -> [%s]" % (
+                self.render_relation_type(relation.type),
+                source_resource_name,
+                parent_ids_ins_string)
+            ex = ParentValidationError(error)
+            ex.failed_ids = [relation.destination
+                             for relation in parent_relations]
+            raise ex
 
     def _find_cycle(self, source_to_check, relation, path):
         #todo: optimize this
@@ -417,32 +340,120 @@ class RelationsSystem(Component):
     def render_relation_type(self, end):
         return self._labels[end]
 
-    def find_blockers(self, relation, blockers):
+    def find_blockers(self, resource_instance, is_blocker_method):
+        all_blockers = []
+        for relation in self._select_relations_for_resource_instance(
+                resource_instance):
+            if self.is_blocker(relation.type):
+                blockers = self._recursive_find_blockers(relation, 
is_blocker_method)
+                if blockers:
+                    all_blockers.extend(blockers)
+        return all_blockers
+
+    def _recursive_find_blockers(self, relation, blockers, is_blocker_method):
         #todo: optimize performance by possibility to select more
         # source ids at once
-        for linked_relation in self._select_relations_by_source(
+        for linked_relation in self._select_relations(
                 relation.destination):
-            resource_instance = self._create_resource_instance_by_full_id(
+            resource = ResourceIdSerializer.get_resource_by_id(
                 linked_relation.destination)
-            if self._is_resource_blocked(resource_instance):
+            resource_instance = is_blocker_method(resource)
+            if resource_instance is not None:
                 blockers.append(resource_instance)
             else:
-                self.find_blockers(linked_relation, blockers)
+                self._recursive_find_blockers(linked_relation, blockers)
         return blockers
 
-    def split_full_id(self, resource_full_id):
-        return resource_full_id.split(self.RESOURCE_ID_DELIMITER)
+    def _get_resource_name_from_id(self, resource_id):
+        resource = ResourceIdSerializer.get_resource_by_id(resource_id)
+        return get_resource_shortname(self.env, resource)
+
+
 
-    def _is_resource_blocked(self, resource_instance):
-        #todo: implement more generic blocker validation
-        realm = resource_instance.resource.realm
-        if realm == "ticket":
-            ticket = resource_instance
-            return ticket['status'] != 'closed'
+class ResourceIdSerializer(object):
+    RESOURCE_ID_DELIMITER = u":"
+
+    @classmethod
+    def get_resource_by_id(cls, resource_full_id):
+        """
+        * 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)
+            return neighborhood.child(realm, id=resource_id)
         else:
-            raise TracError("Block validation for type %s is not supported" +
-                        " by Bloodhound Relations" % realm)
+            return Resource(realm, id=resource_id)
+
+    @classmethod
+    def _split_full_id(cls, resource_full_id):
+        return resource_full_id.split(cls.RESOURCE_ID_DELIMITER)
+
+
+    @classmethod
+    def get_resource_id_from_instance(cls, env, resource_instance):
+        """
+        * resource_instance: can be instance of a ticket, wiki page etc.
+        """
+        resource = resource_instance.resource
+        rsys = ResourceSystem(manager_for_neighborhood(
+            env, resource.neighborhood))
+        nbhprefix = rsys.neighborhood_prefix(resource.neighborhood)
+        resource_full_id = cls.RESOURCE_ID_DELIMITER.join(
+            (nbhprefix, resource.realm, unicode(resource.id))
+        )
+        return resource_full_id
+
+
+class TicketRelationsSpecifics(Component):
+    implements(ITicketManipulator)
+
+    def prepare_ticket(self, req, ticket, fields, actions):
+        pass
+
+    def validate_ticket(self, req, ticket):
+        action = req.args.get('action')
+        if action == 'resolve':
+            blockers = RelationsSystem(self.env).find_blockers(
+                ticket,self.is_blocker)
+            if blockers:
+                blockers_str = ', '.join(
+                    get_resource_shortname(blocker_ticket.resource)
+                    for blocker_ticket in unique(blockers))
+                msg = ("Cannot resolve this ticket because it is "
+                       "blocked by tickets [%s]"
+                       % blockers_str)
+                yield None, msg
+
+    def is_blocker(self, resource):
+        ticket = self._create_ticket_by_full_id(resource)
+        if ticket['status'] != 'closed':
+            return ticket
+        return None
 
+    def _create_ticket_by_full_id(self, resource):
+        env = self._get_env_by_prefix(resource.nbhprefix)
+        if resource.realm == "ticket":
+            return Ticket(env, resource.id)
+        else:
+            raise TracError("Resource type %s is not supported by " +
+                            "Bloodhound Relations" % resource.realm)
 
+    def _get_env_by_prefix(self, nbhprefix):
+        if nbhprefix:
+            env = ProductEnvironment(nbhprefix)
+        elif hasattr(self.env, "parent") and self.env.parent:
+            env = self.env.parent
+        else:
+            env = self.env
+        return env
 
+# Copied from trac/utils.py, ticket-links-trunk branch
+def unique(self, seq):
+    """Yield unique elements from sequence of hashables, preserving order.
+    (New in 0.13)
+    """
+    seen = set()
+    return (x for x in seq if x not in seen and not seen.add(x))
 

Modified: bloodhound/trunk/bloodhound_relations/bhrelations/model.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/model.py?rev=1477361&r1=1477360&r2=1477361&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/model.py (original)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/model.py Mon Apr 29 
21:44:59 2013
@@ -23,6 +23,8 @@ from trac.resource import Resource
 
 class Relation(ModelBase):
     """The Relation table"""
+    RELATION_ID_DELIMITER = u","
+
     _meta = {'table_name':'bloodhound_relations',
             'object_name':'Relation',
             'key_fields':['source', 'type', 'destination'],
@@ -54,3 +56,24 @@ class Relation(ModelBase):
         relation.type = type
         return relation
 
+    def get_relation_id(self):
+        return self.RELATION_ID_DELIMITER.join((
+            self.source,
+            self.destination,
+            self.type))
+
+    @classmethod
+    def _parse_relation_id(cls, relation_id):
+        source, destination, relation_type = relation_id.split(
+            cls.RELATION_ID_DELIMITER)
+        return source, destination, relation_type
+
+    @classmethod
+    def load_by_relation_id(cls, env, relation_id):
+        source, destination, relation_type = cls._parse_relation_id(
+            relation_id)
+        return Relation(env, keys=dict(
+            source=source,
+            destination=destination,
+            type=relation_type
+            ))

Modified: bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py?rev=1477361&r1=1477360&r2=1477361&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py (original)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py Mon Apr 29 
21:44:59 2013
@@ -18,7 +18,7 @@
 #  specific language governing permissions and limitations
 #  under the License.
 from _sqlite3 import OperationalError, IntegrityError
-from bhrelations.api import EnvironmentSetup, RelationsSystem, ValidationError
+from bhrelations.api import EnvironmentSetup, RelationsSystem, 
CycleValidationError, ParentValidationError, TicketRelationsSpecifics
 from trac.ticket.model import Ticket
 from trac.test import EnvironmentStub, Mock
 from trac.core import TracError
@@ -88,11 +88,11 @@ class ApiTestCase(unittest.TestCase):
         #assert
         relations = relations_system.get_relations(ticket)
         self.assertEqual("dependent", relations[0]["type"])
-        self.assertEqual(dependent.id, relations[0]["destination"].id)
+        self.assertEqual(unicode(dependent.id), relations[0]["destination"].id)
 
         relations = relations_system.get_relations(dependent)
         self.assertEqual("dependson", relations[0]["type"])
-        self.assertEqual(ticket.id, relations[0]["destination"].id)
+        self.assertEqual(unicode(ticket.id), relations[0]["destination"].id)
 
     def test_can_add_single_way_relations(self):
         #arrange
@@ -104,7 +104,7 @@ class ApiTestCase(unittest.TestCase):
         #assert
         relations = relations_system.get_relations(ticket)
         self.assertEqual("refersto", relations[0]["type"])
-        self.assertEqual(referred.id, relations[0]["destination"].id)
+        self.assertEqual(unicode(referred.id), relations[0]["destination"].id)
 
         relations = relations_system.get_relations(referred)
         self.assertEqual(0, len(relations))
@@ -157,7 +157,8 @@ class ApiTestCase(unittest.TestCase):
         #assert
         relations = relations_system.get_relations(ticket)
         self.assertEqual("refersto", relations[0]["type"])
-        self.assertEqual(referred_ticket.id, relations[0]["destination"].id)
+        self.assertEqual(unicode(referred_ticket.id),
+                         relations[0]["destination"].id)
 
         relations = relations_system.get_relations(referred_ticket)
         self.assertEqual(0, len(relations))
@@ -214,12 +215,37 @@ class ApiTestCase(unittest.TestCase):
         #act
         relations_system = self.relations_system
         relations_system.add(ticket1, ticket2, "dependson")
+
+        try:
+            relations_system.add(ticket2, ticket1, "dependson")
+            self.assertFalse(True, "Should throw an exception")
+        except CycleValidationError, ex:
+            self.assertEqual(":ticket:1", ex.failed_ids[0])
+
+
+    def test_can_add_more_dependsons(self):
+        #arrange
+        ticket1 = self._insert_and_load_ticket("A1")
+        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")
+
+    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.assertRaises(
-            ValidationError,
+            CycleValidationError,
             relations_system.add,
-            ticket2,
             ticket1,
-            "dependson")
+            ticket2,
+            "dependent")
 
     def test_can_not_add_cycled_relations(self):
         #arrange
@@ -230,22 +256,72 @@ class ApiTestCase(unittest.TestCase):
         relations_system = self.relations_system
         relations_system.add(ticket1, ticket2, "dependson")
         relations_system.add(ticket2, ticket3, "dependson")
-        self.assertRaises(
-            ValidationError,
+        self.assertRaisesRegexp(
+            CycleValidationError,
+            "Cycle in Dependson: #1 -> #2 -> #3",
             relations_system.add,
             ticket3,
             ticket1,
             "dependson")
 
-    # def _find_relation(self, relations, destination, relation_type):
-    #     destination_id = self.relations_system.get_resource_id(destination)
-    #     for relation in relations:
-    #         if relation["destination_id"] == destination_id and \
-    #             relation["type"] == relation_type:
-    #             return relation
-    #     raise Exception("Relation was not found for destination_id: %s,"+
-    #                     " relation_type: %s" % (destination_id, 
relation_type))
+    def test_can_not_add_more_than_one_parents(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.assertRaises(
+            ParentValidationError,
+            relations_system.add,
+            child,
+            parent2,
+            "parent")
+
+    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(
+            ParentValidationError,
+            relations_system.add,
+            parent2,
+            child,
+            "children")
+
+    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")
+        #act
+        relations_system = self.relations_system
+        relations_system.add(parent1, child, "children")
+        self.assertRaises(
+            ParentValidationError,
+            relations_system.add,
+            parent2,
+            child,
+            "children")
+
+    def test_blocked_ticket_cannot_be_resolved(self):
+        ticket1 = self._insert_and_load_ticket("A1")
+        ticket2 = self._insert_and_load_ticket("A2")
+        self.relations_system.add(ticket1, ticket2, "dependent")
+
+        self.req.args=dict(action='resolve')
+        ticket_relations = TicketRelationsSpecifics(self.env)
+        warnings = ticket_relations.validate_ticket(self.req, ticket1)
+        self.assertEqual(1, len(list(warnings)))
+
+    #todo: add tests that relation were deleted when ticket was deleted
 
+    #todo: add multi-product test
     def _debug_select(self):
         """
         used for debug purposes


Reply via email to