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) > 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}&status=!closed&group=time&col=id&col=summary&col=owner&col=status&col=priority&order=priority&groupdesc=1&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}&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}&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}&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