You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by an...@apache.org on 2013/01/29 17:08:25 UTC
svn commit: r1439965 - in
/incubator/bloodhound/trunk/bloodhound_search/bhsearch: query_parser.py
templates/bhsearch.html tests/real_index_view.py tests/utils.py
tests/web_ui.py tests/whoosh_backend.py web_ui.py whoosh_backend.py
Author: andrej
Date: Tue Jan 29 16:08:25 2013
New Revision: 1439965
URL: http://svn.apache.org/viewvc?rev=1439965&view=rev
Log:
358 Add quick jump by TracLink in Bloodhound Search
Modified:
incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/real_index_view.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py Tue Jan 29 16:08:25 2013
@@ -24,7 +24,7 @@ from bhsearch.api import IQueryParser
from bhsearch.whoosh_backend import WhooshBackend
from trac.core import Component, implements
from whoosh import query
-from whoosh.qparser import MultifieldParser, QueryParser
+from whoosh.qparser import MultifieldParser
class DefaultQueryParser(Component):
implements(IQueryParser)
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html Tue Jan 29 16:08:25 2013
@@ -60,7 +60,8 @@
<form id="fullsearch" action="${href.bhsearch()}" method="get">
<p>
<input type="text" id="q" name="q" size="40" value="${query}" />
- <input type="hidden" name="noquickjump" value="1" />
+ <!--So far, we will not support noquickjump mode for form submission-->
+ <!--<input type="hidden" name="noquickjump" value="1" />-->
<input py:if="active_type" type="hidden" name="type" value="${active_type}" />
<py:for each="active_filter in active_filter_queries">
<input type="hidden" name="fq" value="${active_filter.query}" />
@@ -69,6 +70,13 @@
</p>
</form>
+ <py:if test="quickjump">
+ <dt id="quickjump">
+ <a href="${quickjump.href}" i18n:msg="name">Quickjump to ${quickjump.name}</a>
+ </dt>
+ <dd>${quickjump.description}</dd>
+ </py:if>
+
<!--This just a prototype implementation. Should be replaced by proper UI mocks-->
<div>
<!--Render type tabs: All, Ticket, Wiki, etc.-->
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/real_index_view.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/real_index_view.py?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/real_index_view.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/real_index_view.py Tue Jan 29 16:08:25 2013
@@ -45,9 +45,6 @@ class RealIndexTestCase(BaseBloodhoundSe
"../../../installer/bloodhound/environments/main")
self.env.path = real_env_path
self.whoosh_backend = WhooshBackend(self.env)
- self.search_api = BloodhoundSearchApi(self.env)
- self.web_ui = BloodhoundSearchModule(self.env)
- self.query_parser = DefaultQueryParser(self.env)
self.req = Mock(
perm=MockPerm(),
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py Tue Jan 29 16:08:25 2013
@@ -23,6 +23,7 @@ Test utils methods
"""
from pprint import pprint
import unittest
+from bhsearch.web_ui import BloodhoundSearchModule
from trac.ticket import Ticket, Milestone
from trac.wiki import WikiPage
@@ -84,7 +85,7 @@ class BaseBloodhoundSearchTest(unittest.
return milestone
def process_request(self):
- response = self.web_ui.process_request(self.req)
+ response = BloodhoundSearchModule(self.env).process_request(self.req)
url, data, x = response
print "Received url: %s data:" % url
pprint(data)
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py Tue Jan 29 16:08:25 2013
@@ -23,41 +23,45 @@ import shutil
from urllib import urlencode, unquote
-from bhsearch.api import BloodhoundSearchApi
from bhsearch.tests.utils import BaseBloodhoundSearchTest
-from bhsearch.ticket_search import TicketSearchParticipant
-from bhsearch.web_ui import BloodhoundSearchModule, RequestParameters
+from bhsearch.web_ui import RequestParameters
from bhsearch.whoosh_backend import WhooshBackend
from trac.test import EnvironmentStub, Mock, MockPerm
-from trac.ticket.api import TicketSystem
from trac.ticket import Ticket
from trac.util.datefmt import FixedOffset
from trac.util import format_datetime
-from trac.web import Href, arg_list_to_args, parse_arg_list
+from trac.web import Href, arg_list_to_args, parse_arg_list, RequestDone
-BHSEARCH_URL = "/main/bhsearch"
+BASE_PATH = "/main/"
+BHSEARCH_URL = BASE_PATH + "bhsearch"
DEFAULT_DOCS_PER_PAGE = 10
class WebUiTestCaseWithWhoosh(BaseBloodhoundSearchTest):
def setUp(self):
- self.env = EnvironmentStub(enable=['bhsearch.*'])
+ self.env = EnvironmentStub(enable=['trac.*', 'bhsearch.*'])
self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
- self.ticket_system = TicketSystem(self.env)
- self.whoosh_backend = WhooshBackend(self.env)
- self.whoosh_backend.recreate_index()
- self.search_api = BloodhoundSearchApi(self.env)
- self.ticket_participant = TicketSearchParticipant(self.env)
- self.web_ui = BloodhoundSearchModule(self.env)
+ whoosh_backend = WhooshBackend(self.env)
+ whoosh_backend.recreate_index()
self.req = Mock(
perm=MockPerm(),
chrome={'logo': {}},
href=Href("/main"),
+ base_path=BASE_PATH,
args=arg_list_to_args([]),
+ redirect=self.redirect
)
+ self.redirect_url = None
+ self.redirect_permanent = None
+
+ def redirect(self, url, permanent=False):
+ self.redirect_url = url
+ self.redirect_permanent = permanent
+ raise RequestDone
+
def tearDown(self):
shutil.rmtree(self.env.path)
self.env.reset_db()
@@ -407,6 +411,36 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
self.assertIn("type=ticket", ticket_facet_href)
self.assertNotIn("fq=", ticket_facet_href)
+ def test_that_there_is_no_quick_jump_on_ordinary_query(self):
+ #arrange
+ self.insert_ticket("T1", component="c1", status="new", milestone="A")
+ #act
+ self.req.args[RequestParameters.QUERY] = "*"
+ data = self.process_request()
+ #assert
+ self.assertNotIn("quickjump", data)
+
+ def test_can_redirect_on_ticket_id_query(self):
+ #arrange
+ self.insert_ticket("T1", component="c1", status="new", milestone="A")
+ #act
+ self.req.args[RequestParameters.QUERY] = "#1"
+ self.assertRaises(RequestDone, self.process_request)
+ #assert
+ self.assertEqual('/main/ticket/1', self.redirect_url)
+
+ def test_can_return_quick_jump_data_on_noquickjump(self):
+ #arrange
+ self.insert_ticket("T1", component="c1", status="new", milestone="A")
+ #act
+ self.req.args[RequestParameters.QUERY] = "#1"
+ self.req.args[RequestParameters.NO_QUICK_JUMP] = "1"
+ data = self.process_request()
+ #assert
+ quick_jump_data = data["quickjump"]
+ self.assertEqual('T1 (new)', quick_jump_data["description"])
+ self.assertEqual('/main/ticket/1', quick_jump_data["href"])
+
def _count_parameter_in_url(self, url, parameter_name, value):
parameter_to_find = (parameter_name, value)
parsed_parameters = parse_arg_list(url)
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py Tue Jan 29 16:08:25 2013
@@ -308,7 +308,6 @@ class WhooshBackendTestCase(BaseBloodhou
)
self.print_result(result)
self.assertEqual(1, result.hits)
- facets = result.docs[0]
self.assertEqual("1", result.docs[0]["id"])
@@ -322,7 +321,6 @@ class WhooshBackendTestCase(BaseBloodhou
)
self.print_result(result)
self.assertEqual(1, result.hits)
- facets = result.docs[0]
self.assertEqual("1", result.docs[0]["id"])
@@ -485,7 +483,8 @@ class WhooshEmptyFacetErrorWorkaroundTes
result_filter = query_parameters["filter"]
print result_filter
- self.assertEquals('(type:ticket AND milestone:empty)', str(result_filter))
+ self.assertEquals('(type:ticket AND milestone:empty)',
+ str(result_filter))
def test_does_interfere_query_filter_if_not_needed(self):
parsed_filter = self.parser.parse_filters(
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py Tue Jan 29 16:08:25 2013
@@ -33,10 +33,12 @@ from trac.util.presentation import Pagin
from trac.util.datefmt import format_datetime, user_time
from trac.web import IRequestHandler
from trac.util.translation import _
-from trac.web.chrome import INavigationContributor, ITemplateProvider, \
- add_link, add_stylesheet
-from bhsearch.api import BloodhoundSearchApi, ISearchParticipant, SCORE, ASC, \
- DESC, IndexFields
+from trac.util.html import find_element
+from trac.web.chrome import (INavigationContributor, ITemplateProvider,
+ add_link, add_stylesheet, web_context)
+from bhsearch.api import (BloodhoundSearchApi, ISearchParticipant, SCORE, ASC,
+ DESC, IndexFields)
+from trac.wiki.formatter import extract_link
SEARCH_PERMISSION = 'SEARCH_VIEW'
DEFAULT_RESULTS_PER_PAGE = 10
@@ -59,14 +61,15 @@ class RequestParameters(object):
def __init__(self, req):
self.req = req
- self.query = req.args.getfirst(RequestParameters.QUERY)
- if self.query == None:
+ self.query = req.args.getfirst(self.QUERY)
+ if self.query is None:
self.query = ""
+ else:
+ self.query = self.query.strip()
- #TODO: add quick jump functionality
- self.noquickjump = 1
+ self.no_quick_jump = int(req.args.getfirst(self.NO_QUICK_JUMP, '0'))
- self.filter_queries = req.args.getlist(RequestParameters.FILTER_QUERY)
+ self.filter_queries = req.args.getlist(self.FILTER_QUERY)
self.filter_queries = self._remove_possible_duplications(
self.filter_queries)
@@ -76,13 +79,16 @@ class RequestParameters(object):
self.pagelen = int(req.args.getfirst(
RequestParameters.PAGELEN,
DEFAULT_RESULTS_PER_PAGE))
- self.page = int(req.args.getfirst(RequestParameters.PAGE, '1'))
- self.type = req.args.getfirst(RequestParameters.TYPE, None)
+ self.page = int(req.args.getfirst(self.PAGE, '1'))
+ self.type = req.args.getfirst(self.TYPE, None)
self.params = {
- self.NO_QUICK_JUMP: self.noquickjump,
RequestParameters.FILTER_QUERY: []
}
+
+ if self.no_quick_jump > 0:
+ self.params[self.NO_QUICK_JUMP] = self.no_quick_jump
+
if self.query:
self.params[self.QUERY] = self.query
if self.pagelen != DEFAULT_RESULTS_PER_PAGE:
@@ -105,11 +111,14 @@ class RequestParameters(object):
type=None,
skip_type = False,
skip_page = False,
- filter_query = None,
- skip_filter_query = False,
- force_filters = None
+ additional_filter = None,
+ force_filters = None,
):
params = copy.deepcopy(self.params)
+
+ #noquickjump parameter should be always set to 1 for urls
+ params[self.NO_QUICK_JUMP] = 1
+
if page:
params[self.PAGE] = page
@@ -122,10 +131,9 @@ class RequestParameters(object):
if skip_type and self.TYPE in params:
del(params[self.TYPE])
- if skip_filter_query:
- params[self.FILTER_QUERY] = []
- elif filter_query and filter_query not in params[self.FILTER_QUERY]:
- params[self.FILTER_QUERY].append(filter_query)
+ if additional_filter and \
+ additional_filter not in params[self.FILTER_QUERY]:
+ params[self.FILTER_QUERY].append(additional_filter)
elif force_filters is not None:
params[self.FILTER_QUERY] = force_filters
@@ -175,28 +183,31 @@ class BloodhoundSearchModule(Component):
def process_request(self, req):
req.perm.assert_permission(SEARCH_PERMISSION)
parameters = RequestParameters(req)
- query_string = parameters.query
-
- #TODO add quick jump support
-
allowed_participants = self._get_allowed_participants(req)
data = {
- 'query': query_string,
+ 'query': parameters.query,
}
self._prepare_allowed_types(allowed_participants, parameters, data)
self._prepare_active_filter_queries(
parameters,
data,
)
+ working_query_string = parameters.query.strip()
+
#TBD: should search return results on empty query?
# if not any((
-# query_string,
+# working_query_string,
# parameters.type,
# parameters.filter_queries,
# )):
# return self._return_data(req, data)
+ self._prepare_quick_jump(
+ parameters,
+ working_query_string,
+ data)
+
query_filter = self._prepare_query_filter(
parameters,
allowed_participants)
@@ -205,7 +216,7 @@ class BloodhoundSearchModule(Component):
query_system = BloodhoundSearchApi(self.env)
query_result = query_system.query(
- query_string,
+ working_query_string,
pagenum=parameters.page,
pagelen=parameters.pagelen,
sort=parameters.sort,
@@ -250,6 +261,47 @@ class BloodhoundSearchModule(Component):
data['page_href'] = parameters.create_href()
return self._return_data(req, data)
+ def _prepare_quick_jump(self,
+ parameters,
+ working_query_string,
+ data):
+ if not working_query_string:
+ return
+ check_result = self._check_quickjump(
+ parameters.req,
+ working_query_string)
+ if check_result:
+ data["quickjump"] = check_result
+
+ #the method below is "copy/paste" from trac search/web_ui.py
+ def _check_quickjump(self, req, kwd):
+ """Look for search shortcuts"""
+ noquickjump = int(req.args.get('noquickjump', '0'))
+ # Source quickjump FIXME: delegate to ISearchSource.search_quickjump
+ quickjump_href = None
+ if kwd[0] == '/':
+ quickjump_href = req.href.browser(kwd)
+ name = kwd
+ description = _('Browse repository path %(path)s', path=kwd)
+ else:
+ context = web_context(req, 'search')
+ link = find_element(extract_link(self.env, context, kwd), 'href')
+ if link is not None:
+ quickjump_href = link.attrib.get('href')
+ name = link.children
+ description = link.attrib.get('title', '')
+ if quickjump_href:
+ # Only automatically redirect to local quickjump links
+ if not quickjump_href.startswith(req.base_path or '/'):
+ noquickjump = True
+ if noquickjump:
+ return {'href': quickjump_href, 'name': tag.EM(name),
+ 'description': description}
+ else:
+ req.redirect(quickjump_href)
+
+
+
def _prepare_allowed_types(self, allowed_participants, parameters, data):
active_type = parameters.type
if active_type and active_type not in allowed_participants:
@@ -332,17 +384,17 @@ class BloodhoundSearchModule(Component):
for field, facets_dict in result_facets.iteritems():
per_field_dict = dict()
for field_value, count in facets_dict.iteritems():
- if field==IndexFields.TYPE:
+ if field == IndexFields.TYPE:
href = parameters.create_href(
skip_page=True,
- skip_filter_query=True,
+ force_filters=[],
type=field_value)
else:
href = parameters.create_href(
skip_page=True,
- filter_query=self._create_field_term_expression(
- field,
- field_value)
+ additional_filter=self._create_term_expression(
+ field,
+ field_value)
)
per_field_dict[field_value] = dict(
count=count,
@@ -352,7 +404,7 @@ class BloodhoundSearchModule(Component):
data['facet_counts'] = facet_counts
- def _create_field_term_expression(self, field, field_value):
+ def _create_term_expression(self, field, field_value):
if field_value is None:
query = "NOT (%s:*)" % field
elif isinstance(field_value, basestring):
@@ -366,7 +418,7 @@ class BloodhoundSearchModule(Component):
type = parameters.type
if type in allowed_participants:
query_filters.append(
- self._create_field_term_expression(IndexFields.TYPE, type))
+ self._create_term_expression(IndexFields.TYPE, type))
else:
self.log.debug("Unsupported type in web request: %s", type)
return query_filters
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py?rev=1439965&r1=1439964&r2=1439965&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py Tue Jan 29 16:08:25 2013
@@ -28,7 +28,8 @@ from trac.config import Option
from trac.util.text import empty
from trac.util.datefmt import utc
from whoosh.fields import Schema, ID, DATETIME, KEYWORD, TEXT
-from whoosh import index, sorting, query
+#from whoosh import index, sorting, query
+import whoosh
from whoosh.writing import AsyncWriter
from datetime import datetime
@@ -149,11 +150,11 @@ class WhooshBackend(Component):
def recreate_index(self):
self.log.info('Creating Whoosh index in %s' % self.index_dir)
self._make_dir_if_not_exists()
- return index.create_in(self.index_dir, schema=self.SCHEMA)
+ return whoosh.index.create_in(self.index_dir, schema=self.SCHEMA)
def _open_or_create_index_if_missing(self):
- if index.exists_in(self.index_dir):
- return index.open_dir(self.index_dir)
+ if whoosh.index.exists_in(self.index_dir):
+ return whoosh.index.open_dir(self.index_dir)
else:
return self.recreate_index()
@@ -198,7 +199,7 @@ class WhooshBackend(Component):
pagelen = pagelen,
sortedby = sortedby,
groupedby = groupedby,
- maptype=sorting.Count,
+ maptype=whoosh.sorting.Count,
#workaround of Whoosh bug, read method __doc__
#filter = filter,
)
@@ -214,7 +215,7 @@ class WhooshBackend(Component):
query_filter):
if not query_filter:
return query_expression
- return query.And((query_expression, query_filter))
+ 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)
@@ -244,12 +245,12 @@ class WhooshBackend(Component):
def _prepare_groupedby(self, facets):
if not facets:
return None
- groupedby = sorting.Facets()
+ groupedby = whoosh.sorting.Facets()
for facet_name in facets:
groupedby.add_field(
facet_name,
allow_overlap=True,
- maptype=sorting.Count)
+ maptype=whoosh.sortingwhoosh.Count)
return groupedby
def _prepare_sortedby(self, sort):
@@ -263,9 +264,9 @@ class WhooshBackend(Component):
# "score DESC" support
raise TracError(
"Whoosh does not support DESC score ordering.")
- sort_condition = sorting.ScoreFacet()
+ sort_condition = whoosh.sorting.ScoreFacet()
else:
- sort_condition = sorting.FieldFacet(
+ sort_condition = whoosh.sorting.FieldFacet(
field,
reverse=self._is_desc(order))
sortedby.append(sort_condition)
@@ -316,6 +317,7 @@ class WhooshBackend(Component):
return result_doc
def _load_facets(self, page):
+ """This method can be also used by unit-tests"""
non_paged_results = page.results
facet_names = non_paged_results.facet_names()
if not facet_names:
@@ -393,15 +395,15 @@ class WhooshEmptyFacetErrorWorkaround(Co
self._find_and_fix_condition(query_parameters["query"])
def _find_and_fix_condition(self, filter_condition):
- if isinstance(filter_condition, query.CompoundQuery):
- subqueries = list(filter_condition.subqueries)
- for i, subquery in enumerate(subqueries):
+ if isinstance(filter_condition, whoosh.query.CompoundQuery):
+ sub_queries = list(filter_condition.subqueries)
+ for i, subquery in enumerate(sub_queries):
term_to_replace = self._find_and_fix_condition(subquery)
if term_to_replace:
filter_condition.subqueries[i] = term_to_replace
- elif isinstance(filter_condition, query.Not):
+ elif isinstance(filter_condition, whoosh.query.Not):
not_query = filter_condition.query
- if isinstance(not_query, query.Every) and \
+ if isinstance(not_query, whoosh.query.Every) and \
not_query.fieldname in self.should_not_be_empty_fields:
- return query.Term(not_query.fieldname, self.NULL_MARKER)
+ return whoosh.query.Term(not_query.fieldname, self.NULL_MARKER)
return None