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',
},