You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by ju...@apache.org on 2013/04/02 13:28:41 UTC

svn commit: r1463489 [2/3] - in /bloodhound/trunk: ./ bloodhound_dashboard/ bloodhound_dashboard/bhdashboard/ bloodhound_dashboard/bhdashboard/layouts/templates/ bloodhound_dashboard/bhdashboard/widgets/ bloodhound_dashboard/bhdashboard/widgets/templat...

Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/ticket/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/multiproduct/ticket/web_ui.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/multiproduct/ticket/web_ui.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/multiproduct/ticket/web_ui.py Tue Apr  2 11:28:39 2013
@@ -20,6 +20,8 @@
 
 import re
 
+from genshi.builder import tag
+
 from trac.core import TracError
 from trac.ticket.model import Ticket
 from trac.ticket.web_ui import TicketModule
@@ -30,8 +32,11 @@ from trac.resource import Resource, get_
 from trac.search import search_to_sql, shorten_result
 from trac.util.datefmt import from_utimestamp
 from trac.util.translation import _, tag_
-from genshi.builder import tag
+from trac.wiki.parser import WikiParser
 
+from multiproduct.api import MultiProductSystem, PRODUCT_SYNTAX_DELIMITER_RE
+from multiproduct.env import lookup_product_env, ProductEnvironment
+from multiproduct.util import IDENTIFIER
 from multiproduct.web_ui import ProductModule
 
 REPORT_RE = re.compile(r'/report(?:/(?:([0-9]+)|-1))?$')
@@ -47,14 +52,13 @@ class ProductTicketModule(TicketModule):
         """Override for TicketModule process_request"""
         ticketid = req.args.get('id')
         productid = req.args.get('productid','')
-        ticket = Ticket(self.env, ticketid)
         
         if ticketid:
             if (req.path_info == '/products/' + productid + '/newticket' or
                 req.path_info == '/products'):
                 raise TracError(_("id can't be set for a new ticket request."))
             ticket = Ticket(self.env, ticketid)
-            if productid and ticket['product'] != req.args.get('product',''):
+            if productid and ticket['product'] != productid:
                 msg = "Ticket %(id)s in product '%(prod)' does not exist."
                 raise ResourceNotFound(_(msg, id=ticketid, prod=productid),
                                        _("Invalid ticket number"))
@@ -136,3 +140,67 @@ class ProductReportModule(ReportModule):
         if 'REPORT_VIEW' in req.perm:
             href = ProductModule.get_product_path(self.env, req, 'report')
             yield ('mainnav', 'tickets', tag.a(_('View Tickets'), href=href))
+
+    # IWikiSyntaxProvider methods
+    #def get_link_resolvers(self):
+    # not yet required
+
+    def get_wiki_syntax(self):
+        # FIXME: yield from
+        for s in super(ProductReportModule, self).get_wiki_syntax():
+            yield s
+        # Previously unmatched prefix 
+        yield (r"!?\{(?P<prp>%s(?:\s+|(?:%s)))[0-9]+\}" % 
+                    (IDENTIFIER, PRODUCT_SYNTAX_DELIMITER_RE),
+               lambda x, y, z: self._format_link(x, 'report', y[1:-1], y, z))
+        # Absolute product report syntax
+        yield (r"!?\{(?P<prns>global:|product:%s(?:\s+|:))"
+               r"(?P<prid>[0-9]+)\}" % (IDENTIFIER,),
+               lambda x, y, z: (self._format_mplink(x, 'report', y[1:-1], y, z)))
+
+    def _format_link(self, formatter, ns, target, label, fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, target, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
+
+        # second chance to match InterTrac prefix as product prefix
+        it_report = fullmatch.group('it_' + ns) or fullmatch.group('prp')
+        if it_report:
+            return self._format_mplink(formatter, ns, target, label, fullmatch)
+
+        report, args, fragment = formatter.split_link(target)
+        return tag.a(label, href=formatter.href.report(report) + args,
+                     class_='report')
+
+    def _format_mplink(self, formatter, ns, target, label, fullmatch=None):
+        mpsys = self.env[MultiProductSystem]
+        if mpsys is not None:
+            substeps = []
+            prns = fullmatch.group('prns')
+            if not prns:
+                # Forwarded from _format_link, inherit current context
+                product_id = fullmatch.group('it_' + ns) or \
+                             fullmatch.group('prp') 
+                if product_id:
+                    product_ns = 'product'
+                    substeps = [product_id.strip()]
+                elif isinstance(self.env, ProductEnvironment):
+                    product_ns = 'product'
+                    substeps = [self.env.product.prefix]
+                else:
+                    product_ns = 'global'
+            elif prns == 'global:':
+                product_ns = 'global'
+            elif prns.startswith('product:'):
+                product_ns, product_id = prns.strip().split(':')[:2]
+                substeps = [product_id]
+            report_id = fullmatch.group('prid') or \
+                        re.match(r'^.*?(\d+)$', target).group(1)
+            substeps += [ns, report_id]
+            
+            return mpsys._format_link(formatter, product_ns, 
+                                      u':'.join(substeps),
+                                      label, fullmatch)
+        else:
+            return tag.a(label, class_='missing product')

Modified: bloodhound/trunk/bloodhound_multiproduct/multiproduct/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/multiproduct/web_ui.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/multiproduct/web_ui.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/multiproduct/web_ui.py Tue Apr  2 11:28:39 2013
@@ -34,8 +34,10 @@ from trac.web.chrome import (add_link, a
 from trac.web.main import RequestDispatcher
 
 from multiproduct.model import Product
+from multiproduct.api import DEFAULT_PRODUCT
 
 PRODUCT_RE = re.compile(r'^/products/(?P<pid>[^/]*)(?P<pathinfo>.*)')
+REDIRECT_DEFAULT_RE = re.compile(r'^/(?P<section>milestone|roadmap|query|report|newticket|ticket|qct|timeline|(raw-|zip-)?attachment|diff|batchmodify|search)(?P<pathinfo>.*)')
 
 class ProductModule(Component):
     """Base Product behaviour"""
@@ -58,26 +60,44 @@ class ProductModule(Component):
     def pre_process_request(self, req, handler):
         """pre process request filter"""
         pid = None
-        match = PRODUCT_RE.match(req.path_info)
-        if match:
+        product_match = PRODUCT_RE.match(req.path_info)
+        if product_match:
             dispatcher = self.env[RequestDispatcher]
             if dispatcher is None:
                 raise TracError('Unable to load RequestDispatcher.')
-            pid = match.group('pid')
-        
+            pid = product_match.group('pid')
+
         if pid:
             products = Product.select(self.env, where={'prefix': pid})
             if pid and len(products) == 1:
                 req.args['productid'] = pid
                 req.args['product'] = products[0].name
-                if handler is self and match.group('pathinfo') not in ('', '/'):
+                if handler is self and product_match.group('pathinfo') not in ('', '/'):
                     # select a new handler
                     environ = req.environ.copy()
                     pathinfo = environ['PATH_INFO'].split('/')
                     pathinfo = '/'.join(pathinfo[:1] + pathinfo[3:])
                     environ['PATH_INFO'] = pathinfo
                     newreq = Request(environ, lambda *args, **kwds: None)
-                    
+                    # Request.args[] are lazily evaluated, so special care must be
+                    # taken when creating new Requests from an old environment, as
+                    # the args will be evaluated again. In case of POST requests,
+                    # this comes down to re-evaluating POST parameters such as
+                    # <form> arguments, which in turn causes yet another read() on
+                    # a socket, causing the request to block (deadlock).
+                    #
+                    # The following happens during Requests.args[] evaluation:
+                    #   1. Requests.callbacks['args'] is called -> arg_list_to_args(req.arg_list)
+                    #   2. req.arg_list is evaluated, calling Request._parse_arg_list
+                    #   3. _parse_arg_list() calls _FieldStorage() for reading the params
+                    #   4. _FieldStorage() constructor calls self.read_urlencoded()
+                    #   5. this calls self.fp.read() which reads from the socket
+                    #
+                    # Since the 'newreq' above is created from the same environ as 'req',
+                    # the newreq.args below caused a re-evaluation, thus a deadlock.
+                    # The fix is to copy the args from the old request to the new one.
+                    setattr(newreq, 'args', req.args)
+
                     new_handler = None
                     for hndlr in dispatcher.handlers:
                         if hndlr is not self and hndlr.match_request(newreq):
@@ -96,7 +116,17 @@ class ProductModule(Component):
             else:
                 raise ResourceNotFound(_("Product %(id)s does not exist.", 
                                          id=pid), _("Invalid product id"))
-        
+        else:
+            redirect_match = REDIRECT_DEFAULT_RE.match(req.path_info)
+            if redirect_match:
+                target = req.href.products(DEFAULT_PRODUCT,
+                                           redirect_match.group('section'),
+                                           redirect_match.group('pathinfo') \
+                                               if redirect_match.group('pathinfo') else None)
+                if req.query_string:
+                    target += '?' + req.query_string
+                req.redirect(target, permanent=True)
+
         return handler
     
     def post_process_request(self, req, template, data, content_type):
@@ -127,10 +157,12 @@ class ProductModule(Component):
     
     # IRequestHandler methods
     def match_request(self, req):
-        """match request handler"""
-        if req.path_info.startswith('/products'):
+        if req.path_info == '/products':
             return True
-        return False
+
+        # handle '/products/...', but excluding QuickCreateTicket (qct) requests
+        m = PRODUCT_RE.match(req.path_info)
+        return m and m.group('pathinfo').strip('/') != 'qct'
     
     def process_request(self, req):
         """process request handler"""
@@ -161,40 +193,17 @@ class ProductModule(Component):
             elif action == 'edit':
                 return self._do_save(req, product)
             elif action == 'delete':
-                req.perm(product.resource).require('PRODUCT_DELETE')
-                retarget_to = req.args.get('retarget', None)
-                name = product.name
-                product.delete(resources_to=retarget_to)
-                add_notice(req, _('The product "%(n)s" has been deleted.',
-                                  n = name))
-                req.redirect(req.href.products())
+                raise TracError(_('Product removal is not allowed!'))
         elif action in ('new', 'edit'):
             return self._render_editor(req, product)
         elif action == 'delete':
-            req.perm(product.resource).require('PRODUCT_DELETE')
-            return 'product_delete.html', data, None
+            raise TracError(_('Product removal is not allowed!'))
         
         if pid is None:
             data = {'products': products,
                     'context': web_context(req, Resource('products', None))}
             return 'product_list.html', data, None
         
-        def add_product_link(rel, product):
-            href = req.href.products(product.prefix)
-            add_link(req, rel, href, _('Product "%(name)s"',
-                                       name=product.name))
-        
-        idx = [i for i, p in enumerate(products) if p.name == product.name]
-        if idx:
-            idx = idx[0]
-            if idx > 0:
-                add_product_link('first', products[0])
-                add_product_link('prev', products[idx - 1])
-            if idx < len(products) - 1:
-                add_product_link('next', products[idx + 1])
-                add_product_link('last', products[-1])        
-        prevnext_nav(req, _('Previous Product'), _('Next Product'),
-                     _('Back to Product List'))
         return 'product_view.html', data, None
     
     def _render_editor(self, req, product):
@@ -264,6 +273,7 @@ class ProductModule(Component):
                 prod.insert()
                 add_notice(req, _('The product "%(id)s" has been added.',
                                   id=prefix))
+
         if warnings:
             product.update_field_dict(keys)
             product.update_field_dict(field_data)

Modified: bloodhound/trunk/bloodhound_multiproduct/setup.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/setup.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/setup.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/setup.py Tue Apr  2 11:28:39 2013
@@ -30,7 +30,9 @@ setup(
     package_data = {'multiproduct' : ['templates/*.html',]},
     entry_points = {'trac.plugins': [
             'multiproduct.model = multiproduct.model',
+            'multiproduct.perm = multiproduct.perm',
             'multiproduct.product_admin = multiproduct.product_admin',
+            'multiproduct.ticket.query = multiproduct.ticket.query',
             'multiproduct.ticket.web_ui = multiproduct.ticket.web_ui',
             'multiproduct.web_ui = multiproduct.web_ui',
         ],},

Modified: bloodhound/trunk/bloodhound_multiproduct/tests/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/tests/__init__.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/tests/__init__.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/tests/__init__.py Tue Apr  2 11:28:39 2013
@@ -15,3 +15,67 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
+
+from collections import deque
+from fnmatch import fnmatch
+import sys
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+from pkg_resources import resource_listdir, resource_isdir, resource_exists
+
+
+class TestLoader(unittest.TestLoader):
+    testLoaderAttribute = '__testloader__'
+    testMethodPrefix = 'test'
+    sortTestMethodsUsing = cmp
+    suiteClass = unittest.TestSuite
+
+    def discover_package(self, package_or_requirement, pattern='test*.py', ignore_subpkg_root=True):
+        """Find and return all test modules from the specified package
+        directory, recursing into subdirectories to find them. Only test files
+        that match the pattern will be loaded. (Using shell style pattern
+        matching.)
+
+        All test modules must be importable from the top level of the project
+        and registered with `pkg_resources` (e.g. via `setup.py develop`).
+
+        If a target test module contains a '__testloader__' attribute then
+        related object will override current loader for every individual 
+        module across the hierarchy.
+        """
+        pending = deque([(package_or_requirement, self, True)])
+        tests = []
+        while pending:
+            mdlnm, loader, isdir = pending.popleft()
+            try:
+                mdl = self._get_module_from_name(mdlnm)
+            except ImportError:
+                # Skip packages not having __init__.py
+                continue
+            loader = getattr(mdl, self.testLoaderAttribute, None) or loader
+            if not (isdir and ignore_subpkg_root):
+                if mdlnm != package_or_requirement and hasattr(mdl, 'test_suite'):
+                    tests.append(mdl.test_suite())
+                else:
+                    tests.append(loader.loadTestsFromModule(mdl))
+            if isdir and resource_exists(mdlnm, '__init__.py'):
+                for fnm in resource_listdir(mdlnm, ''):
+                    if resource_isdir(mdlnm, fnm):
+                        pending.append( (mdlnm + '.' + fnm, loader, True) )
+                    elif any(fnm.endswith(ext) for ext in ['.py', '.pyc']) \
+                            and fnmatch(fnm, pattern) and fnm != '__init__.py':
+                        submdlnm = mdlnm + '.' + fnm.rsplit('.', 1)[0]
+                        pending.append( (submdlnm, loader, False) )
+        return self.suiteClass(tests)
+
+    def _get_module_from_name(self, name):
+        __import__(name)
+        return sys.modules[name]
+
+def test_suite():
+    return TestLoader().discover_package('tests', pattern='*.py')
+
+

Modified: bloodhound/trunk/bloodhound_multiproduct/tests/model.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_multiproduct/tests/model.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_multiproduct/tests/model.py (original)
+++ bloodhound/trunk/bloodhound_multiproduct/tests/model.py Tue Apr  2 11:28:39 2013
@@ -17,9 +17,14 @@
 #  under the License.
 
 """Tests for multiproduct/model.py"""
-import unittest
-import tempfile
 import shutil
+import sys
+import tempfile
+
+if sys.version_info < (2, 7):
+    import unittest2 as unittest
+else:
+    import unittest
 
 from sqlite3 import OperationalError
 

Modified: bloodhound/trunk/bloodhound_search/bhsearch/api.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/api.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/api.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/api.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/search_resources/milestone_search.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/search_resources/milestone_search.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/search_resources/milestone_search.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/search_resources/milestone_search.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/search_resources/ticket_search.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/search_resources/ticket_search.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/search_resources/ticket_search.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/search_resources/ticket_search.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/search_resources/wiki_search.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/search_resources/wiki_search.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/search_resources/wiki_search.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/search_resources/wiki_search.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/tests/api.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/tests/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py Tue Apr  2 11:28:39 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/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py Tue Apr  2 11:28:39 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)

Modified: bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py Tue Apr  2 11:28:39 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,6 +352,9 @@ class BloodhoundSearchModule(Component):
         return handler
 
     def post_process_request(self, req, template, data, content_type):
+        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):
@@ -385,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: [],
@@ -421,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()
@@ -488,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)
@@ -661,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/trunk/bloodhound_search/bhsearch/whoosh_backend.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py Tue Apr  2 11:28:39 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

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/htdocs/bloodhound.css Tue Apr  2 11:28:39 2013
@@ -613,6 +613,10 @@ input[type="submit"].btn.btn-micro {
  padding: 7px 14px;
 }
 
+#breadcrumbbar .btn-group a:hover {
+  text-decoration: none;
+}
+
 @media (max-width: 979px) {
  .hidden-desktop {
   display: inherit !important;
@@ -1103,7 +1107,29 @@ table.wiki th {
 
 /* @end */
 
- 
+/* @group Dashboard */
+
+#products h4 {
+  margin: 5px 0 10px 0;
+}
+
+#products h5 {
+  margin: 5px 0 0 0;
+}
+
+#products .product-well {
+  background-color: white;
+  padding: 10px;
+}
+
+#products .subset {
+  list-style-type: none;
+  margin: 0 0 10px 0;
+  padding: 0;
+}
+
+/* @end */
+
 #changelog div.comment-box{
  border: 2px solid #999999;
  border-radius: 8px 8px 8px 8px;
@@ -1111,4 +1137,4 @@ table.wiki th {
  padding: 2px 4px 2px 4px;
  margin: 10px 0px 10px 10px;
  min-height: 20px;
-}
\ No newline at end of file
+}

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_admin.html
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_admin.html?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_admin.html (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_admin.html Tue Apr  2 11:28:39 2013
@@ -48,7 +48,7 @@
   </py:def>
   <py:def function="admin_function(fix_legacy)">
     <div id="content" class="admin row">
-      <h1 class="span12">Administration</h1>
+      <h1 class="span12">Administration${' - ' + admin_current_product[1] if admin_current_product else None}</h1>
 
       <div id="tabs" class="span3 hidden-phone">
         ${admin_nav_list(False)}

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_links.html
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_links.html?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_links.html (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_links.html Tue Apr  2 11:28:39 2013
@@ -60,8 +60,8 @@
     py:with="last = idx == len(path_hidden) - 1"><a 
       class="pathentry" href="${part.href}"
       title="${_('View %(name)s', name=part.name)}">${part.name}<span 
-      py:if="not last" class="pathentry sep">/</span></a></li></ul></li><span 
-    class="pathentry sep divider">/</span></py:if><py:for 
+      py:if="not last" class="pathentry sep">/</span></a></li></ul><span 
+    class="pathentry sep divider">/</span></li></py:if><py:for 
     each="idx, part in enumerate(path_visible)"><py:with 
     vars="last = idx == len(path_visible) - 1;
     ">${part_ui(part, last)}</py:with></py:for></py:with></py:if>

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_search.html
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_search.html?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_search.html (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_search.html Tue Apr  2 11:28:39 2013
@@ -21,9 +21,11 @@
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:i18n="http://genshi.edgewall.org/i18n"
      xmlns:xi="http://www.w3.org/2001/XInclude"
-    py:strip="" py:if="query">
-  <li>Search <span class="divider">/</span></li>
-  <li py:with="query_summary = query[:15] + ' ...'" title="$query">
+    py:strip="">
+  <xi:include href="bh_path_general.html" />
+  <li><span class="divider">/</span></li>
+  <li><a href="${href.search()}">Search</a><span py:if="query" class="divider">/</span></li>
+  <li py:if="query" py:with="query_summary = query[:15] + ' ...'" title="$query">
     <code>${query_summary if len(query) &gt; 19 else query}</code>
   </li>
 </div>

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_ticket.html
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_ticket.html?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_ticket.html (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_path_ticket.html Tue Apr  2 11:28:39 2013
@@ -22,23 +22,10 @@
      xmlns:i18n="http://genshi.edgewall.org/i18n"
      xmlns:xi="http://www.w3.org/2001/XInclude"
     py:strip="">
-  <li py:if="bhdb">
-    <a title="View the dashboard" href="${href.dashboard()}">Dashboard</a>
+
+  <xi:include href="bh_path_general.html" />
+  <li>
     <span class="divider">/</span>
+    <span>Ticket #${ticket.id}</span>
   </li>
-  <li py:if="ticket.product" py:with="prodlink = path_show_product and href.products(ticket.product) or href.query(product=ticket['product'], active='!closed')">
-    <a title="Active tickets for product ${ticket.product}"
-        href="${prodlink}">
-      ${ticket.product}
-    </a>
-    <span class="divider">/</span>
-  </li>
-  <li py:if="ticket.milestone" py:with="mslink = path_show_milestone and href.milestone(ticket.milestone) or href.query(milestone=ticket['milestone'], active='!closed')">
-    <a title="Active tickets in milestone ${ticket.milestone}"
-        href="${mslink}">
-      ${ticket.milestone}
-    </a>
-    <span class="divider">/</span>
-  </li>
-  <li><span>Ticket #${ticket.id}</span></li>
 </div>

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_product_view.html
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_product_view.html?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_product_view.html (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_product_view.html Tue Apr  2 11:28:39 2013
@@ -47,7 +47,7 @@
   <py:def function="product_buttons()">
     <div py:if="'PRODUCT_MODIFY' in perm(product.resource) or
                 'PRODUCT_DELETE' in perm(product.resource) or
-                attachments.can_create"
+                (attachments and attachments.can_create)"
         class="buttons">
       <form py:if="'PRODUCT_MODIFY' in perm(product.resource)"
           method="get" action="" id="editproduct"
@@ -79,11 +79,12 @@
       <bh:args>
         <bh:arg name="max">10</bh:arg>
         <bh:arg name="query">product=${product.prefix}&amp;status=!closed&amp;group=time&amp;col=id&amp;col=summary&amp;col=owner&amp;col=status&amp;col=priority&amp;order=priority&amp;groupdesc=1&amp;desc=1</bh:arg>
-        <bh:arg name="title">Active tickets</bh:arg>
+        <bh:arg name="title">Active Tickets</bh:arg>
       </bh:args>
     </bh:widget>
     <bh:widget urn="TicketFieldValues">
       <bh:args>
+        <bh:arg name="title">Versions</bh:arg>
         <bh:arg name="field">version</bh:arg>
         <bh:arg name="verbose">true</bh:arg>
         <bh:arg name="query">product=${product.prefix}&amp;group=version</bh:arg>
@@ -91,6 +92,15 @@
     </bh:widget>
     <bh:widget urn="TicketFieldValues">
       <bh:args>
+        <bh:arg name="title">Milestones</bh:arg>
+        <bh:arg name="field">milestone</bh:arg>
+        <bh:arg name="verbose">true</bh:arg>
+        <bh:arg name="query">product=${product.prefix}&amp;group=milestone</bh:arg>
+      </bh:args>
+    </bh:widget>
+    <bh:widget urn="TicketFieldValues">
+      <bh:args>
+        <bh:arg name="title">Components</bh:arg>
         <bh:arg name="field">component</bh:arg>
         <bh:arg name="verbose">true</bh:arg>
         <bh:arg name="query">product=${product.prefix}&amp;group=component</bh:arg>

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=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket.html (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket.html Tue Apr  2 11:28:39 2013
@@ -447,7 +447,7 @@
               <div id="modify" class="well">
                 <!--! Properties -->
                 <fieldset id="properties" py:if="can_modify or can_edit or can_create"
-                          py:with="fields = [f for f in fields if not f.skip]">
+                          py:with="fields = [f for f in fields if not f.skip and f.name != 'product']">
                   <legend py:choose="">
                     <py:when test="ticket.exists">Change Properties</py:when>
                     <py:otherwise>Properties</py:otherwise>
@@ -456,6 +456,16 @@
                   <table id="modifyproperties">
                     <tr py:if="can_modify or can_create">
                       <td colspan="2">
+                        <label for="field-product-name">Product:</label>
+                        <input type="text" disabled="disabled" class="${'span2' if ticket.exists else 'span7'}"
+                          id="field-product-name" name="field_product_name"
+                          value="$req.perm.env.product.name ($req.perm.env.product.prefix)" />
+                        <input type="hidden" id="field-product" name="field_product" value="$req.perm.env.product.prefix" />
+                      </td>
+                    </tr>
+
+                    <tr py:if="can_modify or can_create">
+                      <td colspan="2">
                         <label for="field-summary">Summary:</label>
                         <input class="${'span4' if ticket.exists else 'span7'}" type="text" id="field-summary" name="field_summary" value="$ticket.summary" />
                       </td>

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket_box.html
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket_box.html?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket_box.html (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_ticket_box.html Tue Apr  2 11:28:39 2013
@@ -53,7 +53,7 @@ Arguments:
                 </py:if>
               </h5>
             </div>
-            <div class="span2" data-edit="${'inplace' if (field and field.name != 'status') else None}" id="${'vc-' + field.name if field else None}">
+            <div class="span2" data-edit="${'inplace' if (field and field.name not in ('status', 'product')) else None}" id="${'vc-' + field.name if field else None}">
               <py:if test="field">
                 <py:choose test="">
                   <py:when test="'rendered' in field">${field.rendered}</py:when>

Modified: bloodhound/trunk/bloodhound_theme/bhtheme/theme.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/bhtheme/theme.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/bhtheme/theme.py (original)
+++ bloodhound/trunk/bloodhound_theme/bhtheme/theme.py Tue Apr  2 11:28:39 2013
@@ -16,7 +16,7 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-import sys
+import sys, copy
 
 from genshi.builder import tag
 from genshi.core import TEXT
@@ -29,7 +29,7 @@ from trac.config import ListOption
 from trac.mimeview.api import get_mimetype
 from trac.resource import Resource
 from trac.ticket.api import TicketSystem
-from trac.ticket.model import Ticket
+from trac.ticket.model import Ticket, Milestone
 from trac.ticket.notification import TicketNotifyEmail
 from trac.ticket.web_ui import TicketModule
 from trac.util.compat import set
@@ -50,6 +50,10 @@ from pkg_resources import get_distributi
 from urlparse import urlparse
 from wsgiref.util import setup_testing_defaults
 
+from multiproduct.model import Product
+from multiproduct.env import ProductEnvironment
+from multiproduct.web_ui import PRODUCT_RE
+
 try:
     from multiproduct.ticket.web_ui import ProductTicketModule
 except ImportError:
@@ -67,16 +71,21 @@ class BloodhoundTheme(ThemeBase):
     )
     BLOODHOUND_TEMPLATE_MAP = {
         # Admin
-        'admin_basics.html' : ('bh_admin_basics.html', None),
-        'admin_components.html' : ('bh_admin_components.html', None),
-        'admin_enums.html' : ('bh_admin_enums.html', None),
-        'admin_logging.html' : ('bh_admin_logging.html', None),
-        'admin_milestones.html' : ('bh_admin_milestones.html', None),
-        'admin_perms.html' : ('bh_admin_perms.html', None),
-        'admin_plugins.html' : ('bh_admin_plugins.html', None),
-        'admin_repositories.html' : ('bh_admin_repositories.html', None),
-        'admin_versions.html' : ('bh_admin_versions.html', None),
-        'admin_products.html' : ('bh_admin_products.html', None),
+        'admin_basics.html' : ('bh_admin_basics.html', '_modify_admin_breadcrumb'),
+        'admin_components.html' : ('bh_admin_components.html', '_modify_admin_breadcrumb'),
+        'admin_enums.html' : ('bh_admin_enums.html', '_modify_admin_breadcrumb'),
+        'admin_logging.html' : ('bh_admin_logging.html', '_modify_admin_breadcrumb'),
+        'admin_milestones.html' : ('bh_admin_milestones.html', '_modify_admin_breadcrumb'),
+        'admin_perms.html' : ('bh_admin_perms.html', '_modify_admin_breadcrumb'),
+        'admin_plugins.html' : ('bh_admin_plugins.html', '_modify_admin_breadcrumb'),
+        'admin_repositories.html' : ('bh_admin_repositories.html', '_modify_admin_breadcrumb'),
+        'admin_versions.html' : ('bh_admin_versions.html', '_modify_admin_breadcrumb'),
+        'admin_products.html' : ('bh_admin_products.html', '_modify_admin_breadcrumb'),
+        # no template substitutions below - use the default template,
+        # but call the modifier nonetheless
+        'admin_accountsconfig.html' : ('admin_accountsconfig.html', '_modify_admin_breadcrumb'),
+        'admin_users.html' : ('admin_users.html', '_modify_admin_breadcrumb'),
+        'repository_links.html' : ('repository_links.html', '_modify_admin_breadcrumb'),
 
         # Preferences
         'prefs.html' : ('bh_prefs.html', None),
@@ -99,14 +108,15 @@ class BloodhoundTheme(ThemeBase):
         'wiki_view.html' : ('bh_wiki_view.html', '_modify_wiki_page_path'),
 
         # Ticket
-        'milestone_edit.html' : ('bh_milestone_edit.html', None),
-        'milestone_delete.html' : ('bh_milestone_delete.html', None),
-        'milestone_view.html' : ('bh_milestone_view.html', '_modify_roadmap_css'),
-        'query.html' : ('bh_query.html', None),
-        'report_delete.html' : ('bh_report_delete.html', None),
-        'report_edit.html' : ('bh_report_edit.html', None), 
-        'report_list.html' : ('bh_report_list.html', None),
-        'report_view.html' : ('bh_report_view.html', None),
+        'milestone_edit.html' : ('bh_milestone_edit.html', '_modify_roadmap_page'),
+        'milestone_delete.html' : ('bh_milestone_delete.html', '_modify_roadmap_page'),
+        'milestone_view.html' : ('bh_milestone_view.html', '_modify_roadmap_page'),
+        'roadmap.html' : ('roadmap.html', '_modify_roadmap_page'),
+        'query.html' : ('bh_query.html', '_add_products_general_breadcrumb'),
+        'report_delete.html' : ('bh_report_delete.html', '_add_products_general_breadcrumb'),
+        'report_edit.html' : ('bh_report_edit.html', '_add_products_general_breadcrumb'), 
+        'report_list.html' : ('bh_report_list.html', '_add_products_general_breadcrumb'),
+        'report_view.html' : ('bh_report_view.html', '_add_products_general_breadcrumb'),
         'ticket.html' : ('bh_ticket.html', '_modify_ticket'),
         'ticket_preview.html' : ('bh_ticket_preview.html', None),
         'ticket_delete.html' : ('bh_ticket_delete.html', None),
@@ -121,7 +131,7 @@ class BloodhoundTheme(ThemeBase):
         'dir_entries.html' : ('bh_dir_entries.html', None),
 
         # Multi Product
-        'product_view.html' : ('bh_product_view.html', None),
+        'product_view.html' : ('bh_product_view.html', '_add_products_general_breadcrumb'),
 
         # General purpose
         'about.html' : ('bh_about.html', None),
@@ -251,6 +261,9 @@ class BloodhoundTheme(ThemeBase):
 
         req.chrome['labels'] = self._get_whitelabelling()
 
+        if data is not None:
+            data['product_list'] = self._get_product_list(req)
+
         links = req.chrome.get('links',{})
         # replace favicon if appropriate
         if self.env.project_icon == 'common/trac.ico':
@@ -308,10 +321,10 @@ class BloodhoundTheme(ThemeBase):
         if is_active:
             # Insert query string in search box (see bloodhound_theme.html)
             req.search_query = data.get('query')
-            # Breadcrumbs nav
-            data['resourcepath_template'] = 'bh_path_search.html'
             # Context nav
             prevnext_nav(req, _('Previous'), _('Next'))
+        # Breadcrumbs nav
+        data['resourcepath_template'] = 'bh_path_search.html'
 
     def _modify_wiki_page_path(self, req, template, data, content_type, is_active):
         """Override wiki breadcrumbs nav items
@@ -319,10 +332,14 @@ class BloodhoundTheme(ThemeBase):
         if is_active:
             data['resourcepath_template'] = 'bh_path_wikipage.html'
 
-    def _modify_roadmap_css(self, req, template, data, content_type, is_active):
-        """Insert roadmap.css
+    def _modify_roadmap_page(self, req, template, data, content_type, is_active):
+        """Insert roadmap.css + products breadcrumb
         """
         add_stylesheet(req, 'dashboard/css/roadmap.css')
+        self._add_products_general_breadcrumb(req, template, data,
+            content_type, is_active)
+        data['milestone_list'] = [m.name for m in Milestone.select(self.env)]
+        req.chrome['ctxtnav'] = []
 
     def _modify_ticket(self, req, template, data, content_type, is_active):
         """Ticket modifications
@@ -330,6 +347,18 @@ class BloodhoundTheme(ThemeBase):
         self._modify_resource_breadcrumb(req, template, data, content_type,
                                          is_active)
 
+    def _get_product_list(self, req, href_fcn=None):
+        """Returns a list of products as (prefix, name, url) tuples
+        """
+        if href_fcn is None:
+            href_fcn = req.href.products
+        product_list = []
+        for product in Product.select(self.env):
+            if 'PRODUCT_VIEW' in req.product_perm(product.prefix, product.resource):
+                product_list.append((product.prefix, product.name,
+                    href_fcn(product.prefix)))
+        return product_list
+
     def _modify_resource_breadcrumb(self, req, template, data, content_type,
                                     is_active):
         """Provides logic for breadcrumb resource permissions
@@ -342,6 +371,27 @@ class BloodhoundTheme(ThemeBase):
                 res = Resource(resname, data['ticket'][resname])
                 data['path_show_' + resname] = permname in req.perm(res)
 
+            # add milestone list + current milestone to the breadcrumb
+            data['milestone_list'] = [m.name for m in Milestone.select(self.env)]
+            mname = data['ticket']['milestone']
+            if mname:
+                data['milestone'] = Milestone(self.env, mname)
+
+    def _modify_admin_breadcrumb(self, req, template, data, content_type, is_active):
+        # override 'normal' product list with the admin one
+        glsettings = (None, _('(Global settings)'), req.href.admin())
+        data['admin_product_list'] = [ glsettings, ] + \
+            self._get_product_list(req,
+                lambda x: req.href.products(x, 'admin'))
+
+        if isinstance(req.perm.env, ProductEnvironment):
+            product = req.perm.env.product
+            data['admin_current_product'] = (product.prefix, product.name,
+                req.href.products(product.prefix, 'admin'))
+        else:
+            data['admin_current_product'] = glsettings
+        data['resourcepath_template'] = 'bh_path_general.html'
+
     def _modify_browser(self, req, template, data, content_type, is_active):
         """Locate path to file in breadcrumbs area rather than title.
         Add browser-specific CSS.
@@ -352,6 +402,11 @@ class BloodhoundTheme(ThemeBase):
             ))
         add_stylesheet(req, 'theme/css/browser.css')
 
+
+    def _add_products_general_breadcrumb(self, req, template, data,
+                                         content_type, is_active):
+        data['resourcepath_template'] = 'bh_path_general.html'
+
     # INavigationContributor methods
 
     def get_active_navigation_item(self, req):
@@ -369,7 +424,7 @@ class QuickCreateTicketDialog(Component)
     implements(IRequestFilter, IRequestHandler)
 
     qct_fields = ListOption('ticket', 'quick_create_fields', 
-                            'product,version,type',
+                            'version,type',
         doc="""Multiple selection fields displayed in create ticket menu""")
 
     # IRequestFilter(Interface):
@@ -397,6 +452,11 @@ class QuickCreateTicketDialog(Component)
             all_fields = dict([f['name'], f] \
                               for f in tm._prepare_fields(fakereq, ticket) \
                               if f['type'] == 'select')
+
+            product_field = all_fields['product'];
+            if product_field and self.env.product:
+                product_field['value'] = self.env.product.prefix
+
             data['qct'] = {'fields': [all_fields[k] for k in self.qct_fields
                                       if k in all_fields]}
         return template, data, content_type
@@ -406,7 +466,8 @@ class QuickCreateTicketDialog(Component)
     def match_request(self, req):
         """Handle requests sent to /qct
         """
-        return req.path_info == '/qct'
+        m = PRODUCT_RE.match(req.path_info)
+        return m and m.group('pathinfo').strip('/') == 'qct'
 
     def process_request(self, req):
         """Forward new ticket request to `trac.ticket.web_ui.TicketModule`

Modified: bloodhound/trunk/bloodhound_theme/setup.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_theme/setup.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_theme/setup.py (original)
+++ bloodhound/trunk/bloodhound_theme/setup.py Tue Apr  2 11:28:39 2013
@@ -19,6 +19,7 @@
 #  under the License.
 
 from setuptools import setup
+import sys
 
 setup(
   name = 'BloodhoundTheme',
@@ -36,6 +37,7 @@ setup(
       'Framework :: Trac',
     ],
   install_requires = ['BloodhoundDashboardPlugin', 'TracThemeEngine', 'Trac'],
+  tests_require = ['unittest2'] if sys.version_info < (2, 7) else [],
   entry_points = {
       'trac.plugins': [
             'bhtheme.theme = bhtheme.theme',

Modified: bloodhound/trunk/installer/bloodhound_setup.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/installer/bloodhound_setup.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/installer/bloodhound_setup.py (original)
+++ bloodhound/trunk/installer/bloodhound_setup.py Tue Apr  2 11:28:39 2013
@@ -76,7 +76,9 @@ BASE_CONFIG = {'components': {'bhtheme.*
                'theme': {'theme': 'bloodhound',},
                'trac': {'mainnav': ','.join(['dashboard', 'wiki', 'browser',
                                              'tickets', 'newticket', 'timeline',
-                                             'roadmap', 'search', 'admin']),},
+                                             'roadmap', 'search', 'admin']),
+                        'environment_factory': '',
+                        'request_factory': '',},
                'project': {'footer': ('Get involved with '
                                       '<a href="%(site)s">Apache Bloodhound</a>'
                                       % {'site': BH_PROJECT_SITE,}),},
@@ -215,7 +217,19 @@ class BloodhoundSetup(object):
             print ("Warning: Environment already exists at %s." % new_env)
             self.writeconfig(tracini, [{'inherit': {'file': baseini},},])
 
-        self.writeconfig(baseini, [BASE_CONFIG, accounts_config])
+        base_config = dict(BASE_CONFIG)
+        environment_factory_path = os.path.abspath(
+                                      os.path.normpath(
+                                          os.path.join(options['sourcedir'],
+                                                               'bloodhound_multiproduct/multiproduct/hooks.py')))
+        request_factory_path = os.path.abspath(
+                                   os.path.normpath(
+                                       os.path.join(options['sourcedir'],
+                                                            'bloodhound_multiproduct/multiproduct/hooks.py')))
+        base_config['trac']['environment_factory'] = environment_factory_path
+        base_config['trac']['request_factory'] = request_factory_path
+
+        self.writeconfig(baseini, [base_config, accounts_config])
 
         if os.path.exists(digestfile):
             backupfile(digestfile)
@@ -240,9 +254,18 @@ class BloodhoundSetup(object):
         print "Running wiki upgrades"
         bloodhound.onecmd('wiki upgrade')
         
-        print "Running wiki bh upgrades"
+        print "Running wiki Bloodhound upgrades"
         bloodhound.onecmd('wiki bh-upgrade')
 
+        print "Loading default product wiki"
+        bloodhound.onecmd('product admin @ wiki load %s' % " ".join(pages))
+
+        print "Running default product wiki upgrades"
+        bloodhound.onecmd('product admin @ wiki upgrade')
+
+        print "Running default product wiki Bloodhound upgrades"
+        bloodhound.onecmd('product admin @ wiki bh-upgrade')
+
         print """
 You can now start Bloodhound by running:
 
@@ -300,6 +323,9 @@ def handle_options():
     # Base Trac Options
     parser.add_option('--project', dest='project',
                       help='Set the top project name', default='main')
+    parser.add_option('--source_directory', dest='sourcedir',
+                      help='Specify root source code directory',
+                      default=os.path.normpath(os.path.join(os.getcwd(), '../'))),
     parser.add_option('--environments_directory', dest='envsdir',
                       help='Set the directory to contain environments',
                       default=os.path.join('bloodhound', 'environments'))

Modified: bloodhound/trunk/installer/requirements-dev.txt
URL: http://svn.apache.org/viewvc/bloodhound/trunk/installer/requirements-dev.txt?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/installer/requirements-dev.txt (original)
+++ bloodhound/trunk/installer/requirements-dev.txt Tue Apr  2 11:28:39 2013
@@ -17,6 +17,7 @@
 
 Pygments
 pytz
+sqlparse
 Babel
 Genshi
 -e ../trac

Modified: bloodhound/trunk/installer/requirements.txt
URL: http://svn.apache.org/viewvc/bloodhound/trunk/installer/requirements.txt?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/installer/requirements.txt (original)
+++ bloodhound/trunk/installer/requirements.txt Tue Apr  2 11:28:39 2013
@@ -17,6 +17,7 @@
 
 Pygments
 pytz
+sqlparse
 Babel
 Genshi
 ../trac

Propchange: bloodhound/trunk/trac/
------------------------------------------------------------------------------
  Merged /bloodhound/branches/bep_0003_multiproduct/trac:r1461360-1463488
  Merged /incubator/bloodhound/branches/bep_0003_multiproduct/trac:r1420073-1461359

Modified: bloodhound/trunk/trac/trac/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/trac/trac/__init__.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/trac/trac/__init__.py (original)
+++ bloodhound/trunk/trac/trac/__init__.py Tue Apr  2 11:28:39 2013
@@ -17,3 +17,9 @@ try:
     __version__ = get_distribution('Trac').version
 except DistributionNotFound:
     __version__ = '1.0.1'
+
+try:
+    from hooks import install_global_hooks
+    install_global_hooks()
+except:
+    pass

Modified: bloodhound/trunk/trac/trac/admin/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/trac/trac/admin/web_ui.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/trac/trac/admin/web_ui.py (original)
+++ bloodhound/trunk/trac/trac/admin/web_ui.py Tue Apr  2 11:28:39 2013
@@ -49,6 +49,11 @@ try:
 except ImportError:
     IAdminPageProvider = None
 
+try:
+    from acct_mgr.admin import AccountManagerAdminPanel
+except:
+    AccountManagerAdminPanel = None
+
 
 class AdminModule(Component):
     """Web administration interface provider and panel manager."""
@@ -166,10 +171,17 @@ class AdminModule(Component):
         providers = {}
 
         for provider in self.panel_providers:
-            p = list(provider.get_admin_panels(req) or [])
-            for panel in p:
-                providers[(panel[0], panel[2])] = provider
-            panels += p
+            add_provider = True
+            if AccountManagerAdminPanel is not None and \
+               isinstance(provider, AccountManagerAdminPanel) and \
+               getattr(self.env, 'parent', None):
+                add_provider = False
+
+            if add_provider:
+                p = list(provider.get_admin_panels(req) or [])
+                for panel in p:
+                    providers[(panel[0], panel[2])] = provider
+                panels += p
 
         # Add panels contributed by legacy WebAdmin plugins
         if IAdminPageProvider:
@@ -264,10 +276,12 @@ class LoggingAdminPanel(Component):
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'TRAC_ADMIN' in req.perm:
+        if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None):
             yield ('general', _('General'), 'logging', _('Logging'))
 
     def render_admin_panel(self, req, cat, page, path_info):
+        if getattr(self.env, 'parent', None):
+            raise PermissionError()
         log_type = self.env.log_type
         log_level = self.env.log_level
         log_file = self.env.log_file
@@ -439,10 +453,12 @@ class PluginAdminPanel(Component):
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'TRAC_ADMIN' in req.perm:
+        if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None):
             yield ('general', _('General'), 'plugin', _('Plugins'))
 
     def render_admin_panel(self, req, cat, page, path_info):
+        if getattr(self.env, 'parent', None):
+            raise PermissionError()
         req.perm.require('TRAC_ADMIN')
 
         if req.method == 'POST':

Modified: bloodhound/trunk/trac/trac/config.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/trac/trac/config.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/trac/trac/config.py (original)
+++ bloodhound/trunk/trac/trac/config.py Tue Apr  2 11:28:39 2013
@@ -567,7 +567,9 @@ class Option(object):
     """Descriptor for configuration options."""
 
     registry = {}
-    accessor = Section.get
+
+    def accessor(self, section, name, default):
+        return section.get(name, default)
 
     @staticmethod
     def get_registry(compmgr=None):
@@ -615,17 +617,20 @@ class Option(object):
 
 class BoolOption(Option):
     """Descriptor for boolean configuration options."""
-    accessor = Section.getbool
+    def accessor(self, section, name, default):
+        return section.getbool(name, default)
 
 
 class IntOption(Option):
     """Descriptor for integer configuration options."""
-    accessor = Section.getint
+    def accessor(self, section, name, default):
+        return section.getint(name, default)
 
 
 class FloatOption(Option):
     """Descriptor for float configuration options."""
-    accessor = Section.getfloat
+    def accessor(self, section, name, default):
+        return section.getfloat(name, default)
 
 
 class ListOption(Option):
@@ -673,7 +678,8 @@ class PathOption(Option):
     Relative paths are resolved to absolute paths using the directory
     containing the configuration file as the reference.
     """
-    accessor = Section.getpath
+    def accessor(self, section, name, default):
+        return section.getpath(name, default)
 
 
 class ExtensionOption(Option):

Modified: bloodhound/trunk/trac/trac/core.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/trac/trac/core.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/trac/trac/core.py (original)
+++ bloodhound/trunk/trac/trac/core.py Tue Apr  2 11:28:39 2013
@@ -208,7 +208,10 @@ class ComponentManager(object):
         if not self.is_enabled(cls):
             return None
         component = self.components.get(cls)
-        if not component:
+
+        # Leave other manager components out of extension point lists
+        # see bh:comment:5:ticket:438 and ticket:11121
+        if not component and not issubclass(cls, ComponentManager) :
             if cls not in ComponentMeta._components:
                 raise TracError('Component "%s" not registered' % cls.__name__)
             try:

Modified: bloodhound/trunk/trac/trac/tests/core.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/trac/trac/tests/core.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/trac/trac/tests/core.py (original)
+++ bloodhound/trunk/trac/trac/tests/core.py Tue Apr  2 11:28:39 2013
@@ -300,6 +300,71 @@ class ComponentTestCase(unittest.TestCas
         self.assertEquals('x', tests.next().test())
         self.assertRaises(StopIteration, tests.next)
 
+    def test_component_manager_component_isolation(self):
+        """
+        Verify that a component manager that is also a component will only
+        be listed in extension points for components instantiated in
+        its scope.
+
+        See bh:comment:5:ticket:438 and #11121
+        """
+        from trac.core import ComponentManager
+        class ManagerComponent(ComponentManager, Component):
+            tests = ExtensionPoint(ITest)
+            def __init__(self, foo, bar):
+                ComponentManager.__init__(self)
+                self.foo, self.bar = foo, bar
+
+        class YetAnotherManagerComponent(ComponentManager, Component):
+            implements(ITest)
+            def __init__(self, foo, bar):
+                ComponentManager.__init__(self)
+                self.foo, self.bar = foo, bar
+
+            # ITest methods
+            def test(self):
+                return self.foo + self.bar
+
+        class ComponentA(Component):
+            tests = ExtensionPoint(ITest)
+
+        class Extender(Component):
+            implements(ITest)
+            def test(self):
+                return 'x'
+
+        mgr = ManagerComponent('Test', 42)
+        yamc = YetAnotherManagerComponent('y', 'z')
+
+        assert yamc[ManagerComponent] is None 
+        assert mgr[YetAnotherManagerComponent] is None 
+        assert yamc[ComponentManager] is None 
+        assert self.compmgr[YetAnotherManagerComponent] is None 
+        assert mgr[ComponentManager] is None 
+        assert self.compmgr[ManagerComponent] is None 
+
+        self.assertTrue(any(c.__class__ is YetAnotherManagerComponent
+                            for c in ComponentA(yamc).tests))
+        self.assertFalse(any(c.__class__ is YetAnotherManagerComponent
+                             for c in ComponentA(self.compmgr).tests))
+        self.assertFalse(any(c.__class__ is YetAnotherManagerComponent
+                             for c in ComponentA(mgr).tests))
+        self.assertFalse(any(c.__class__ is ManagerComponent
+                             for c in ComponentA(yamc).tests))
+        self.assertFalse(any(c.__class__ is YetAnotherManagerComponent
+                             for c in mgr.tests))
+
+        results = [test.test() for test in ComponentA(yamc).tests]
+        self.assertEquals(['x', 'yz'], sorted(results))
+
+        results = [test.test() for test in ComponentA(self.compmgr).tests]
+        self.assertEquals(['x'], sorted(results))
+
+        results = [test.test() for test in ComponentA(mgr).tests]
+        self.assertEquals(['x'], sorted(results))
+        results = [test.test() for test in mgr.tests]
+        self.assertEquals(['x'], sorted(results))
+
     def test_instantiation_doesnt_enable(self):
         """
         Make sure that a component disabled by the ComponentManager is not
@@ -315,7 +380,6 @@ class ComponentTestCase(unittest.TestCas
         instance = ComponentA(mgr)
         self.assertEqual(None, mgr[ComponentA])
 
-
 def suite():
     return unittest.makeSuite(ComponentTestCase, 'test')
 

Modified: bloodhound/trunk/trac/trac/tests/env.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/trac/trac/tests/env.py?rev=1463489&r1=1463488&r2=1463489&view=diff
==============================================================================
--- bloodhound/trunk/trac/trac/tests/env.py (original)
+++ bloodhound/trunk/trac/trac/tests/env.py Tue Apr  2 11:28:39 2013
@@ -13,6 +13,7 @@ class EnvironmentTestCase(unittest.TestC
 
     def setUp(self):
         env_path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
+        self.addCleanup(self.cleanupEnvPath, env_path)
         self.env = Environment(env_path, create=True)
 
     def tearDown(self):
@@ -21,6 +22,10 @@ class EnvironmentTestCase(unittest.TestC
         self.env.shutdown() # really closes the db connections
         shutil.rmtree(self.env.path)
 
+    def cleanupEnvPath(self, path):
+        if os.path.exists(path):
+            shutil.rmtree(path)
+
     def test_get_version(self):
         """Testing env.get_version"""
         assert self.env.get_version() == db_default.db_version