Author: andrej
Date: Wed Feb 13 09:40:38 2013
New Revision: 1445519
URL: http://svn.apache.org/r1445519
Log:
#361 add sorting functionality to grid view
Modified:
incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.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/api.py
URL:
http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py?rev=1445519&r1=1445518&r2=1445519&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py Wed Feb 13
09:40:38 2013
@@ -20,7 +20,7 @@
r"""Core Bloodhound Search components."""
from trac.config import ExtensionOption
-from trac.core import Interface, Component, ExtensionPoint
+from trac.core import Interface, Component, ExtensionPoint, TracError
ASC = "asc"
DESC = "desc"
@@ -44,6 +44,35 @@ class QueryResult(object):
self.facets = None
self.debug = {}
+class SortInstruction(object):
+ def __init__(self, field, order):
+ self.field = field
+ self.order = self._parse_sort_order(order)
+
+ def _parse_sort_order(self, order):
+ if not order:
+ return ASC
+ order = order.strip().lower()
+ if order == ASC:
+ return ASC
+ elif order == DESC:
+ return DESC
+ else:
+ raise TracError(
+ "Invalid sort order %s in sort instruction" % order)
+
+ def build_sort_expression(self):
+ return "%s %s" % (self.field, self.order)
+
+ def __str__(self):
+ return str(self.__dict__)
+
+ def __eq__(self, other):
+ if not isinstance(other, SortInstruction):
+ return False
+ return self.__dict__ == other.__dict__
+
+
class ISearchBackend(Interface):
"""Extension point interface for search backend systems.
@@ -97,8 +126,7 @@ class ISearchBackend(Interface):
Perform query implementation
:param query: Parsed query object
- :param sort: list of tuples with field name and sort order:
- [("field_name", "ASC")]
+ :param sort: list of SortInstruction objects
:param fields: list of fields to select
:param boost: list of fields with boost values
:param filter: filter query object
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=1445519&r1=1445518&r2=1445519&view=diff
==============================================================================
---
incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
(original)
+++
incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
Wed Feb 13 09:40:38 2013
@@ -57,12 +57,12 @@
<div id="content" class="row">
<div class="span12">
<h1>Advanced search</h1>
- <div class="btn-group">
<form id="fullsearch" action="${href.bhsearch()}" method="get">
<!--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}" />
<input py:if="active_view" type="hidden" name="view"
value="${active_view}" />
+ <input py:if="active_sort" type="hidden" name="sort"
value="${active_sort.expression}" />
<py:for each="active_filter in active_filter_queries">
<input type="hidden" name="fq" value="${active_filter.query}" />
</py:for>
@@ -74,8 +74,12 @@
<i class="icon-search icon-white"></i>
</button>
</div>
+
+ <div id="active_sort" py:if="active_sort">
+ Sort by: <strong>${active_sort.expression}</strong> (<a
href="${active_sort.href}">remove</a>)
+ </div>
+
</form>
- </div>
</div>
<div class="span12">
@@ -98,13 +102,12 @@
</ul>
</div>
- <py:if test="active_filter_queries">
- <div id="active_filter_queries">
- <py:for each="active_filter in active_filter_queries">
- > <a href="${active_filter.href}">${active_filter.label}</a>
- </py:for>
- </div>
- </py:if>
+ <!--Render filters breadcrumbs-->
+ <div id="active_filter_queries" py:if="active_filter_queries">
+ <py:for each="active_filter in active_filter_queries">
+ > <a href="${active_filter.href}">${active_filter.label}</a>
+ </py:for>
+ </div>
<div py:if="results" class="row">
<div class="span3 facets">
@@ -150,11 +153,15 @@
<thead>
<tr class="trac-columns">
<th py:for="header in headers"
- class="$header.name${(' desc' if
header.sort=='DESC' else ' asc') if header.sort else ''}">
+ class="$header.name${(' desc' if
header.sort=='desc' else ' asc') if header.sort else ''}" py:with="">
<?python asc = _('(ascending)'); desc =
_('(descending)') ?>
<a title="${_('Sort by %(col)s %(direction)s',
col=header.label,
- direction=(desc if header.sort=='ASC'
else asc))}"
- href="$header.href">${header.label}</a>
+ direction=(desc if header.sort=='asc'
else asc))}"
+ href="${header.href}">
+ ${header.label}
+ <i py:if="header.sort"
+ class="${'icon-chevron-down' if
header.sort=='desc' else 'icon-chevron-up'}"></i>
+ </a>
</th>
</tr>
</thead>
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=1445519&r1=1445518&r2=1445519&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py
(original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py Wed
Feb 13 09:40:38 2013
@@ -20,7 +20,7 @@
import unittest
from urllib import urlencode, unquote
-from bhsearch.api import ASC, DESC
+from bhsearch.api import ASC, DESC, SortInstruction
from bhsearch.tests.base import BaseBloodhoundSearchTest
from bhsearch.web_ui import RequestParameters
@@ -525,11 +525,15 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
data = self.process_request()
#assert
api_sort = data["debug"]["api_parameters"]["sort"]
- self.assertEqual([("component", ASC), ("milestone", DESC)], api_sort)
+ self.assertEqual(
+ [
+ SortInstruction("component", ASC),
+ SortInstruction("milestone", DESC),
+ ],
+ api_sort)
ids = [item["summary"] for item in data["results"].items]
self.assertEqual(["T2", "T1", "T3"], ids)
-
def test_that_title_is_set_for_free_text_view(self):
#arrange
self.insert_ticket("T1", component="c1", status="new", milestone="A")
@@ -540,6 +544,57 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
self.assertIn("title", data["results"].items[0])
+ def test_that_grid_header_has_correct_sort_when_default_sorting(self):
+ #arrange
+ self.insert_ticket("T1", component="c1", status="new", milestone="A")
+ #act
+ self.req.args[RequestParameters.QUERY] = "*"
+ self.req.args[RequestParameters.VIEW] = "grid"
+ data = self.process_request()
+ #assert
+ headers = data["headers"]
+ id_header = self._find_header(headers, "id")
+ self.assertIn("sort=id+asc", id_header["href"])
+ self.assertEquals(None, id_header["sort"])
+
+ time_header = self._find_header(headers, "time")
+ self.assertIn("sort=time+asc", time_header["href"])
+ self.assertEquals(None, time_header["sort"])
+
+ def test_that_grid_header_has_correct_sort_if_acs_sorting(self):
+ #arrange
+ self.insert_ticket("T1", component="c1", status="new", milestone="A")
+ #act
+ self.req.args[RequestParameters.QUERY] = "*"
+ self.req.args[RequestParameters.VIEW] = "grid"
+ self.req.args[RequestParameters.SORT] = "id"
+
+ data = self.process_request()
+ #assert
+ headers = data["headers"]
+ id_header = self._find_header(headers, "id")
+ self.assertIn("sort=id+desc", id_header["href"])
+ self.assertEquals("asc", id_header["sort"])
+
+ def test_that_active_sort_is_set(self):
+ #arrange
+ self.insert_ticket("T1", component="c1", status="new", milestone="A")
+ #act
+ self.req.args[RequestParameters.SORT] = "id, time desc"
+
+ data = self.process_request()
+ #assert
+ active_sort = data["active_sort"]
+ self.assertEquals("id, time desc", active_sort["expression"])
+ self.assertNotIn("sort=", active_sort["href"])
+
+ def _find_header(self, headers, name):
+ for header in headers:
+ if header["name"] == name:
+ return header
+ raise Exception("Header not found: %s" % name)
+
+
def _count_parameter_in_url(self, url, parameter_name, value):
parameter_to_find = (parameter_name, value)
parsed_parameters = parse_arg_list(url)
@@ -582,16 +637,16 @@ class RequestParametersTest(unittest.Tes
None,
self._evaluate_sort(" , , "))
self.assertEqual(
- [("f1", ASC),],
+ [SortInstruction("f1", ASC),],
self._evaluate_sort(" f1 "))
self.assertEqual(
- [("f1", ASC),],
+ [SortInstruction("f1", ASC),],
self._evaluate_sort(" f1 asc"))
self.assertEqual(
- [("f1", DESC),],
+ [SortInstruction("f1", DESC),],
self._evaluate_sort("f1 desc"))
self.assertEqual(
- [("f1", ASC), ("f2", DESC)],
+ [SortInstruction("f1", ASC), SortInstruction("f2", DESC)],
self._evaluate_sort("f1, f2 desc"))
def test_can_raise_error_on_invalid_sort_term(self):
@@ -600,6 +655,23 @@ class RequestParametersTest(unittest.Tes
self._evaluate_sort,
"f1 desc bb")
+ def test_can_create_href_with_single_sort(self):
+ href = RequestParameters(self.req).create_href(
+ sort=SortInstruction("field1", ASC))
+ href = unquote(href)
+ print href
+ self.assertIn("sort=field1+asc", href)
+
+ def test_can_create_href_with_multiple_sort(self):
+ href = RequestParameters(self.req).create_href(
+ sort=[
+ SortInstruction("field1", ASC),
+ SortInstruction("field2", DESC),
+ ])
+ href = unquote(href)
+ print href
+ self.assertIn("sort=field1+asc,+field2+desc", href)
+
def _evaluate_sort(self, sort_condition):
self.req.args[RequestParameters.SORT] = sort_condition
parameters = RequestParameters(self.req)
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=1445519&r1=1445518&r2=1445519&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py Wed Feb 13
09:40:38 2013
@@ -38,7 +38,7 @@ 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)
+ DESC, IndexFields, SortInstruction)
from trac.wiki.formatter import extract_link
SEARCH_PERMISSION = 'SEARCH_VIEW'
@@ -75,8 +75,8 @@ class RequestParameters(object):
self.filter_queries = self._remove_possible_duplications(
self.filter_queries)
- sort_string = req.args.getfirst(self.SORT)
- self.sort = self._parse_sort(sort_string)
+ self.sort_string = req.args.getfirst(self.SORT)
+ self.sort = self._parse_sort(self.sort_string)
self.pagelen = int(req.args.getfirst(
RequestParameters.PAGELEN,
@@ -102,8 +102,8 @@ class RequestParameters(object):
self.params[self.TYPE] = self.type
if self.filter_queries:
self.params[RequestParameters.FILTER_QUERY] = self.filter_queries
- if sort_string:
- self.params[RequestParameters.SORT] = sort_string
+ if self.sort_string:
+ self.params[RequestParameters.SORT] = self.sort_string
def _parse_sort(self, sort_string):
if not sort_string:
@@ -111,17 +111,6 @@ class RequestParameters(object):
sort_terms = sort_string.split(",")
sort = []
- def parse_sort_order(sort_order):
- sort_order = sort_order.lower()
- if sort_order == ASC:
- return ASC
- elif sort_order == DESC:
- return DESC
- else:
- raise TracError(
- "Invalid sort order %s in sort parameter %s" %
- (sort_order, sort_string))
-
for term in sort_terms:
term = term.strip()
if not term:
@@ -129,9 +118,9 @@ class RequestParameters(object):
term_parts = term.split()
parts_count = len(term_parts)
if parts_count == 1:
- sort.append((term_parts[0], ASC))
+ sort.append(SortInstruction(term_parts[0], ASC))
elif parts_count == 2:
- sort.append((term_parts[0], parse_sort_order(term_parts[1])))
+ sort.append(SortInstruction(term_parts[0], term_parts[1]))
else:
raise TracError("Invalid sort term %s " % term)
@@ -154,9 +143,16 @@ class RequestParameters(object):
force_filters=None,
view=None,
skip_view=False,
+ sort=None,
+ skip_sort = False,
):
params = copy.deepcopy(self.params)
+ if skip_sort:
+ self._delete_if_exists(params, self.SORT)
+ elif sort:
+ params[self.SORT] = self._create_sort_expression(sort)
+
#noquickjump parameter should be always set to 1 for urls
params[self.NO_QUICK_JUMP] = 1
@@ -183,6 +179,21 @@ class RequestParameters(object):
return self.req.href.bhsearch(**params)
+ def _create_sort_expression(self, sort):
+ """
+ Accepts single sort instruction e.g. SortInstruction(field, ASC) or
+ list of sort instructions e.g.
+ [SortInstruction(field1, ASC), SortInstruction(field2, DESC)]
+ """
+ if not sort:
+ return None
+
+ if isinstance(sort, SortInstruction):
+ return sort.build_sort_expression()
+
+ return ", ".join([item.build_sort_expression() for item in sort])
+
+
def _delete_if_exists(self, params, name):
if name in params:
del params[name]
@@ -288,6 +299,7 @@ class BloodhoundSearchModule(Component):
class RequestContext(object):
DATA_ACTIVE_FILTER_QUERIES = 'active_filter_queries'
DATA_ACTIVE_TYPE = "active_type"
+ DATA_ACTIVE_SORT = "active_sort"
DATA_TYPES = "types"
DATA_HEADERS = "headers"
DATA_ALL_VIEWS = "all_views"
@@ -309,7 +321,7 @@ class RequestContext(object):
VIEWS_WITH_KNOWN_FIELDS = [DATA_VIEW_GRID]
OBLIGATORY_FIELDS_TO_SELECT = [IndexFields.ID, IndexFields.TYPE]
- DEFAULT_SORT = [(SCORE, ASC), ("time", DESC)]
+ DEFAULT_SORT = [SortInstruction(SCORE, ASC), SortInstruction("time", DESC)]
def __init__(
self,
@@ -323,6 +335,7 @@ class RequestContext(object):
self.env = env
self.req = req
self.parameters = RequestParameters(req)
+ self.data = {'query': self.parameters.query}
self.search_participants = search_participants
self.default_view = default_view
self.all_grid_fields = all_grid_fields
@@ -330,6 +343,17 @@ class RequestContext(object):
self.view = None
self.page = self.parameters.page
self.pagelen = self.parameters.pagelen
+
+ if self.parameters.sort:
+ self.sort = self.parameters.sort
+ self.data[self.DATA_ACTIVE_SORT] = dict(
+ expression=self.parameters.sort_string,
+ href=self.parameters.create_href(skip_sort=True)
+ )
+ else:
+ self.sort = self.DEFAULT_SORT
+
+
self.allowed_participants, self.sorted_participants = \
self._get_allowed_participants(req)
@@ -341,17 +365,14 @@ class RequestContext(object):
self.active_type = None
self.active_participant = None
- self.data = {'query': self.parameters.query}
self._prepare_allowed_types()
self._prepare_active_filter_queries()
self._prepare_quick_jump()
+
self.fields = self._prepare_fields_and_view()
self.query_filter = self._prepare_query_filter()
self.facets = self._prepare_facets()
- self.sort = self.parameters.sort if self.parameters.sort \
- else self.DEFAULT_SORT
-
def _get_allowed_participants(self, req):
@@ -506,15 +527,28 @@ class RequestContext(object):
return fields_to_select
def _create_headers_item(self, field):
+ current_sort_direction = self._get_current_sort_direction_for_field(
+ field)
+ href_sort_direction = DESC if current_sort_direction == ASC else ASC
return dict(
name=field,
- href="",
+ href=self.parameters.create_href(
+ skip_page=True,
+ sort=SortInstruction(field, href_sort_direction)
+ ),
#TODO:add translated column label. Now it is really temporary
- #workaround
+ # workaround
label=field,
- sort=None,
+ sort=current_sort_direction,
)
+ def _get_current_sort_direction_for_field(self, field):
+ if self.sort and len(self.sort) == 1:
+ single_sort = self.sort[0]
+ if single_sort.field == field:
+ return single_sort.order
+ return None
+
def _prepare_query_filter(self):
query_filters = list(self.parameters.filter_queries)
if self.active_type:
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=1445519&r1=1445518&r2=1445519&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
(original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py Wed
Feb 13 09:40:38 2013
@@ -257,7 +257,9 @@ class WhooshBackend(Component):
if not sort:
return None
sortedby = []
- for (field, order) in sort:
+ for sort_instruction in sort:
+ field = sort_instruction.field
+ order = sort_instruction.order
if field.lower() == SCORE:
if self._is_desc(order):
#We can implement tis later by our own ScoreFacet with