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


Reply via email to