incubator-bloodhound-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From and...@apache.org
Subject svn commit: r1445519 - in /incubator/bloodhound/trunk/bloodhound_search/bhsearch: api.py templates/bhsearch.html tests/web_ui.py web_ui.py whoosh_backend.py
Date Wed, 13 Feb 2013 09:40:38 GMT
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">
-              &gt; <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">
+            &gt; <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



Mime
View raw message