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