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