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