Author: astaric
Date: Fri May 17 12:19:49 2013
New Revision: 1483766

URL: http://svn.apache.org/r1483766
Log:
Upgraded ExclusiveValidator. Towards #528.


Added:
    bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py
Modified:
    bloodhound/trunk/bloodhound_relations/bhrelations/api.py
    bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py
    bloodhound/trunk/bloodhound_relations/bhrelations/validation.py
    bloodhound/trunk/installer/bloodhound_setup.py

Modified: bloodhound/trunk/bloodhound_relations/bhrelations/api.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/api.py?rev=1483766&r1=1483765&r2=1483766&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/api.py (original)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/api.py Fri May 17 
12:19:49 2013
@@ -162,15 +162,20 @@ class RelationsSystem(Component):
     global_validators = OrderedExtensionsOption(
         'bhrelations', 'global_validators',
         interface=IRelationValidator,
-        default=['NoSelfReferenceValidator'],
         include_missing=False,
         doc="""Validators used to validate all relations,
         regardless of their type."""
     )
 
     def __init__(self):
-        self._links, self._labels, self._validators, self._blockers, \
-            self._copy_fields = self._get_links_config()
+        links, labels, validators, blockers, copy_fields, exclusive = \
+            self._parse_config()
+        self._links = links
+        self._labels = labels
+        self._validators = validators
+        self._blockers = blockers
+        self._copy_fields = copy_fields
+        self._exclusive = exclusive
 
         self.link_ends_map = {}
         for end1, end2 in self.get_ends():
@@ -300,53 +305,41 @@ class RelationsSystem(Component):
             order_by=order_by
         )
 
-    # Copied from trac/ticket/links.py, ticket-links-trunk branch
-    def _get_links_config(self):
+    def _parse_config(self):
         links = []
         labels = {}
         validators = {}
         blockers = {}
         copy_fields = {}
+        exclusive = set()
 
         config = self.config[self.RELATIONS_CONFIG_NAME]
         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:
+            reltypes = config.getlist(name)
+            if not reltypes:
                 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
+            if len(reltypes) == 1:
+                reltypes += [None]
+            links.append(tuple(reltypes))
 
             custom_validators = self._parse_validators(config, name)
-            if custom_validators:
-                validators[end1] = custom_validators
-                if end2:
-                    validators[end2] = custom_validators
-
-            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
+            for rel in filter(None, reltypes):
+                labels[rel] = \
+                    config.get(rel + '.label') or rel.capitalize()
+                blockers[rel] = \
+                    config.getbool(rel + '.blocks', default=False)
+                if config.getbool(rel + '.exclusive'):
+                    exclusive.add(rel)
+                validators[rel] = custom_validators
+
+                # <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
+                cf_key = '%s.copy_fields' % rel
+                if cf_key in config:
+                    copy_fields[rel] = config.getlist(cf_key)
+        return links, labels, validators, blockers, copy_fields, exclusive
 
     def _parse_validators(self, section, name):
         custom_validators = set(

Modified: bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py?rev=1483766&r1=1483765&r2=1483766&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py (original)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py Fri May 17 
12:19:49 2013
@@ -45,6 +45,8 @@ class BaseApiApiTestCase(MultiproductTes
             default_data=True,
             enable=['trac.*', 'multiproduct.*', 'bhrelations.*']
         )
+        env.config.set('bhrelations', 'global_validators',
+                       'NoSelfReferenceValidator,ExclusiveValidator')
         config_name = RelationsSystem.RELATIONS_CONFIG_NAME
         env.config.set(config_name, 'dependency', 'dependson,dependent')
         env.config.set(config_name, 'dependency.validators',
@@ -52,10 +54,11 @@ class BaseApiApiTestCase(MultiproductTes
         env.config.set(config_name, 'dependent.blocks', 'true')
         env.config.set(config_name, 'parent_children', 'parent,children')
         env.config.set(config_name, 'parent_children.validators',
-                       'OneToMany,SingleProduct,NoCycles,Exclusive')
+                       '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')
 
@@ -242,7 +245,7 @@ class ApiTestCase(BaseApiApiTestCase):
             self.fail("Should throw an exception")
         except ValidationError as ex:
             self.assertSequenceEqual(
-                ["tp1:ticket:1", "tp1:ticket:2"], ex.failed_ids)
+                ["tp1:ticket:2", "tp1:ticket:1"], ex.failed_ids)
 
     def test_can_add_more_depends_ons(self):
         #arrange
@@ -462,6 +465,58 @@ class ApiTestCase(BaseApiApiTestCase):
             ticket1, ticket2, "children"
         )
 
+    def test_cannot_create_other_relations_between_descendants(self):
+        t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, range(5))
+        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.assertRaises(
+            ValidationError,
+            self.relations_system.add, t1, t2, "dependent"
+        )
+        self.assertRaises(
+            ValidationError,
+            self.relations_system.add, t2, t1, "dependent"
+        )
+        self.assertRaises(
+            ValidationError,
+            self.relations_system.add, t1, t4, "dependent"
+        )
+        self.assertRaises(
+            ValidationError,
+            self.relations_system.add, t3, t1, "dependent"
+        )
+        try:
+            self.relations_system.add(t1, t5, "dependent")
+            self.relations_system.add(t3, t4, "dependent")
+        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, range(5))
+        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.assertRaises(
+            ValidationError,
+            self.relations_system.add, t5, t2, "parent"
+        )
+        self.assertRaises(
+            ValidationError,
+            self.relations_system.add, t5, t3, "parent"
+        )
+        self.assertRaises(
+            ValidationError,
+            self.relations_system.add, t1, t5, "parent"
+        )
+        try:
+            self.relations_system.add(t5, t1, "parent")
+        except ValidationError:
+            self.fail("Could not add valid relation.")
+
 
 class RelationChangingListenerTestCase(BaseApiApiTestCase):
     def test_can_sent_adding_event(self):

Added: bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py?rev=1483766&view=auto
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py 
(added)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py Fri 
May 17 12:19:49 2013
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+
+from trac.test import Mock
+
+from bhrelations.validation import Validator
+from bhrelations.tests.api import BaseApiApiTestCase
+
+
+class GraphFunctionsTestCase(BaseApiApiTestCase):
+    edges = [
+        ('A', 'B', 'p'),  #      A    H
+        ('A', 'C', 'p'),  #     /  \ /
+        ('C', 'D', 'p'),  #    B    C
+        ('C', 'E', 'p'),  #        /  \
+        ('E', 'F', 'p'),  #       D    E - F - G
+        ('F', 'G', 'p'),  #
+        ('H', 'C', 'p'),
+    ]
+
+    def setUp(self):
+        BaseApiApiTestCase.setUp(self)
+        # bhrelations point from destination to source
+        for destination, source, type in self.edges:
+            self.env.db_direct_transaction(
+                """INSERT INTO bloodhound_relations (source, destination, type)
+                        VALUES ('%s', '%s', '%s')""" %
+                (source, destination, type)
+            )
+        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)
+
+    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'])
+        )
+
+    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()
+        )
+
+
+

Modified: bloodhound/trunk/bloodhound_relations/bhrelations/validation.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/validation.py?rev=1483766&r1=1483765&r2=1483766&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_relations/bhrelations/validation.py (original)
+++ bloodhound/trunk/bloodhound_relations/bhrelations/validation.py Fri May 17 
12:19:49 2013
@@ -38,14 +38,59 @@ class Validator(Component):
         resource = ResourceIdSerializer.get_resource_by_id(resource_id)
         return get_resource_shortname(self.env, resource)
 
+    def _find_path(self, source, destination, relation_type):
+        known_nodes, paths = self._bfs(source, destination, relation_type)
+        return paths.get((source, destination), None)
+
+    def _descendants(self, source, relation_type):
+        known_nodes, paths = self._bfs(source, None, relation_type)
+        return known_nodes - set([source])
+
+    def _ancestors(self, source, relation_type):
+        known_nodes, paths = self._bfs(source, None, relation_type,
+                                       reverse=True)
+        return known_nodes - set([source])
+
+    def _bfs(self, source, destination, relation_type, reverse=False):
+        known_nodes = set([source])
+        new_nodes = set([source])
+        paths = {(source, source): [source]}
+
+        while new_nodes:
+            if reverse:
+                relation = 'source, destination'
+                origin = 'source'
+            else:
+                relation = 'destination, source'
+                origin = 'destination'
+            query = """
+                SELECT %(relation)s
+                  FROM bloodhound_relations
+                 WHERE type = '%(relation_type)s'
+                   AND %(origin)s IN (%(new_nodes)s)
+            """ % dict(
+                relation=relation,
+                relation_type=relation_type,
+                new_nodes=', '.join("'%s'" % n for n in new_nodes),
+                origin=origin)
+            new_nodes = set()
+            for s, d in self.env.db_query(query):
+                if d not in known_nodes:
+                    new_nodes.add(d)
+                paths[(source, d)] = paths[(source, s)] + [d]
+            known_nodes = set.union(known_nodes, new_nodes)
+            if destination in new_nodes:
+                break
+        return known_nodes, paths
+
 
 class NoCyclesValidator(Validator):
     def validate(self, relation):
         """If a path exists from relation's destination to its source,
          adding the relation will create a cycle.
          """
-        path = self._find_path(relation.destination,
-                               relation.source,
+        path = self._find_path(relation.source,
+                               relation.destination,
                                relation.type)
         if path:
             cycle_str = map(self.get_resource_name, path)
@@ -56,48 +101,54 @@ class NoCyclesValidator(Validator):
             error.failed_ids = path
             raise error
 
-    def _find_path(self, source, destination, relation_type):
-        known_nodes = set()
-        new_nodes = set([source])
-        paths = {(source, source): [source]}
-
-        while new_nodes:
-            known_nodes = set.union(known_nodes, new_nodes)
-            with self.env.db_query as db:
-                relations = dict(db("""
-                    SELECT source, destination
-                      FROM bloodhound_relations
-                     WHERE type = '%(relation_type)s'
-                       AND source IN (%(new_nodes)s)
-                """ % dict(
-                    relation_type=relation_type,
-                    new_nodes=', '.join("'%s'" % n for n in new_nodes))
-                ))
-            new_nodes = set(relations.values()) - known_nodes
-            for s, d in relations.items():
-                paths[(source, d)] = paths[(source, s)] + [d]
-            if destination in new_nodes:
-                return paths[(source, destination)]
-
 
 class ExclusiveValidator(Validator):
     def validate(self, relation):
+        """If a path of exclusive type exists between source and destination,
+        adding a relation is not allowed.
+        """
         rls = RelationsSystem(self.env)
-        incompatible_relations = [
-            rel for rel in rls._select_relations(relation.source)
-            if rel.destination == relation.destination
-        ] + [
-            rel for rel in rls._select_relations(relation.destination)
-            if rel.destination == relation.source
-        ]
-        if incompatible_relations:
-            raise ValidationError(
-                "Relation %s is incompatible with the "
-                "following existing relations: %s" % (
-                    self.render_relation_type(relation.type),
-                    ','.join(map(str, incompatible_relations))
+        source, destination = relation.source, relation.destination
+
+        for exclusive_type in rls._exclusive:
+            path = (self._find_path(source, destination, exclusive_type)
+                    or self._find_path(destination, source, exclusive_type))
+            if path:
+                raise ValidationError(
+                    "Cannot add relation %s, source and destination "
+                    "are connected with %s relation." % (
+                        self.render_relation_type(relation.type),
+                        self.render_relation_type(exclusive_type),
+                    )
+                )
+        if relation.type in rls._exclusive:
+            d_ancestors = self._ancestors(destination, exclusive_type)
+            d_ancestors.add(destination)
+            s_descendants = self._descendants(source, exclusive_type)
+            s_descendants.add(source)
+            query = """
+                SELECT source, destination, type
+                  FROM bloodhound_relations
+                 WHERE (source in (%(s_ancestors)s)
+                        AND destination in (%(d_descendants)s))
+                    OR
+                       (source in (%(d_descendants)s)
+                        AND destination in (%(s_ancestors)s))
+            """ % dict(
+                s_ancestors=', '.join("'%s'" % n for n in d_ancestors),
+                d_descendants=', '.join("'%s'" % n for n in s_descendants))
+            conflicting_relations = list(self.env.db_query(query))
+            if conflicting_relations:
+                raise ValidationError(
+                    "Connecting %s and %s with relation %s "
+                    "would make the following relations invalid:\n"
+                    "%s" % (
+                        source,
+                        destination,
+                        self.render_relation_type(relation.type),
+                        '\n'.join(map(str, conflicting_relations))
+                    )
                 )
-            )
 
 
 class SingleProductValidator(Validator):
@@ -131,7 +182,7 @@ class OneToManyValidator(Validator):
         if existing_relations:
             raise ValidationError(
                 "%s can only have one %s" % (
-                    relation.destination,
+                    relation.source,
                     self.render_relation_type(relation.type)
                 ))
 

Modified: bloodhound/trunk/installer/bloodhound_setup.py
URL: 
http://svn.apache.org/viewvc/bloodhound/trunk/installer/bloodhound_setup.py?rev=1483766&r1=1483765&r2=1483766&view=diff
==============================================================================
--- bloodhound/trunk/installer/bloodhound_setup.py (original)
+++ bloodhound/trunk/installer/bloodhound_setup.py Fri May 17 12:19:49 2013
@@ -89,6 +89,10 @@ BASE_CONFIG = {'components': {'bhtheme.*
                           'footer_left_postfix': '',
                           'footer_right': ''},
                'bhsearch': {'is_default': 'true', 'enable_redirect': 'true'},
+               'bhrelations': {
+                   'global_validators':
+                       'NoSelfReferenceValidator,ExclusiveValidator',
+               },
                'bhrelations_links': {
                     'children.label': 'Child',
                     'dependency': 'dependson,dependent',
@@ -98,8 +102,9 @@ BASE_CONFIG = {'components': {'bhtheme.*
                     'dependent.label': 'Dependent',
                     'oneway': 'refersto',
                     'parent_children': 'parent,children',
+                    'parent.exclusive': 'true',
                     'parent_children.validators':
-                        'OneToMany,SingleProduct,NoCycles,Exclusive',
+                        'OneToMany,SingleProduct,NoCycles',
                     'refersto.label': 'Refers to',
                },
 


Reply via email to