BTW, this is what happened at the client side:

(bloodhound)~/dev/bloodhound$ svn commit
Sending        bloodhound_relations/bhrelations/api.py
Sending        bloodhound_relations/bhrelations/tests/api.py
Adding         bloodhound_relations/bhrelations/tests/base.py
Sending        bloodhound_relations/bhrelations/tests/notification.py
Sending        bloodhound_relations/bhrelations/tests/search.py
Sending        bloodhound_relations/bhrelations/tests/validation.py
Sending        bloodhound_relations/bhrelations/tests/web_ui.py
Adding         bloodhound_relations/bhrelations/utils.py
Sending        bloodhound_relations/bhrelations/web_ui.py
Sending        bloodhound_relations/bhrelations/widgets/relations.py
Sending        bloodhound_theme/bhtheme/htdocs/bloodhound.css
Sending        bloodhound_theme/bhtheme/templates/bh_ticket.html
Sending        installer/bloodhound_setup.py
Transmitting file data .............
Committed revision 1501152.

(bloodhound)~/dev/bloodhound$ svn update -r r1501127
Updating '.':
Restored 'bloodhound_relations/bhrelations/tests/base.py'
Restored 'bloodhound_relations/bhrelations/utils.py'
svn: E175002: REPORT of '/repos/asf/!svn/me': Could not read chunk
size: Secure connection truncated (https://svn.apache.org)
(bloodhound)~/dev/bloodhound$ svn update
Updating '.':
svn: E000000: A reported revision is higher than the current
repository HEAD revision.  Perhaps the repository is out of date with
respect to the master repository?

On Tue, Jul 9, 2013 at 11:36 AM, Anze Staric <[email protected]> wrote:
> Why does the link on the top of the email say that the revision does not 
> exist?
> (http://svn.apache.org/r1501152)
>
> Did I do something wrong?
>
> On Tue, Jul 9, 2013 at 11:19 AM,  <[email protected]> wrote:
>> Author: astaric
>> Date: Tue Jul  9 09:19:00 2013
>> New Revision: 1501152
>>
>> URL: http://svn.apache.org/r1501152
>> Log:
>> Integration of duplicate relations to close as duplicate workflow.
>>
>> Refs: #588
>>
>>
>> Added:
>>     bloodhound/trunk/bloodhound_relations/bhrelations/tests/base.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/utils.py
>> Modified:
>>     bloodhound/trunk/bloodhound_relations/bhrelations/api.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/tests/search.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/tests/web_ui.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/web_ui.py
>>     bloodhound/trunk/bloodhound_relations/bhrelations/widgets/relations.py
>>     bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css
>>     bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket.html
>>     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=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/api.py (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/api.py Tue Jul  9 
>> 09:19:00 2013
>> @@ -17,17 +17,24 @@
>>  #  KIND, either express or implied.  See the License for the
>>  #  specific language governing permissions and limitations
>>  #  under the License.
>> +import itertools
>> +
>> +import re
>>  from datetime import datetime
>>  from pkg_resources import resource_filename
>>  from bhrelations import db_default
>>  from bhrelations.model import Relation
>> +from bhrelations.utils import unique
>>  from multiproduct.api import ISupportMultiProductEnvironment
>> -from trac.config import OrderedExtensionsOption
>> +from multiproduct.model import Product
>> +from multiproduct.env import ProductEnvironment
>> +
>> +from trac.config import OrderedExtensionsOption, Option
>>  from trac.core import (Component, implements, TracError, Interface,
>>                         ExtensionPoint)
>>  from trac.env import IEnvironmentSetupParticipant
>>  from trac.db import DatabaseManager
>> -from trac.resource import (ResourceSystem, Resource,
>> +from trac.resource import (ResourceSystem, Resource, ResourceNotFound,
>>                             get_resource_shortname, Neighborhood)
>>  from trac.ticket import Ticket, ITicketManipulator, ITicketChangeListener
>>  from trac.util.datefmt import utc, to_utimestamp
>> @@ -167,6 +174,12 @@ class RelationsSystem(Component):
>>          regardless of their type."""
>>      )
>>
>> +    duplicate_relation_type = Option(
>> +        'bhrelations',
>> +        'duplicate_relation',
>> +        '',
>> +        "Relation type to be used with the resolve as duplicate workflow.")
>> +
>>      def __init__(self):
>>          links, labels, validators, blockers, copy_fields, exclusive = \
>>              self._parse_config()
>> @@ -443,28 +456,46 @@ class ResourceIdSerializer(object):
>>  class TicketRelationsSpecifics(Component):
>>      implements(ITicketManipulator, ITicketChangeListener)
>>
>> -    #ITicketChangeListener methods
>> +    def __init__(self):
>> +        self.rls = RelationsSystem(self.env)
>>
>> +    #ITicketChangeListener methods
>>      def ticket_created(self, ticket):
>>          pass
>>
>>      def ticket_changed(self, ticket, comment, author, old_values):
>> -        pass
>> +        if (
>> +            self._closed_as_duplicate(ticket) and
>> +            self.rls.duplicate_relation_type
>> +        ):
>> +            try:
>> +                self.rls.add(ticket, ticket.duplicate,
>> +                             self.rls.duplicate_relation_type,
>> +                             comment, author)
>> +            except TracError:
>> +                pass
>> +
>> +    def _closed_as_duplicate(self, ticket):
>> +        return (ticket['status'] == 'closed' and
>> +                ticket['resolution'] == 'duplicate')
>>
>>      def ticket_deleted(self, ticket):
>> -        RelationsSystem(self.env).delete_resource_relations(ticket)
>> +        self.rls.delete_resource_relations(ticket)
>>
>>      #ITicketManipulator methods
>> -
>>      def prepare_ticket(self, req, ticket, fields, actions):
>>          pass
>>
>>      def validate_ticket(self, req, ticket):
>> -        action = req.args.get('action')
>> -        if action == 'resolve':
>> -            rls = RelationsSystem(self.env)
>> -            blockers = rls.find_blockers(
>> -                ticket, self.is_blocker)
>> +        return itertools.chain(
>> +            self._check_blockers(req, ticket),
>> +            self._check_open_children(req, ticket),
>> +            self._check_duplicate_id(req, ticket),
>> +        )
>> +
>> +    def _check_blockers(self, req, ticket):
>> +        if req.args.get('action') == 'resolve':
>> +            blockers = self.rls.find_blockers(ticket, self.is_blocker)
>>              if blockers:
>>                  blockers_str = ', '.join(
>>                      get_resource_shortname(self.env, 
>> blocker_ticket.resource)
>> @@ -474,14 +505,61 @@ class TicketRelationsSpecifics(Component
>>                         % blockers_str)
>>                  yield None, msg
>>
>> -            for relation in [r for r in rls.get_relations(ticket)
>> -                             if r['type'] == rls.CHILDREN_RELATION_TYPE]:
>> +    def _check_open_children(self, req, ticket):
>> +        if req.args.get('action') == 'resolve':
>> +            for relation in [r for r in self.rls.get_relations(ticket)
>> +                             if r['type'] == 
>> self.rls.CHILDREN_RELATION_TYPE]:
>>                  ticket = 
>> self._create_ticket_by_full_id(relation['destination'])
>>                  if ticket['status'] != 'closed':
>>                      msg = ("Cannot resolve this ticket because it has open"
>>                             "child tickets.")
>>                      yield None, msg
>>
>> +    def _check_duplicate_id(self, req, ticket):
>> +        if req.args.get('action') == 'resolve':
>> +            resolution = req.args.get('action_resolve_resolve_resolution')
>> +            if resolution == 'duplicate':
>> +                duplicate_id = req.args.get('duplicate_id')
>> +                if not duplicate_id:
>> +                    yield None, "Duplicate ticket ID must be provided."
>> +
>> +                try:
>> +                    duplicate_ticket = self.find_ticket(duplicate_id)
>> +                    req.perm.require('TICKET_MODIFY',
>> +                                     Resource(duplicate_ticket.id))
>> +                    ticket.duplicate = duplicate_ticket
>> +                except NoSuchTicketError:
>> +                    yield None, "Invalid duplicate ticket ID."
>> +
>> +    def find_ticket(self, ticket_spec):
>> +        ticket = None
>> +        m = re.match(r'#?(?P<tid>\d+)', ticket_spec)
>> +        if m:
>> +            tid = m.group('tid')
>> +            try:
>> +                ticket = Ticket(self.env, tid)
>> +            except ResourceNotFound:
>> +                # ticket not found in current product, try all other 
>> products
>> +                for p in Product.select(self.env):
>> +                    if p.prefix != self.env.product.prefix:
>> +                        # TODO: check for PRODUCT_VIEW permissions
>> +                        penv = ProductEnvironment(self.env.parent, p.prefix)
>> +                        try:
>> +                            ticket = Ticket(penv, tid)
>> +                        except ResourceNotFound:
>> +                            pass
>> +                        else:
>> +                            break
>> +
>> +        # ticket still not found, use fallback for <prefix>:ticket:<id> 
>> syntax
>> +        if ticket is None:
>> +            try:
>> +                resource = 
>> ResourceIdSerializer.get_resource_by_id(ticket_spec)
>> +                ticket = self._create_ticket_by_full_id(resource)
>> +            except:
>> +                raise NoSuchTicketError
>> +        return ticket
>> +
>>      def is_blocker(self, resource):
>>          ticket = self._create_ticket_by_full_id(resource)
>>          if ticket['status'] != 'closed':
>> @@ -573,14 +651,10 @@ class TicketChangeRecordUpdater(Componen
>>              new_value,
>>              product))
>>
>> -# Copied from trac/utils.py, ticket-links-trunk branch
>> -def unique(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))
>> -
>>
>>  class UnknownRelationType(ValueError):
>>      pass
>> +
>> +
>> +class NoSuchTicketError(ValueError):
>> +    pass
>>
>> Modified: bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/api.py Tue Jul  
>> 9 09:19:00 2013
>> @@ -18,99 +18,19 @@
>>  #  specific language governing permissions and limitations
>>  #  under the License.
>>  from datetime import datetime
>> -from _sqlite3 import OperationalError, IntegrityError
>> +from _sqlite3 import IntegrityError
>>  import unittest
>> -from bhrelations.api import (EnvironmentSetup, RelationsSystem,
>> -                             TicketRelationsSpecifics)
>> +from bhrelations.api import TicketRelationsSpecifics
>>  from bhrelations.tests.mocks import TestRelationChangingListener
>>  from bhrelations.validation import ValidationError
>> +from bhrelations.tests.base import BaseRelationsTestCase
>>  from multiproduct.env import ProductEnvironment
>> -from tests.env import MultiproductTestCase
>>  from trac.ticket.model import Ticket
>> -from trac.test import EnvironmentStub, Mock, MockPerm
>>  from trac.core import TracError
>>  from trac.util.datefmt import utc
>>
>> -try:
>> -    from babel import Locale
>>
>> -    locale_en = Locale.parse('en_US')
>> -except ImportError:
>> -    locale_en = None
>> -
>> -
>> -class BaseApiApiTestCase(MultiproductTestCase):
>> -    def setUp(self, enabled=()):
>> -        env = EnvironmentStub(
>> -            default_data=True,
>> -            enable=(['trac.*', 'multiproduct.*', 'bhrelations.*'] +
>> -                    list(enabled))
>> -        )
>> -        env.config.set('bhrelations', 'global_validators',
>> -                       'NoSelfReferenceValidator,ExclusiveValidator,'
>> -                       'BlockerValidator')
>> -        config_name = RelationsSystem.RELATIONS_CONFIG_NAME
>> -        env.config.set(config_name, 'dependency', 'dependson,dependent')
>> -        env.config.set(config_name, 'dependency.validators',
>> -                       'NoCycles,SingleProduct')
>> -        env.config.set(config_name, 'dependson.blocks', 'true')
>> -        env.config.set(config_name, 'parent_children', 'parent,children')
>> -        env.config.set(config_name, 'parent_children.validators',
>> -                       'OneToMany,SingleProduct,NoCycles')
>> -        env.config.set(config_name, 'children.label', 'Overridden')
>> -        env.config.set(config_name, 'parent.copy_fields',
>> -                       'summary, foo')
>> -        env.config.set(config_name, 'parent.exclusive', 'true')
>> -        env.config.set(config_name, 'multiproduct_relation', 
>> 'mprel,mpbackrel')
>> -        env.config.set(config_name, 'oneway', 'refersto')
>> -        env.config.set(config_name, 'duplicate', 'duplicateof,duplicatedby')
>> -        env.config.set(config_name, 'duplicate.validators', 
>> 'ReferencesOlder')
>> -        env.config.set(config_name, 'duplicateof.label', 'Duplicate of')
>> -        env.config.set(config_name, 'duplicatedby.label', 'Duplicated by')
>> -        env.config.set(config_name, 'blocker', 'blockedby,blocks')
>> -        env.config.set(config_name, 'blockedby.blocks', 'true')
>> -
>> -        self.global_env = env
>> -        self._upgrade_mp(self.global_env)
>> -        self._setup_test_log(self.global_env)
>> -        self._load_product_from_data(self.global_env, self.default_product)
>> -        self.env = ProductEnvironment(self.global_env, self.default_product)
>> -
>> -        self.req = Mock(href=self.env.href, authname='anonymous', tz=utc,
>> -                        args=dict(action='dummy'),
>> -                        locale=locale_en, lc_time=locale_en)
>> -        self.req.perm = MockPerm()
>> -        self.relations_system = RelationsSystem(self.env)
>> -        self._upgrade_env()
>> -
>> -    def tearDown(self):
>> -        self.global_env.reset_db()
>> -
>> -    def _upgrade_env(self):
>> -        environment_setup = EnvironmentSetup(self.env)
>> -        try:
>> -            environment_setup.upgrade_environment(self.env.db_transaction)
>> -        except OperationalError:
>> -            # table remains but database version is deleted
>> -            pass
>> -
>> -    @classmethod
>> -    def _insert_ticket(cls, env, summary, **kw):
>> -        """Helper for inserting a ticket into the database"""
>> -        ticket = Ticket(env)
>> -        ticket["Summary"] = summary
>> -        for k, v in kw.items():
>> -            ticket[k] = v
>> -        return ticket.insert()
>> -
>> -    def _insert_and_load_ticket(self, summary, **kw):
>> -        return Ticket(self.env, self._insert_ticket(self.env, summary, 
>> **kw))
>> -
>> -    def _insert_and_load_ticket_with_env(self, env, summary, **kw):
>> -        return Ticket(env, self._insert_ticket(env, summary, **kw))
>> -
>> -
>> -class ApiTestCase(BaseApiApiTestCase):
>> +class ApiTestCase(BaseRelationsTestCase):
>>      def test_can_add_two_ways_relations(self):
>>          #arrange
>>          ticket = self._insert_and_load_ticket("A1")
>> @@ -475,7 +395,7 @@ class ApiTestCase(BaseApiApiTestCase):
>>          )
>>
>>      def test_cannot_create_other_relations_between_descendants(self):
>> -        t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, range(5))
>> +        t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, "12345")
>>          self.relations_system.add(t4, t2, "parent")  #    t1 -> t2
>>          self.relations_system.add(t3, t2, "parent")  #         /  \
>>          self.relations_system.add(t2, t1, "parent")  #       t3    t4
>> @@ -503,7 +423,7 @@ class ApiTestCase(BaseApiApiTestCase):
>>              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))
>> +        t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, "12345")
>>          self.relations_system.add(t4, t2, "parent")  #    t1 -> t2
>>          self.relations_system.add(t3, t2, "parent")  #         /  \
>>          self.relations_system.add(t2, t1, "parent")  #       t3    t4    t5
>> @@ -553,7 +473,7 @@ class ApiTestCase(BaseApiApiTestCase):
>>          self.relations_system.add(t2, t1, "duplicateof")
>>
>>      def test_detects_blocker_cycles(self):
>> -        t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, range(5))
>> +        t1, t2, t3, t4, t5 = map(self._insert_and_load_ticket, "12345")
>>          self.relations_system.add(t1, t2, "blocks")
>>          self.relations_system.add(t3, t2, "dependson")
>>          self.relations_system.add(t4, t3, "blockedby")
>> @@ -577,7 +497,7 @@ class ApiTestCase(BaseApiApiTestCase):
>>          self.relations_system.add(t2, t1, "refersto")
>>
>>
>> -class RelationChangingListenerTestCase(BaseApiApiTestCase):
>> +class RelationChangingListenerTestCase(BaseRelationsTestCase):
>>      def test_can_sent_adding_event(self):
>>          #arrange
>>          ticket1 = self._insert_and_load_ticket("A1")
>> @@ -608,7 +528,7 @@ class RelationChangingListenerTestCase(B
>>          self.assertEqual("dependent", relation.type)
>>
>>
>> -class TicketChangeRecordUpdaterTestCase(BaseApiApiTestCase):
>> +class TicketChangeRecordUpdaterTestCase(BaseRelationsTestCase):
>>      def test_can_update_ticket_history_on_relation_add_on(self):
>>          #arrange
>>          ticket1 = self._insert_and_load_ticket("A1")
>>
>> Added: bloodhound/trunk/bloodhound_relations/bhrelations/tests/base.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/base.py?rev=1501152&view=auto
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/tests/base.py (added)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/base.py Tue Jul  
>> 9 09:19:00 2013
>> @@ -0,0 +1,87 @@
>> +from _sqlite3 import OperationalError
>> +from tests.env import MultiproductTestCase
>> +from multiproduct.env import ProductEnvironment
>> +from bhrelations.api import RelationsSystem, EnvironmentSetup
>> +from trac.test import EnvironmentStub, Mock, MockPerm
>> +from trac.ticket import Ticket
>> +from trac.util.datefmt import utc
>> +
>> +try:
>> +    from babel import Locale
>> +
>> +    locale_en = Locale.parse('en_US')
>> +except ImportError:
>> +    locale_en = None
>> +
>> +
>> +class BaseRelationsTestCase(MultiproductTestCase):
>> +    def setUp(self, enabled=()):
>> +        env = EnvironmentStub(
>> +            default_data=True,
>> +            enable=(['trac.*', 'multiproduct.*', 'bhrelations.*'] +
>> +                    list(enabled))
>> +        )
>> +        env.config.set('bhrelations', 'global_validators',
>> +                       'NoSelfReferenceValidator,ExclusiveValidator,'
>> +                       'BlockerValidator')
>> +        env.config.set('bhrelations', 'duplicate_relation',
>> +                       'duplicateof')
>> +        config_name = RelationsSystem.RELATIONS_CONFIG_NAME
>> +        env.config.set(config_name, 'dependency', 'dependson,dependent')
>> +        env.config.set(config_name, 'dependency.validators',
>> +                       'NoCycles,SingleProduct')
>> +        env.config.set(config_name, 'dependson.blocks', 'true')
>> +        env.config.set(config_name, 'parent_children', 'parent,children')
>> +        env.config.set(config_name, 'parent_children.validators',
>> +                       'OneToMany,SingleProduct,NoCycles')
>> +        env.config.set(config_name, 'children.label', 'Overridden')
>> +        env.config.set(config_name, 'parent.copy_fields',
>> +                       'summary, foo')
>> +        env.config.set(config_name, 'parent.exclusive', 'true')
>> +        env.config.set(config_name, 'multiproduct_relation', 
>> 'mprel,mpbackrel')
>> +        env.config.set(config_name, 'oneway', 'refersto')
>> +        env.config.set(config_name, 'duplicate', 'duplicateof,duplicatedby')
>> +        env.config.set(config_name, 'duplicate.validators', 
>> 'ReferencesOlder')
>> +        env.config.set(config_name, 'duplicateof.label', 'Duplicate of')
>> +        env.config.set(config_name, 'duplicatedby.label', 'Duplicated by')
>> +        env.config.set(config_name, 'blocker', 'blockedby,blocks')
>> +        env.config.set(config_name, 'blockedby.blocks', 'true')
>> +
>> +        self.global_env = env
>> +        self._upgrade_mp(self.global_env)
>> +        self._setup_test_log(self.global_env)
>> +        self._load_product_from_data(self.global_env, self.default_product)
>> +        self.env = ProductEnvironment(self.global_env, self.default_product)
>> +
>> +        self.req = Mock(href=self.env.href, authname='anonymous', tz=utc,
>> +                        args=dict(action='dummy'),
>> +                        locale=locale_en, lc_time=locale_en)
>> +        self.req.perm = MockPerm()
>> +        self.relations_system = RelationsSystem(self.env)
>> +        self._upgrade_env()
>> +
>> +    def tearDown(self):
>> +        self.global_env.reset_db()
>> +
>> +    def _upgrade_env(self):
>> +        environment_setup = EnvironmentSetup(self.env)
>> +        try:
>> +            environment_setup.upgrade_environment(self.env.db_transaction)
>> +        except OperationalError:
>> +            # table remains but database version is deleted
>> +            pass
>> +
>> +    @classmethod
>> +    def _insert_ticket(cls, env, summary, **kw):
>> +        """Helper for inserting a ticket into the database"""
>> +        ticket = Ticket(env)
>> +        ticket["summary"] = summary
>> +        for k, v in kw.items():
>> +            ticket[k] = v
>> +        return ticket.insert()
>> +
>> +    def _insert_and_load_ticket(self, summary, **kw):
>> +        return Ticket(self.env, self._insert_ticket(self.env, summary, 
>> **kw))
>> +
>> +    def _insert_and_load_ticket_with_env(self, env, summary, **kw):
>> +        return Ticket(env, self._insert_ticket(env, summary, **kw))
>>
>> Modified: 
>> bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py 
>> (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/notification.py 
>> Tue Jul  9 09:19:00 2013
>> @@ -21,11 +21,11 @@ import unittest
>>  from trac.tests.notification import SMTPServerStore, SMTPThreadedServer
>>  from trac.ticket.tests.notification import (
>>      SMTP_TEST_PORT, smtp_address, parse_smtp_message)
>> +from bhrelations.tests.base import BaseRelationsTestCase
>>  from bhrelations.notification import RelationNotifyEmail
>> -from bhrelations.tests.api import BaseApiApiTestCase
>>
>>
>> -class NotificationTestCase(BaseApiApiTestCase):
>> +class NotificationTestCase(BaseRelationsTestCase):
>>      @classmethod
>>      def setUpClass(cls):
>>          cls.smtpd = CustomSMTPThreadedServer(SMTP_TEST_PORT)
>>
>> Modified: bloodhound/trunk/bloodhound_relations/bhrelations/tests/search.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/search.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/tests/search.py 
>> (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/search.py Tue 
>> Jul  9 09:19:00 2013
>> @@ -21,25 +21,25 @@ import shutil
>>  import tempfile
>>  import unittest
>>
>> -from bhrelations.tests.api import BaseApiApiTestCase
>>  from bhsearch.api import BloodhoundSearchApi
>>
>>  # TODO: Figure how to get trac to load components from these modules
>>  import bhsearch.query_parser, bhsearch.search_resources.ticket_search, \
>>      bhsearch.whoosh_backend
>>  import bhrelations.search
>> +from bhrelations.tests.base import BaseRelationsTestCase
>>
>>
>> -class SearchIntegrationTestCase(BaseApiApiTestCase):
>> +class SearchIntegrationTestCase(BaseRelationsTestCase):
>>      def setUp(self):
>> -        BaseApiApiTestCase.setUp(self, enabled=['bhsearch.*'])
>> +        BaseRelationsTestCase.setUp(self, enabled=['bhsearch.*'])
>>          self.global_env.path = tempfile.mkdtemp('bhrelations-tempenv')
>>          self.search_api = BloodhoundSearchApi(self.env)
>>          self.search_api.upgrade_environment(self.env.db_transaction)
>>
>>      def tearDown(self):
>>          shutil.rmtree(self.env.path)
>> -        BaseApiApiTestCase.tearDown(self)
>> +        BaseRelationsTestCase.tearDown(self)
>>
>>      def test_relations_are_indexed_on_creation(self):
>>          t1 = self._insert_and_load_ticket("Foo")
>>
>> Modified: 
>> bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py 
>> (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/validation.py 
>> Tue Jul  9 09:19:00 2013
>> @@ -20,10 +20,10 @@
>>  import unittest
>>
>>  from bhrelations.validation import Validator
>> -from bhrelations.tests.api import BaseApiApiTestCase
>> +from bhrelations.tests.base import BaseRelationsTestCase
>>
>>
>> -class GraphFunctionsTestCase(BaseApiApiTestCase):
>> +class GraphFunctionsTestCase(BaseRelationsTestCase):
>>      edges = [
>>          ('A', 'B', 'p'),  #      A    H
>>          ('A', 'C', 'p'),  #     /  \ /
>> @@ -35,7 +35,7 @@ class GraphFunctionsTestCase(BaseApiApiT
>>      ]
>>
>>      def setUp(self):
>> -        BaseApiApiTestCase.setUp(self)
>> +        BaseRelationsTestCase.setUp(self)
>>          # bhrelations point from destination to source
>>          for destination, source, type in self.edges:
>>              self.env.db_direct_transaction(
>>
>> Modified: bloodhound/trunk/bloodhound_relations/bhrelations/tests/web_ui.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/tests/web_ui.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/tests/web_ui.py 
>> (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/tests/web_ui.py Tue 
>> Jul  9 09:19:00 2013
>> @@ -18,27 +18,31 @@
>>  #  specific language governing permissions and limitations
>>  #  under the License.
>>  import unittest
>> -
>> +from bhrelations.api import ResourceIdSerializer
>>  from bhrelations.web_ui import RelationManagementModule
>> -from bhrelations.tests.api import BaseApiApiTestCase
>> +from bhrelations.tests.base import BaseRelationsTestCase
>> +
>> +from multiproduct.ticket.web_ui import TicketModule
>> +from trac.ticket import Ticket
>> +from trac.util.datefmt import to_utimestamp
>> +from trac.web import RequestDone
>>
>>
>> -class RelationManagementModuleTestCase(BaseApiApiTestCase):
>> +class RelationManagementModuleTestCase(BaseRelationsTestCase):
>>      def setUp(self):
>> -        BaseApiApiTestCase.setUp(self)
>> +        BaseRelationsTestCase.setUp(self)
>>          ticket_id = self._insert_ticket(self.env, "Foo")
>> -        args=dict(action='add', id=ticket_id, dest_tid='', reltype='', 
>> comment='')
>> -        self.req.method = 'GET',
>> +        self.req.method = 'POST'
>>          self.req.args['id'] = ticket_id
>>
>>      def test_can_process_empty_request(self):
>> +        self.req.method = 'GET'
>>          data = self.process_request()
>>
>>          self.assertSequenceEqual(data['relations'], [])
>>          self.assertEqual(len(data['reltypes']), 11)
>>
>>      def test_handles_missing_ticket_id(self):
>> -        self.req.method = "POST"
>>          self.req.args['add'] = 'add'
>>
>>          data = self.process_request()
>> @@ -46,8 +50,7 @@ class RelationManagementModuleTestCase(B
>>          self.assertIn("Invalid ticket", data["error"])
>>
>>      def test_handles_invalid_ticket_id(self):
>> -        self.req.method = "POST"
>> -        self.req.args['add'] = 'add'
>> +        self.req.args['add'] = True
>>          self.req.args['dest_tid'] = 'no such ticket'
>>
>>          data = self.process_request()
>> @@ -56,8 +59,7 @@ class RelationManagementModuleTestCase(B
>>
>>      def test_handles_missing_relation_type(self):
>>          t2 = self._insert_ticket(self.env, "Bar")
>> -        self.req.method = "POST"
>> -        self.req.args['add'] = 'add'
>> +        self.req.args['add'] = True
>>          self.req.args['dest_tid'] = str(t2)
>>
>>          data = self.process_request()
>> @@ -66,8 +68,7 @@ class RelationManagementModuleTestCase(B
>>
>>      def test_handles_invalid_relation_type(self):
>>          t2 = self._insert_ticket(self.env, "Bar")
>> -        self.req.method = "POST"
>> -        self.req.args['add'] = 'add'
>> +        self.req.args['add'] = True
>>          self.req.args['dest_tid'] = str(t2)
>>          self.req.args['reltype'] = 'no such relation'
>>
>> @@ -77,8 +78,7 @@ class RelationManagementModuleTestCase(B
>>
>>      def test_shows_relation_that_was_just_added(self):
>>          t2 = self._insert_ticket(self.env, "Bar")
>> -        self.req.method = "POST"
>> -        self.req.args['add'] = 'add'
>> +        self.req.args['add'] = True
>>          self.req.args['dest_tid'] = str(t2)
>>          self.req.args['reltype'] = 'dependson'
>>
>> @@ -92,6 +92,102 @@ class RelationManagementModuleTestCase(B
>>          return data
>>
>>
>> +class ResolveTicketIntegrationTestCase(BaseRelationsTestCase):
>> +    def setUp(self):
>> +        BaseRelationsTestCase.setUp(self)
>> +
>> +        self.mock_request()
>> +        self.configure()
>> +
>> +        self.req.redirect = self.redirect
>> +        self.redirect_url = None
>> +        self.redirect_permanent = None
>> +
>> +    def test_creates_duplicate_relation_from_duplicate_id(self):
>> +        t1 = self._insert_and_load_ticket("Foo")
>> +        t2 = self._insert_and_load_ticket("Bar")
>> +
>> +        self.assertRaises(RequestDone,
>> +                          self.resolve_as_duplicate,
>> +                          t2, self.get_id(t1))
>> +        relations = self.relations_system.get_relations(t2)
>> +        self.assertEqual(len(relations), 1)
>> +        relation = relations[0]
>> +        self.assertEqual(relation['destination_id'], self.get_id(t1))
>> +        self.assertEqual(relation['type'], 'duplicateof')
>> +
>> +    def test_prefills_duplicate_id_if_relation_exists(self):
>> +        t1 = self._insert_and_load_ticket("Foo")
>> +        t2 = self._insert_and_load_ticket("Bar")
>> +        self.relations_system.add(t2, t1, 'duplicateof')
>> +        self.req.args['id'] = t2.id
>> +        self.req.path_info = '/ticket/%d' % t2.id
>> +
>> +        data = self.process_request()
>> +
>> +        self.assertIn('ticket_duplicate_of', data)
>> +        t1id = ResourceIdSerializer.get_resource_id_from_instance(self.env, 
>> t1)
>> +        self.assertEqual(data['ticket_duplicate_of'], t1id)
>> +
>> +    def test_can_set_duplicate_resolution_even_if_relation_exists(self):
>> +        t1 = self._insert_and_load_ticket("Foo")
>> +        t2 = self._insert_and_load_ticket("Bar")
>> +        self.relations_system.add(t2, t1, 'duplicateof')
>> +
>> +        self.assertRaises(RequestDone,
>> +                          self.resolve_as_duplicate,
>> +                          t2, self.get_id(t1))
>> +        t2 = Ticket(self.env, t2.id)
>> +        self.assertEqual(t2['status'], 'closed')
>> +        self.assertEqual(t2['resolution'], 'duplicate')
>> +
>> +    def resolve_as_duplicate(self, ticket, duplicate_id):
>> +        self.req.method = 'POST'
>> +        self.req.path_info = '/ticket/%d' % ticket.id
>> +        self.req.args['id'] = ticket.id
>> +        self.req.args['action'] = 'resolve'
>> +        self.req.args['action_resolve_resolve_resolution'] = 'duplicate'
>> +        self.req.args['duplicate_id'] = duplicate_id
>> +        self.req.args['view_time'] = 
>> str(to_utimestamp(ticket['changetime']))
>> +        self.req.args['submit'] = True
>> +
>> +        return self.process_request()
>> +
>> +    def process_request(self):
>> +        template, data, content_type = \
>> +            TicketModule(self.env).process_request(self.req)
>> +        template, data, content_type = \
>> +            RelationManagementModule(self.env).post_process_request(
>> +                self.req, template, data, content_type)
>> +        return data
>> +
>> +    def mock_request(self):
>> +        self.req.method = 'GET'
>> +        self.req.get_header = lambda x: None
>> +        self.req.authname = 'x'
>> +        self.req.session = {}
>> +        self.req.chrome = {'warnings': []}
>> +        self.req.form_token = ''
>> +
>> +    def configure(self):
>> +        config = self.env.config
>> +        config['ticket-workflow'].set('resolve', 'new -> closed')
>> +        config['ticket-workflow'].set('resolve.operations', 
>> 'set_resolution')
>> +        config['ticket-workflow'].set('resolve.permissions', 
>> 'TICKET_MODIFY')
>> +        with self.env.db_transaction as db:
>> +            db("INSERT INTO enum VALUES "
>> +               "('resolution', 'duplicate', 'duplicate')")
>> +
>> +    def redirect(self, url, permanent=False):
>> +        self.redirect_url = url
>> +        self.redirect_permanent = permanent
>> +        raise RequestDone
>> +
>> +    def get_id(self, ticket):
>> +        return ResourceIdSerializer.get_resource_id_from_instance(self.env,
>> +                                                                  ticket)
>> +
>> +
>>  def suite():
>>      test_suite = unittest.TestSuite()
>>      test_suite.addTest(unittest.makeSuite(RelationManagementModuleTestCase, 
>> 'test'))
>>
>> Added: bloodhound/trunk/bloodhound_relations/bhrelations/utils.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/utils.py?rev=1501152&view=auto
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/utils.py (added)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/utils.py Tue Jul  9 
>> 09:19:00 2013
>> @@ -0,0 +1,28 @@
>> +#!/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.
>> +
>> +
>> +# Copied from trac/utils.py, ticket-links-trunk branch
>> +def unique(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/web_ui.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/web_ui.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/web_ui.py (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/web_ui.py Tue Jul  9 
>> 09:19:00 2013
>> @@ -28,22 +28,20 @@ import re
>>  import pkg_resources
>>
>>  from trac.core import Component, implements, TracError
>> -from trac.resource import get_resource_url, ResourceNotFound, Resource
>> +from trac.resource import get_resource_url, Resource
>>  from trac.ticket.model import Ticket
>>  from trac.util.translation import _
>> -from trac.web import IRequestHandler
>> +from trac.web import IRequestHandler, IRequestFilter
>>  from trac.web.chrome import ITemplateProvider, add_warning
>>
>>  from bhrelations.api import RelationsSystem, ResourceIdSerializer, \
>> -    TicketRelationsSpecifics, UnknownRelationType
>> +    TicketRelationsSpecifics, UnknownRelationType, NoSuchTicketError
>>  from bhrelations.model import Relation
>>  from bhrelations.validation import ValidationError
>>
>> -from multiproduct.model import Product
>> -from multiproduct.env import ProductEnvironment
>>
>>  class RelationManagementModule(Component):
>> -    implements(IRequestHandler, ITemplateProvider)
>> +    implements(IRequestFilter, IRequestHandler, ITemplateProvider)
>>
>>      # IRequestHandler methods
>>      def match_request(self, req):
>> @@ -88,22 +86,27 @@ class RelationManagementModule(Component
>>                      comment=req.args.get('comment', ''),
>>                  )
>>                  try:
>> -                    dest_ticket = self.find_ticket(relation['destination'])
>> -                    req.perm.require('TICKET_MODIFY',
>> -                                     Resource(dest_ticket.id))
>> -                    relsys.add(ticket, dest_ticket,
>> -                               relation['type'],
>> -                               relation['comment'],
>> -                               req.authname)
>> +                    trs = TicketRelationsSpecifics(self.env)
>> +                    dest_ticket = trs.find_ticket(relation['destination'])
>>                  except NoSuchTicketError:
>> -                    data['error'] = _('Invalid ticket id.')
>> -                except UnknownRelationType:
>> -                    data['error'] = _('Unknown relation type.')
>> -                except ValidationError as ex:
>> -                    data['error'] = ex.message
>> +                    data['error'] = _('Invalid ticket ID.')
>> +                else:
>> +                    req.perm.require('TICKET_MODIFY', 
>> Resource(dest_ticket.id))
>> +
>> +                    try:
>> +                        relsys.add(ticket, dest_ticket,
>> +                            relation['type'],
>> +                            relation['comment'],
>> +                            req.authname)
>> +                    except NoSuchTicketError:
>> +                        data['error'] = _('Invalid ticket ID.')
>> +                    except UnknownRelationType:
>> +                        data['error'] = _('Unknown relation type.')
>> +                    except ValidationError as ex:
>> +                        data['error'] = ex.message
>> +
>>                  if 'error' in data:
>>                      data['relation'] = relation
>> -
>>              else:
>>                  raise TracError(_('Invalid operation.'))
>>
>> @@ -123,6 +126,25 @@ class RelationManagementModule(Component
>>          resource_filename = pkg_resources.resource_filename
>>          return [resource_filename('bhrelations', 'templates'), ]
>>
>> +    # IRequestFilter methods
>> +    def pre_process_request(self, req, handler):
>> +        return handler
>> +
>> +    def post_process_request(self, req, template, data, content_type):
>> +        if 'ticket' in data:
>> +            ticket = data['ticket']
>> +            rls = RelationsSystem(self.env)
>> +            resid = ResourceIdSerializer.get_resource_id_from_instance(
>> +                self.env, ticket)
>> +
>> +            if rls.duplicate_relation_type:
>> +                duplicate_relations = \
>> +                    rls._select_relations(resid, 
>> rls.duplicate_relation_type)
>> +                if duplicate_relations:
>> +                    data['ticket_duplicate_of'] = \
>> +                        duplicate_relations[0].destination
>> +        return template, data, content_type
>> +
>>      # utility functions
>>      def get_ticket_relations(self, ticket):
>>          grouped_relations = {}
>> @@ -136,36 +158,6 @@ class RelationManagementModule(Component
>>              grouped_relations.setdefault(reltypes[r['type']], []).append(r)
>>          return grouped_relations
>>
>> -    def find_ticket(self, ticket_spec):
>> -        ticket = None
>> -        m = re.match(r'#?(?P<tid>\d+)', ticket_spec)
>> -        if m:
>> -            tid = m.group('tid')
>> -            try:
>> -                ticket = Ticket(self.env, tid)
>> -            except ResourceNotFound:
>> -                # ticket not found in current product, try all other 
>> products
>> -                for p in Product.select(self.env):
>> -                    if p.prefix != self.env.product.prefix:
>> -                        # TODO: check for PRODUCT_VIEW permissions
>> -                        penv = ProductEnvironment(self.env.parent, p.prefix)
>> -                        try:
>> -                            ticket = Ticket(penv, tid)
>> -                        except ResourceNotFound:
>> -                            pass
>> -                        else:
>> -                            break
>> -
>> -        # ticket still not found, use fallback for <prefix>:ticket:<id> 
>> syntax
>> -        if ticket is None:
>> -            trs = TicketRelationsSpecifics(self.env)
>> -            try:
>> -                resource = ResourceIdSerializer.get_resource_by_id(tid)
>> -                ticket = trs._create_ticket_by_full_id(resource)
>> -            except:
>> -                raise NoSuchTicketError
>> -        return ticket
>> -
>>      def remove_relations(self, req, rellist):
>>          relsys = RelationsSystem(self.env)
>>          for relid in rellist:
>> @@ -177,7 +169,3 @@ class RelationManagementModule(Component
>>              else:
>>                  add_warning(req,
>>                      _('Not enough permissions to remove relation "%s"' % 
>> relid))
>> -
>> -
>> -class NoSuchTicketError(ValueError):
>> -    pass
>>
>> Modified: 
>> bloodhound/trunk/bloodhound_relations/bhrelations/widgets/relations.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_relations/bhrelations/widgets/relations.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_relations/bhrelations/widgets/relations.py 
>> (original)
>> +++ bloodhound/trunk/bloodhound_relations/bhrelations/widgets/relations.py 
>> Tue Jul  9 09:19:00 2013
>> @@ -69,7 +69,7 @@ class TicketRelationsWidget(WidgetBase):
>>                  
>> RelationManagementModule(self.env).get_ticket_relations(ticket),
>>          }
>>          return 'widget_relations.html', \
>> -            { 'title': title, 'data': data, }, context
>> +            {'title': title, 'data': data, }, context
>>
>>      render_widget = pretty_wrapper(render_widget, check_widget_name)
>>
>>
>> Modified: bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css 
>> (original)
>> +++ bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css Tue Jul  
>> 9 09:19:00 2013
>> @@ -174,6 +174,11 @@ div.reports form {
>>   text-align: right;
>>  }
>>
>> +#duplicate_id {
>> +  margin-left: 10px;
>> +  margin-right: 10px;
>> +}
>> +
>>  #trac-ticket-title {
>>   margin-bottom: 5px;
>>  }
>>
>> Modified: bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket.html
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket.html?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket.html 
>> (original)
>> +++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket.html Tue 
>> Jul  9 09:19:00 2013
>> @@ -160,6 +160,10 @@
>>          }
>>
>>          function install_workflow(){
>> +          <py:if test="bhrelations">
>> +            var act = $('#action_resolve_resolve_resolution').parent();
>> +            act.append('<span id="duplicate_id" class="hide">Duplicate 
>> ID:&nbsp;<input name="duplicate_id" type="text" class="input-mini" 
>> value="${ticket_duplicate_of}"></input></span>');
>> +          </py:if>
>>            var actions_box = $('#workflow-actions')
>>                .click(function(e) { e.stopPropagation(); });
>>            $('#action').children('div').each(function() {
>> @@ -180,7 +184,17 @@
>>                  else if (newowner)
>>                    newlabel = newlabel + ' to ' + newowner;
>>                  else if (newresolution)
>> +                {
>>                    newlabel = newlabel + ' as ' + newresolution;
>> +                  if (newresolution === 'duplicate')
>> +                  {
>> +                    $('#duplicate_id').show();
>> +                  }
>> +                  else
>> +                  {
>> +                    $('#duplicate_id').hide();
>> +                  }
>> +                }
>>                  $('#submit-action-label').text(newlabel);
>>
>>                  // Enable | disable action controls
>>
>> Modified: bloodhound/trunk/installer/bloodhound_setup.py
>> URL: 
>> http://svn.apache.org/viewvc/bloodhound/trunk/installer/bloodhound_setup.py?rev=1501152&r1=1501151&r2=1501152&view=diff
>> ==============================================================================
>> --- bloodhound/trunk/installer/bloodhound_setup.py (original)
>> +++ bloodhound/trunk/installer/bloodhound_setup.py Tue Jul  9 09:19:00 2013
>> @@ -93,6 +93,8 @@ BASE_CONFIG = {'components': {'bhtheme.*
>>                     'global_validators':
>>                         'NoSelfReferenceValidator,ExclusiveValidator,'
>>                         'BlockerValidator',
>> +                   'duplicate_relation':
>> +                        'duplicateof',
>>                 },
>>                 'bhrelations_links': {
>>                      'children.label': 'Child',
>>
>>

Reply via email to