You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@bloodhound.apache.org by Anze Staric <an...@gmail.com> on 2013/07/09 11:36:01 UTC

Re: svn commit: r1501152 - in /bloodhound/trunk: bloodhound_relations/bhrelations/ bloodhound_relations/bhrelations/tests/ bloodhound_relations/bhrelations/widgets/ bloodhound_theme/bhtheme/htdocs/ bloodhound_theme/bhtheme/templates/ installer/

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,  <as...@apache.org> 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',
>
>

Re: svn commit: r1501152 - in /bloodhound/trunk: bloodhound_relations/bhrelations/ bloodhound_relations/bhrelations/tests/ bloodhound_relations/bhrelations/widgets/ bloodhound_theme/bhtheme/htdocs/ bloodhound_theme/bhtheme/templates/ installer/

Posted by Anze Staric <an...@gmail.com>.
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 <an...@gmail.com> 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,  <as...@apache.org> 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',
>>
>>

Re: svn commit: r1501152 - in /bloodhound/trunk: bloodhound_relations/bhrelations/ bloodhound_relations/bhrelations/tests/ bloodhound_relations/bhrelations/widgets/ bloodhound_theme/bhtheme/htdocs/ bloodhound_theme/bhtheme/templates/ installer/

Posted by Branko Čibej <br...@wandisco.com>.
On 09.07.2013 11:54, Gary Martin wrote:
> Looks like I am basically right although I seem to remember that the
> eu svn is only used for read operations so things should not be all
> that bad.

Anže's report is precisely the symptom of an update being run just after
a commit, and before the EU mirror has had a chance to synchronize with
the master server. This happens sometimes. Waiting a few minutes and
retrying the update should work. If not, report it to infra@apache.org.

-- Brane


-- 
Branko Čibej | Director of Subversion
WANdisco // Non-Stop Data
e. brane@wandisco.com

Re: svn commit: r1501152 - in /bloodhound/trunk: bloodhound_relations/bhrelations/ bloodhound_relations/bhrelations/tests/ bloodhound_relations/bhrelations/widgets/ bloodhound_theme/bhtheme/htdocs/ bloodhound_theme/bhtheme/templates/ installer/

Posted by Gary Martin <ga...@wandisco.com>.
Looks like I am basically right although I seem to remember that the eu 
svn is only used for read operations so things should not be all that bad.

Cheers,
     Gary

On 09/07/13 10:47, Gary Martin wrote:
> Yes, that is what I find too. Viewvc states that as the latest 
> revision as well. I wonder if it is something like the eu mirror being 
> out of sync.
>
> Cheers,
> Gary
>
> On 09/07/13 10:43, Matevž Bradač wrote:
>> It seems we're experiencing some SVN issues, 'svn update' says
>>    'At revision 1501136.'
>>
>> Were there any changes done on the server side?
>>
>> -- 
>> matevz
>>
>> On 9. Jul, 2013, at 11:36, Anze Staric 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,  <as...@apache.org> 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
>>>>
>


Re: svn commit: r1501152 - in /bloodhound/trunk: bloodhound_relations/bhrelations/ bloodhound_relations/bhrelations/tests/ bloodhound_relations/bhrelations/widgets/ bloodhound_theme/bhtheme/htdocs/ bloodhound_theme/bhtheme/templates/ installer/

Posted by Gary Martin <ga...@wandisco.com>.
Yes, that is what I find too. Viewvc states that as the latest revision 
as well. I wonder if it is something like the eu mirror being out of sync.

Cheers,
Gary

On 09/07/13 10:43, Matevž Bradač wrote:
> It seems we're experiencing some SVN issues, 'svn update' says
>    'At revision 1501136.'
>
> Were there any changes done on the server side?
>
> --
> matevz
>
> On 9. Jul, 2013, at 11:36, Anze Staric 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,  <as...@apache.org> 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
>>>


Re: svn commit: r1501152 - in /bloodhound/trunk: bloodhound_relations/bhrelations/ bloodhound_relations/bhrelations/tests/ bloodhound_relations/bhrelations/widgets/ bloodhound_theme/bhtheme/htdocs/ bloodhound_theme/bhtheme/templates/ installer/

Posted by Matevž Bradač <ma...@digiverse.si>.
It seems we're experiencing some SVN issues, 'svn update' says
  'At revision 1501136.'

Were there any changes done on the server side?

--
matevz

On 9. Jul, 2013, at 11:36, Anze Staric 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,  <as...@apache.org> 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',
>> 
>>