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