Author: peter
Date: Fri Mar 29 13:23:37 2013
New Revision: 1462468
URL: http://svn.apache.org/r1462468
Log:
#476 Integrate bhsearch into multi-product
Added:
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/core.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/core.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/utils.py
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py
Added:
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/core.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/core.py?rev=1462468&view=auto
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/core.py
(added)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/core.py
Fri Mar 29 13:23:37 2013
@@ -0,0 +1,49 @@
+#!/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.
+
+from trac.core import ComponentMeta, ExtensionPoint
+
+
+class MultiProductExtensionPoint(ExtensionPoint):
+ """Marker class for multiproduct extension points in components."""
+
+ def extensions(self, component):
+ """Return a multiproduct aware list of components that declare to
+ implement the extension point interface.
+
+ When accessed in product environment, only components for that
+ environment are returned.
+
+ When accessed in global environment, a separate instance will be
+ returned for global and each of the product environments.
+ """
+ compmgr = component.compmgr
+ if not hasattr(compmgr, 'parent') or compmgr.parent is not None:
+ return \
+ super(MultiProductExtensionPoint, self).extensions(component)
+
+ classes = ComponentMeta._registry.get(self.interface, ())
+ components = [component.compmgr[cls] for cls in classes]
+ components += [
+ env[cls]
+ for cls in classes
+ for env in component.compmgr.all_product_envs()
+ ]
+ return [c for c in components if c]
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
Fri Mar 29 13:23:37 2013
@@ -128,7 +128,7 @@ class Environment(trac.env.Environment):
def db_direct_transaction(self):
return ProductEnvContextManager(super(Environment,
self).db_transaction)
- def _all_product_envs(self):
+ def all_product_envs(self):
return [ProductEnvironment(self, product) for product in
Product.select(self)]
def needs_upgrade(self):
@@ -153,7 +153,7 @@ class Environment(trac.env.Environment):
# until schema is multi product aware, product environments can't (and
shouldn't) be
# instantiated
if self._multiproduct_schema_enabled:
- product_envs = [self] + self._all_product_envs()
+ product_envs = [self] + self.all_product_envs()
if needs_upgrade_in_env_list(product_envs,
self._product_setup_participants):
return True
return False
@@ -185,7 +185,7 @@ class Environment(trac.env.Environment):
return upgraders
def upgraders_for_product_envs():
- product_envs = [self] + self._all_product_envs()
+ product_envs = [self] + self.all_product_envs()
return upgraders_for_env_list(product_envs,
self._product_setup_participants)
# first enumerate components that are multi product aware and require
upgrade
@@ -303,6 +303,9 @@ class EnvironmentStub(trac.test.Environm
# FIXME: Shall we ?
#env.config.save()
+ def all_product_envs(self):
+ return []
+
def reset_db(self, default_data=None):
multiproduct_schema = self._multiproduct_schema_enabled
self._multiproduct_schema_enabled = False
Added:
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/core.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/core.py?rev=1462468&view=auto
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/core.py
(added)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/core.py
Fri Mar 29 13:23:37 2013
@@ -0,0 +1,94 @@
+# 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.
+
+import unittest
+
+from trac.core import Interface, implements, Component
+
+from multiproduct.core import MultiProductExtensionPoint
+
+class MultiProductExtensionPointTestCase(unittest.TestCase):
+ def setUp(self):
+ from trac.core import ComponentManager, ComponentMeta
+ self.compmgr = ComponentManager()
+
+ # Make sure we have no external components hanging around in the
+ # component registry
+ self.old_registry = ComponentMeta._registry
+ ComponentMeta._registry = {}
+
+ def tearDown(self):
+ # Restore the original component registry
+ from trac.core import ComponentMeta
+ ComponentMeta._registry = self.old_registry
+
+ def test_with_trac_component_manager(self):
+ """No parent attribute, no _all_product_envs method"""
+ class ComponentA(Component):
+ implements(ITest)
+
+ class ComponentB(Component):
+ mp_extension_point = MultiProductExtensionPoint(ITest)
+
+ components = ComponentB(self.compmgr).mp_extension_point
+ self.assertEqual(len(components), 1)
+ for c in components:
+ self.assertIsInstance(c, ComponentA)
+
+ def test_with_global_product_component_manager(self):
+ self.compmgr.parent = None
+ self.compmgr.all_product_envs = lambda: [self.compmgr, self.compmgr]
+
+ class ComponentA(Component):
+ implements(ITest)
+
+ class ComponentB(Component):
+ mp_extension_point = MultiProductExtensionPoint(ITest)
+
+ components = ComponentB(self.compmgr).mp_extension_point
+ self.assertEqual(len(components), 3)
+ for c in components:
+ self.assertIsInstance(c, ComponentA)
+
+ def test_with_product_component_manager(self):
+ self.compmgr.parent = self
+ self.compmgr.all_product_envs = lambda: [self.compmgr, self.compmgr]
+
+ class ComponentA(Component):
+ implements(ITest)
+
+ class ComponentB(Component):
+ mp_extension_point = MultiProductExtensionPoint(ITest)
+
+ components = ComponentB(self.compmgr).mp_extension_point
+ self.assertEqual(len(components), 1)
+ for c in components:
+ self.assertIsInstance(c, ComponentA)
+
+
+class ITest(Interface):
+ def test():
+ """Dummy function."""
+
+
+def test_suite():
+ return unittest.TestSuite([
+ unittest.makeSuite(MultiProductExtensionPointTestCase, 'test'),
+ ])
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
--- bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
(original)
+++ bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
Fri Mar 29 13:23:37 2013
@@ -23,6 +23,8 @@ from trac.config import ExtensionOption
from trac.core import (Interface, Component, ExtensionPoint, TracError,
implements)
from trac.env import IEnvironmentSetupParticipant
+from multiproduct.api import ISupportMultiProductEnvironment
+from multiproduct.core import MultiProductExtensionPoint
ASC = "asc"
DESC = "desc"
@@ -35,6 +37,7 @@ class IndexFields(object):
AUTHOR = 'author'
CONTENT = 'content'
STATUS = 'status'
+ PRODUCT = 'product'
class QueryResult(object):
def __init__(self):
@@ -94,7 +97,7 @@ class ISearchBackend(Interface):
Called when new document instance must be added
"""
- def delete_doc(doc_type, doc_id, operation_context):
+ def delete_doc(product, doc_type, doc_id, operation_context):
"""
Delete document from index
"""
@@ -226,7 +229,7 @@ class BloodhoundSearchApi(Component):
"""Implements core indexing functionality, provides methods for
searching, adding and deleting documents from index.
"""
- implements(IEnvironmentSetupParticipant)
+ implements(IEnvironmentSetupParticipant, ISupportMultiProductEnvironment)
backend = ExtensionOption('bhsearch', 'search_backend',
ISearchBackend, 'WhooshBackend',
@@ -242,7 +245,7 @@ class BloodhoundSearchApi(Component):
result_post_processors = ExtensionPoint(IResultPostprocessor)
query_processors = ExtensionPoint(IQueryPreprocessor)
- index_participants = ExtensionPoint(IIndexParticipant)
+ index_participants = MultiProductExtensionPoint(IIndexParticipant)
def query(
self,
@@ -335,6 +338,7 @@ class BloodhoundSearchApi(Component):
def _change_doc_id(self, doc, old_id, operation_context):
self.backend.delete_doc(
+ doc[IndexFields.PRODUCT],
doc[IndexFields.TYPE],
old_id,
operation_context
@@ -354,13 +358,10 @@ class BloodhoundSearchApi(Component):
preprocessor.pre_process(doc)
self.backend.add_doc(doc, operation_context)
-
- def delete_doc(self, doc_type, doc_id):
- """Add a document from underlying search backend.
-
- The doc must be dictionary with obligatory "type" field
+ def delete_doc(self, product, doc_type, doc_id):
+ """Delete the document from underlying search backend.
"""
- self.backend.delete_doc(doc_type, doc_id)
+ self.backend.delete_doc(product, doc_type, doc_id)
# IEnvironmentSetupParticipant methods
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py
Fri Mar 29 13:23:37 2013
@@ -24,10 +24,12 @@ from bhsearch.api import (IIndexParticip
ISearchParticipant)
from bhsearch.search_resources.base import BaseIndexer, BaseSearchParticipant
from bhsearch.search_resources.ticket_search import TicketIndexer
+from bhsearch.utils import get_product
from trac.ticket import Milestone
from trac.config import ListOption, Option
from trac.core import implements
from trac.resource import IResourceChangeListener
+from genshi.builder import tag
MILESTONE_TYPE = u"milestone"
@@ -65,7 +67,8 @@ class MilestoneIndexer(BaseIndexer):
# pylint: disable=unused-argument
try:
search_api = BloodhoundSearchApi(self.env)
- search_api.delete_doc(MILESTONE_TYPE, resource.name)
+ search_api.delete_doc(
+ get_product(self.env).prefix, MILESTONE_TYPE, resource.name)
except Exception, e:
if self.silence_on_error:
self.log.error("Error occurs during milestone indexing. \
@@ -113,6 +116,7 @@ class MilestoneIndexer(BaseIndexer):
IndexFields.ID: milestone.name,
IndexFields.TYPE: MILESTONE_TYPE,
IndexFields.STATUS: status,
+ IndexFields.PRODUCT: get_product(self.env).prefix,
}
for field, index_field in self.optional_fields.iteritems():
@@ -161,4 +165,6 @@ class MilestoneSearchParticipant(BaseSea
def format_search_results(self, res):
#TODO: add better milestone rendering
- return u'Milestone: %s' % res['id']
+
+ id = res['hilited_id'] or res['id']
+ return tag(u'[', res['product'], u'] Milestone:', id)
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py
Fri Mar 29 13:23:37 2013
@@ -23,6 +23,7 @@ from bhsearch import BHSEARCH_CONFIG_SEC
from bhsearch.api import (ISearchParticipant, BloodhoundSearchApi,
IIndexParticipant, IndexFields)
from bhsearch.search_resources.base import BaseIndexer, BaseSearchParticipant
+from bhsearch.utils import get_product
from genshi.builder import tag
from trac.ticket.api import TicketSystem
from trac.ticket import Ticket
@@ -112,7 +113,7 @@ class TicketIndexer(BaseIndexer):
"""Called when a ticket is deleted."""
try:
search_api = BloodhoundSearchApi(self.env)
- search_api.delete_doc(TICKET_TYPE, ticket.id)
+ search_api.delete_doc(ticket.product, TICKET_TYPE, ticket.id)
except Exception, e:
if self.silence_on_error:
self.log.error("Error occurs during deleting ticket. \
@@ -143,8 +144,7 @@ class TicketIndexer(BaseIndexer):
for row in self.env.db_query(sql, args):
yield int(row[0])
- def _index_ticket(
- self, ticket, search_api = None, operation_context = None):
+ def _index_ticket(self, ticket, search_api=None, operation_context=None):
try:
if not search_api:
search_api = BloodhoundSearchApi(self.env)
@@ -164,7 +164,10 @@ class TicketIndexer(BaseIndexer):
IndexFields.ID: str(ticket.id),
IndexFields.TYPE: TICKET_TYPE,
IndexFields.TIME: ticket.time_changed,
- }
+ IndexFields.PRODUCT: get_product(self.env).prefix,
+ }
+ # TODO: Add support for moving tickets between products.
+
for field, index_field in self.optional_fields.iteritems():
if field in ticket.values:
@@ -241,5 +244,5 @@ class TicketSearchParticipant(BaseSearch
id = res['hilited_id'] or res['id']
id = tag.span('#', id, class_=css_class)
summary = res['hilited_summary'] or res['summary']
- return tag(id, ': ', summary, ' (%s)' % stat)
+ return tag('[', res['product'], '] ', id, ': ', summary, ' (%s)' %
stat)
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py
Fri Mar 29 13:23:37 2013
@@ -23,9 +23,11 @@ from bhsearch import BHSEARCH_CONFIG_SEC
from bhsearch.api import (ISearchParticipant, BloodhoundSearchApi,
IIndexParticipant, IndexFields)
from bhsearch.search_resources.base import BaseIndexer, BaseSearchParticipant
+from bhsearch.utils import get_product
from trac.core import implements
from trac.config import ListOption, Option
from trac.wiki import IWikiChangeListener, WikiSystem, WikiPage
+from genshi.builder import tag
WIKI_TYPE = u"wiki"
@@ -47,7 +49,8 @@ class WikiIndexer(BaseIndexer):
"""Called when a ticket is deleted."""
try:
search_api = BloodhoundSearchApi(self.env)
- search_api.delete_doc(WIKI_TYPE, page.name)
+ search_api.delete_doc(
+ get_product(self.env).prefix, WIKI_TYPE, page.name)
except Exception, e:
if self.silence_on_error.lower() == "true":
self.log.error("Error occurs during wiki indexing. \
@@ -100,6 +103,7 @@ class WikiIndexer(BaseIndexer):
IndexFields.TIME: page.time,
IndexFields.AUTHOR: page.author,
IndexFields.CONTENT: self.wiki_formatter.format(page.text),
+ IndexFields.PRODUCT: get_product(self.env).prefix,
}
return doc
@@ -148,7 +152,7 @@ class WikiSearchParticipant(BaseSearchPa
def format_search_results(self, res):
title = res['hilited_id'] or res['id']
- return title
+ return tag('[', res['product'], '] ', title)
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py
Fri Mar 29 13:23:37 2013
@@ -19,6 +19,9 @@
# under the License.
import unittest
import shutil
+
+from trac.test import Mock
+
from bhsearch.api import BloodhoundSearchApi, ASC, SortInstruction
from bhsearch.query_parser import DefaultQueryParser
from bhsearch.tests.base import BaseBloodhoundSearchTest
@@ -121,6 +124,16 @@ class ApiQueryWithWhooshTestCase(BaseBlo
self.assertEqual(2, results.hits)
+ def test_can_index_wiki_with_same_id_from_different_products(self):
+ self.env.product = Mock(prefix='p1')
+ self.insert_wiki('title', 'content')
+ self.env.product = Mock(prefix='p2')
+ self.insert_wiki('title', 'content 2')
+
+ results = self.search_api.query("type:wiki")
+
+ self.assertEqual(results.hits, 2)
+
#TODO: check this later
# @unittest.skip("Check with Whoosh community")
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py
Fri Mar 29 13:23:37 2013
@@ -26,6 +26,7 @@ from bhsearch.tests.base import BaseBloo
from bhsearch.whoosh_backend import WhooshBackend
from trac.ticket import Milestone
+from trac.test import Mock
class MilestoneIndexerEventsTestCase(BaseBloodhoundSearchTest):
@@ -172,6 +173,25 @@ class MilestoneIndexerEventsTestCase(Bas
self.assertEqual(RETARGET_MILESTONE, results.docs[0]["milestone"])
self.assertEqual(RETARGET_MILESTONE, results.docs[1]["milestone"])
+ def test_fills_product_field_if_product_is_set(self):
+ self.env.product = Mock(prefix="p")
+
+ self.insert_milestone("T1")
+
+ results = self.search_api.query("*")
+ self.assertEqual(results.docs[0]["product"], 'p')
+
+ def test_can_work_if_env_does_not_have_product(self):
+ if 'product' in self.env:
+ del self.env["product"]
+
+ self.insert_milestone("T1")
+
+ results = self.search_api.query("*")
+ self.assertEqual(results.hits, 1)
+ self.assertNotIn("product", results.docs[0])
+
+
class MilestoneSearchParticipantTestCase(BaseBloodhoundSearchTest):
def setUp(self):
super(MilestoneSearchParticipantTestCase, self).setUp()
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py
Fri Mar 29 13:23:37 2013
@@ -24,6 +24,7 @@ from bhsearch.whoosh_backend import Whoo
from bhsearch.tests.base import BaseBloodhoundSearchTest
from bhsearch.search_resources.ticket_search import TicketIndexer
from trac.ticket.model import Component
+from trac.test import Mock
class TicketIndexerTestCase(BaseBloodhoundSearchTest):
def setUp(self):
@@ -85,6 +86,24 @@ class TicketIndexerTestCase(BaseBloodhou
self.print_result(results)
self.assertEqual(CHANGED_SUMMARY, results.docs[0]["summary"])
+ def test_fills_product_field_if_product_is_set(self):
+ self.env.product = Mock(prefix="p")
+
+ self.insert_ticket("T1")
+
+ results = self.search_api.query("*")
+ self.assertEqual(results.docs[0]["product"], 'p')
+
+ def test_can_work_if_env_does_not_have_product(self):
+ if 'product' in self.env:
+ del self.env["product"]
+
+ self.insert_ticket("T1")
+
+ results = self.search_api.query("*")
+ self.assertEqual(results.hits, 1)
+ self.assertNotIn("product", results.docs[0])
+
def _insert_component(self, name):
component = Component(self.env)
component.name = name
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py
Fri Mar 29 13:23:37 2013
@@ -27,6 +27,7 @@ from bhsearch.whoosh_backend import Whoo
from bhsearch.search_resources.wiki_search import (
WikiIndexer, WikiSearchParticipant)
+from trac.test import Mock
from trac.wiki import WikiSystem, WikiPage
@@ -138,6 +139,25 @@ class WikiIndexerEventsTestCase(BaseBloo
self.assertEqual(1, results.hits)
self.assertEqual("Header", results.docs[0]["content"])
+ def test_fills_product_field_if_product_is_set(self):
+ self.env.product = Mock(prefix="p")
+
+ self.insert_wiki(self.DUMMY_PAGE_NAME, "content")
+
+ results = self.search_api.query("*")
+ self.assertEqual(results.docs[0]["product"], 'p')
+
+ def test_can_work_if_env_does_not_have_product(self):
+ if 'product' in self.env:
+ del self.env["product"]
+
+ self.insert_wiki(self.DUMMY_PAGE_NAME, "content")
+
+ results = self.search_api.query("*")
+ self.assertEqual(results.hits, 1)
+ self.assertNotIn("product", results.docs[0])
+
+
def suite():
test_suite = unittest.TestSuite()
test_suite.addTest(unittest.makeSuite(
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
Fri Mar 29 13:23:37 2013
@@ -20,8 +20,9 @@
import unittest
from urllib import urlencode, unquote, unquote_plus
-from bhsearch.api import ASC, DESC, SortInstruction
+from bhsearch import web_ui
+from bhsearch.api import ASC, DESC, SortInstruction
from bhsearch.tests.base import BaseBloodhoundSearchTest
from bhsearch.web_ui import RequestParameters, BloodhoundSearchModule
from bhsearch.whoosh_backend import WhooshBackend
@@ -51,6 +52,23 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
self.redirect_url = None
self.redirect_permanent = None
+ self.old_product_environment = web_ui.ProductEnvironment
+ self._inject_mocked_product_environment()
+
+ def _inject_mocked_product_environment(self):
+ class MockProductEnvironment(object):
+ def __init__(self, env, product):
+ # pylint: disable=unused-argument
+ self.product = product
+
+ def href(self, *args):
+ return ('/main/products/%s/' % self.product) + '/'.join(args)
+
+ web_ui.ProductEnvironment = MockProductEnvironment
+
+ def tearDown(self):
+ web_ui.ProductEnvironment = self.old_product_environment
+
def redirect(self, url, permanent=False):
self.redirect_url = url
self.redirect_permanent = permanent
@@ -114,6 +132,15 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
self.assertEqual(1, len(docs))
self.assertEqual("/main/ticket/1", docs[0]["href"])
+ def test_product_ticket_href(self):
+ self.env.product = Mock(prefix='xxx')
+ self._insert_tickets(1)
+ self.req.args[RequestParameters.QUERY] = "*:*"
+ data = self.process_request()
+ docs = data["results"].items
+ self.assertEqual(1, len(docs))
+ self.assertEqual("/main/products/xxx/ticket/1", docs[0]["href"])
+
def test_page_href(self):
self._insert_tickets(DEFAULT_DOCS_PER_PAGE+1)
self.req.args[RequestParameters.QUERY] = "*:*"
@@ -167,7 +194,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
self.req.args[RequestParameters.QUERY] = "*"
data = self.process_request()
#assert
- self.assertEquals(1, len(data["facet_counts"]))
+ self.assertEquals(2, len(data["facet_counts"]))
def test_can_return_facets_counts_for_tickets(self):
#arrange
@@ -651,7 +678,8 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
self.req.args['changeset'] = 'on'
self.assertRaises(RequestDone, self.process_request)
redirect_url = unquote_plus(self.redirect_url)
- self.assertIn('fq=type:(ticket OR wiki OR milestone OR changeset)',
redirect_url)
+ self.assertIn(
+ 'fq=type:(ticket OR wiki OR milestone OR changeset)', redirect_url)
self.assertIn('/bhsearch', self.redirect_url)
self.assertEqual(self.redirect_permanent, True)
@@ -766,6 +794,17 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
self.assertNotIn("&q=", unquote(active_query["href"]))
self.assertIn("fq=", unquote(active_query["href"]))
+ def test_redirects_if_product_url_is_used_to_access_search(self):
+ self.req.path_info = '/products/xxx/bhsearch'
+ self.req.args['productid'] = 'xxx'
+
+ self.assertRaises(RequestDone, self.process_request)
+
+ self.assertIn('/bhsearch', self.redirect_url)
+ self.assertNotIn('/products', self.redirect_url)
+ self.assertIn('product_prefix=xxx', self.redirect_url)
+ self.assertTrue(self.redirect_permanent)
+
def _find_header(self, headers, name):
for header in headers:
if header["name"] == name:
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
Fri Mar 29 13:23:37 2013
@@ -52,11 +52,11 @@ class WhooshBackendTestCase(BaseBloodhou
self.assertEqual(2, result.hits)
docs = result.docs
self.assertEqual(
- {'id': '1', 'type': 'ticket', 'unique_id': 'ticket:1',
+ {'id': u'1', 'type': u'ticket', 'unique_id': u'empty:ticket:1',
'score': u'1'},
docs[0])
self.assertEqual(
- {'id': '2', 'type': 'ticket', 'unique_id': 'ticket:2',
+ {'id': u'2', 'type': u'ticket', 'unique_id': u'empty:ticket:2',
'score': u'2'},
docs[1])
@@ -66,7 +66,7 @@ class WhooshBackendTestCase(BaseBloodhou
self.print_result(result)
docs = result.docs
self.assertEqual(
- {'id': '1', 'type': 'ticket', 'unique_id': 'ticket:1',
+ {'id': u'1', 'type': u'ticket', 'unique_id': u'empty:ticket:1',
"score": 1.0},
docs[0])
@@ -80,7 +80,6 @@ class WhooshBackendTestCase(BaseBloodhou
{'id': '1', 'type': 'ticket'},
docs[0])
-
def test_can_survive_after_restart(self):
self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
whoosh_backend2 = WhooshBackend(self.env)
Added:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/utils.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/utils.py?rev=1462468&view=auto
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/utils.py
(added)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/utils.py
Fri Mar 29 13:23:37 2013
@@ -0,0 +1,39 @@
+#!/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.
+
+
+def get_global_env(env):
+ if hasattr(env, 'parent') and env.parent is not None:
+ return env.parent
+ else:
+ return env
+
+
+class GlobalProduct(object):
+ prefix = ""
+ name = ""
+GlobalProduct = GlobalProduct()
+
+
+def get_product(env):
+ if getattr(env, 'product', None) is not None:
+ return env.product
+ else:
+ return GlobalProduct
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
Fri Mar 29 13:23:37 2013
@@ -42,7 +42,9 @@ from trac.web.chrome import (INavigation
web_context)
from bhsearch.api import (BloodhoundSearchApi, ISearchParticipant, SCORE, ASC,
DESC, IndexFields, SortInstruction)
+from bhsearch.utils import get_global_env
from trac.wiki.formatter import extract_link
+from multiproduct.env import ProductEnvironment
SEARCH_PERMISSION = 'SEARCH_VIEW'
DEFAULT_RESULTS_PER_PAGE = 10
@@ -50,6 +52,7 @@ SEARCH_URL = '/search'
BHSEARCH_URL = '/bhsearch'
SEARCH_URLS_RE = re.compile(r'/(?P<prefix>bh)?search(?P<suffix>.*)')
+
class RequestParameters(object):
"""
Helper class for parameter parsing and creation of bhsearch specific URLs.
@@ -65,9 +68,12 @@ class RequestParameters(object):
VIEW = "view"
SORT = "sort"
DEBUG = "debug"
+ PRODUCT_PREFIX = "product_prefix"
+ PRODUCT_ID = "productid"
- def __init__(self, req):
+ def __init__(self, req, href=None):
self.req = req
+ self.href = href or req.href
self.original_query = req.args.getfirst(self.QUERY)
if self.original_query is None:
@@ -90,6 +96,12 @@ class RequestParameters(object):
DEFAULT_RESULTS_PER_PAGE))
self.page = int(req.args.getfirst(self.PAGE, '1'))
self.type = req.args.getfirst(self.TYPE)
+
+ if self.PRODUCT_ID in req.args:
+ # Accessed using product url
+ self.product = req.args.getfirst(self.PRODUCT_ID)
+ else:
+ self.product = req.args.getfirst(self.PRODUCT_PREFIX)
self.debug = int(req.args.getfirst(self.DEBUG, '0'))
self.params = {
@@ -108,6 +120,8 @@ class RequestParameters(object):
self.params[self.PAGE] = self.page
if self.type:
self.params[self.TYPE] = self.type
+ if self.product:
+ self.params[self.PRODUCT_PREFIX] = self.product
if self.filter_queries:
self.params[RequestParameters.FILTER_QUERY] = self.filter_queries
if self.sort_string:
@@ -189,7 +203,7 @@ class RequestParameters(object):
if skip_query:
self._delete_if_exists(params, self.QUERY)
- return self.req.href.bhsearch(**params)
+ return self.href.bhsearch(**params)
def _create_sort_expression(self, sort):
"""
@@ -227,6 +241,7 @@ class BloodhoundSearchModule(Component):
prefix = "all"
default_grid_fields = [
+ IndexFields.PRODUCT,
IndexFields.ID,
IndexFields.TYPE,
IndexFields.TIME,
@@ -237,7 +252,7 @@ class BloodhoundSearchModule(Component):
default_facets = ListOption(
BHSEARCH_CONFIG_SECTION,
prefix + '_default_facets',
- default=",".join([IndexFields.TYPE]),
+ default=",".join([IndexFields.TYPE, IndexFields.PRODUCT]),
doc="""Default facets applied to search view of all resources""")
default_view = Option(
@@ -337,15 +352,17 @@ class BloodhoundSearchModule(Component):
return handler
def post_process_request(self, req, template, data, content_type):
- if data is not None:
- if self.redirect_enabled:
- data['search_handler'] = req.href.bhsearch()
- elif req.path_info.startswith(SEARCH_URL):
- data['search_handler'] = req.href.search()
- elif self.default_search or req.path_info.startswith(BHSEARCH_URL):
- data['search_handler'] = req.href.bhsearch()
- else:
- data['search_handler'] = req.href.search()
+ if data is None:
+ return template, data, content_type
+
+ if self.redirect_enabled:
+ data['search_handler'] = req.href.bhsearch()
+ elif req.path_info.startswith(SEARCH_URL):
+ data['search_handler'] = req.href.search()
+ elif self.default_search or req.path_info.startswith(BHSEARCH_URL):
+ data['search_handler'] = req.href.bhsearch()
+ else:
+ data['search_handler'] = req.href.search()
return template, data, content_type
@@ -386,7 +403,10 @@ class RequestContext(object):
):
self.env = env
self.req = req
- self.parameters = RequestParameters(req)
+ self.parameters = RequestParameters(
+ req,
+ href=get_global_env(self.env).href
+ )
self.data = {
self.DATA_QUERY: self.parameters.query,
self.DATA_SEARCH_EXTRAS: [],
@@ -422,7 +442,7 @@ class RequestContext(object):
# Compatibility with trac search
self._process_legacy_type_filters(req, search_participants)
- if req.path_info.startswith(SEARCH_URL):
+ if not req.path_info.startswith(BHSEARCH_URL):
self.requires_redirect = True
self.fields = self._prepare_fields_and_view()
@@ -489,6 +509,11 @@ class RequestContext(object):
(RequestParameters.TYPE, self.active_type)
)
+ if self.parameters.product:
+ self.data[self.DATA_SEARCH_EXTRAS].append(
+ (RequestParameters.PRODUCT_PREFIX, self.parameters.product)
+ )
+
if self.parameters.view:
self.data[self.DATA_SEARCH_EXTRAS].append(
(RequestParameters.VIEW, self.parameters.view)
@@ -662,7 +687,12 @@ class RequestContext(object):
def _process_doc(self, doc):
ui_doc = dict(doc)
- ui_doc["href"] = self.req.href(doc['type'], doc['id'])
+ if doc['product']:
+ product_href = ProductEnvironment(self.env, doc['product']).href
+ # pylint: disable=too-many-function-args
+ ui_doc["href"] = product_href(doc['type'], doc['id'])
+ else:
+ ui_doc["href"] = self.req.href(doc['type'], doc['id'])
#todo: perform content adaptation here
if doc['content']:
Modified:
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py
URL:
http://svn.apache.org/viewvc/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py?rev=1462468&r1=1462467&r2=1462468&view=diff
==============================================================================
---
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py
(original)
+++
bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py
Fri Mar 29 13:23:37 2013
@@ -24,6 +24,7 @@ from bhsearch.api import ISearchBackend,
IQueryPreprocessor
import os
from bhsearch.search_resources.ticket_search import TicketFields
+from bhsearch.utils import get_global_env
from trac.core import Component, implements, TracError
from trac.config import Option, IntOption
from trac.util.text import empty
@@ -91,7 +92,8 @@ class WhooshBackend(Component):
def __init__(self):
self.index_dir = self.index_dir_setting
if not os.path.isabs(self.index_dir):
- self.index_dir = os.path.join(self.env.path, self.index_dir)
+ self.index_dir = os.path.join(get_global_env(self.env).path,
+ self.index_dir)
if index.exists_in(self.index_dir):
self.index = index.open_dir(self.index_dir)
else:
@@ -117,7 +119,9 @@ class WhooshBackend(Component):
writer = self._create_writer()
self._reformat_doc(doc)
- doc[UNIQUE_ID] = self._create_unique_id(doc["type"], doc["id"])
+ doc[UNIQUE_ID] = self._create_unique_id(doc.get("product", ''),
+ doc["type"],
+ doc["id"])
self.log.debug("Doc to index: %s", doc)
try:
writer.update_document(**doc)
@@ -142,8 +146,8 @@ class WhooshBackend(Component):
else:
doc[key] = self._to_whoosh_format(value)
- def delete_doc(self, doc_type, doc_id, operation_context=None):
- unique_id = self._create_unique_id(doc_type, doc_id)
+ def delete_doc(self, product, doc_type, doc_id, operation_context=None):
+ unique_id = self._create_unique_id(product, doc_type, doc_id)
self.log.debug('Removing document from the index: %s', unique_id)
writer = operation_context
is_local_writer = False
@@ -232,7 +236,8 @@ class WhooshBackend(Component):
try:
actual_query = unicode(query.simplify(searcher))
results.debug['actual_query'] = actual_query
- except TypeError:
+ # pylint: disable=bare-except
+ except:
# Simplify has a bug that causes it to fail sometimes.
pass
return results
@@ -245,8 +250,26 @@ class WhooshBackend(Component):
return query_expression
return whoosh.query.And((query_expression, query_filter))
- def _create_unique_id(self, doc_type, doc_id):
- return u"%s:%s" % (doc_type, doc_id)
+ def _create_unique_id(self, product, doc_type, doc_id):
+ product, doc_type, doc_id = \
+ self._apply_empty_facets_workaround(product, doc_type, doc_id)
+
+ if product:
+ return u"%s:%s:%s" % (product, doc_type, doc_id)
+ else:
+ return u"%s:%s" % (doc_type, doc_id)
+
+ def _apply_empty_facets_workaround(self, product, doc_type, doc_id):
+ # Apply the same workaround that is used at insertion time
+ doc = {
+ IndexFields.PRODUCT: product,
+ IndexFields.TYPE: doc_type,
+ IndexFields.ID: doc_id,
+ }
+ WhooshEmptyFacetErrorWorkaround(self.env).pre_process(doc)
+ return (doc[IndexFields.PRODUCT],
+ doc[IndexFields.TYPE],
+ doc[IndexFields.ID])
def _to_whoosh_format(self, value):
if isinstance(value, basestring):
@@ -440,6 +463,7 @@ class WhooshEmptyFacetErrorWorkaround(Co
IndexFields.STATUS,
TicketFields.MILESTONE,
TicketFields.COMPONENT,
+ IndexFields.PRODUCT,
]
#IDocIndexPreprocessor methods