incubator-bloodhound-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From j...@apache.org
Subject svn commit: r1440987 [1/2] - in /incubator/bloodhound/branches/bep_0003_multiproduct: ./ bloodhound_search/ bloodhound_search/bhsearch/ bloodhound_search/bhsearch/default-pages/ bloodhound_search/bhsearch/search_resources/ bloodhound_search/bhsearch/te...
Date Thu, 31 Jan 2013 14:41:22 GMT
Author: jure
Date: Thu Jan 31 14:41:22 2013
New Revision: 1440987

URL: http://svn.apache.org/viewvc?rev=1440987&view=rev
Log:
Sync merge from trunk


Added:
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/
      - copied from r1440984, incubator/bloodhound/trunk/bloodhound_search/bhsearch/search_resources/
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/milestone_search.py
      - copied unchanged from r1440984, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/milestone_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/real_index_view.py
      - copied unchanged from r1440984, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/real_index_view.py
Removed:
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/ticket_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/wiki_search.py
Modified:
    incubator/bloodhound/branches/bep_0003_multiproduct/   (props changed)
    incubator/bloodhound/branches/bep_0003_multiproduct/NOTICE
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/admin.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/default-pages/BloodhoundSearch
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/query_parser.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/ticket_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/utils.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/wiki_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py

Propchange: incubator/bloodhound/branches/bep_0003_multiproduct/
------------------------------------------------------------------------------
  Merged /incubator/bloodhound/trunk:r1433321-1440984

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/NOTICE
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/NOTICE?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/NOTICE (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/NOTICE Thu Jan 31 14:41:22 2013
@@ -12,7 +12,7 @@ Bootstrap - licensed under the Apache Li
 This product includes software (Bootstrap) developed by
 Twitter (http://twitter.github.com/bootstrap/)
 
-JQuery - licensed under the MIT License
-This product includes software (JQuery) developed by
-JQuery (http://jquery.org/)
+jQuery - licensed under the MIT License
+This product includes software (jQuery) developed by
+jQuery (http://jquery.org/)
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/admin.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/admin.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/admin.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/admin.py Thu Jan 31 14:41:22 2013
@@ -19,7 +19,8 @@
 #  under the License.
 
 r"""Administration commands for Bloodhound Search."""
-from trac.admin import *
+from trac.core import Component, implements
+from trac.admin import IAdminCommandProvider
 from bhsearch.api import BloodhoundSearchApi
 
 class BloodhoundSearchAdmin(Component):

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py Thu Jan 31 14:41:22 2013
@@ -19,9 +19,8 @@
 #  under the License.
 
 r"""Core Bloodhound Search components."""
-
-from trac.core import *
 from trac.config import ExtensionOption
+from trac.core import Interface, Component, ExtensionPoint
 
 ASC = "asc"
 DESC = "desc"
@@ -33,6 +32,11 @@ class IndexFields(object):
     TIME = 'time'
     AUTHOR = 'author'
     CONTENT = 'content'
+    STATUS = 'status'
+    DUE = 'due'
+    COMPLETED = 'completed'
+    MILESTONE = 'milestone'
+    COMPONENT = 'component'
 
 class QueryResult(object):
     def __init__(self):
@@ -55,7 +59,7 @@ class ISearchBackend(Interface):
         Called when new document instance must be added
         """
 
-    def delete_doc(type, id, **kwargs):
+    def delete_doc(doc_type, doc_id, **kwargs):
         """
         Delete document from index
         """
@@ -90,14 +94,15 @@ class ISearchBackend(Interface):
         """
         Perform query implementation
 
-        :param query:
-        :param sort:
-        :param fields:
-        :param boost:
-        :param filter:
-        :param facets:
-        :param pagenum:
-        :param pagelen:
+        :param query: Parsed query object
+        :param sort: list of tuples  with field name and sort order:
+            [("field_name", "ASC")]
+        :param fields: list of fields to select
+        :param boost: list of fields with boost values
+        :param filter: filter query object
+        :param facets: list of facet fields
+        :param pagenum: page number
+        :param pagelen: page length
         :return: ResultsPage
         """
 
@@ -124,13 +129,42 @@ class ISearchParticipant(Interface):
     def get_title():
         """Return resource title"""
 
+    def get_default_facets():
+        """Return default facets for the specific resource type"""
+
 class IQueryParser(Interface):
     """Extension point for Bloodhound Search query parser.
     """
 
-    def parse(query_string, req = None):
+    def parse(query_string):
         """Parse query from string"""
 
+    def parse_filters(filters):
+        """Parse query filters"""
+
+class IDocIndexPreprocessor(Interface):
+    """Extension point for Bloodhound Search document pre-processing before
+    adding or update documents into index.
+    """
+
+    def pre_process(doc):
+        """Process document"""
+
+class IResultPostprocessor(Interface):
+    """Extension point for Bloodhound Search result post-processing before
+    returning result to caller.
+    """
+
+    def post_process(query_result):
+        """Process document"""
+
+class IQueryPreprocessor(Interface):
+    """Extension point for Bloodhound Search query pre processing.
+    """
+
+    def query_pre_process(query_parameters):
+        """Process query parameters"""
+
 class BloodhoundSearchApi(Component):
     """Implements core indexing functionality, provides methods for
     searching, adding and deleting documents from index.
@@ -145,6 +179,10 @@ class BloodhoundSearchApi(Component):
         'Name of the component implementing Bloodhound Search query \
         parser.')
 
+    index_pre_processors = ExtensionPoint(IDocIndexPreprocessor)
+    result_post_processors = ExtensionPoint(IResultPostprocessor)
+    query_processors = ExtensionPoint(IQueryPreprocessor)
+
     index_participants = ExtensionPoint(IIndexParticipant)
 
     def query(self, query, sort = None, fields = None,
@@ -170,19 +208,28 @@ class BloodhoundSearchApi(Component):
 
         parsed_query = self.parser.parse(query)
 
+        parsed_filters = self.parser.parse_filters(filter)
         # TODO: add query parsers and meta keywords post-parsing
 
         # TODO: apply security filters
 
-        query_result = self.backend.query(
+        query_parameters = dict(
             query = parsed_query,
             sort = sort,
             fields = fields,
-            filter = filter,
+            filter = parsed_filters,
             facets = facets,
             pagenum = pagenum,
             pagelen = pagelen,
+            boost = boost,
         )
+        for query_processor in self.query_processors:
+            query_processor.query_pre_process(query_parameters)
+
+        query_result = self.backend.query(**query_parameters)
+
+        for post_processor in self.result_post_processors:
+            post_processor.post_process(query_result)
 
         return query_result
 
@@ -192,13 +239,19 @@ class BloodhoundSearchApi(Component):
         self.log.info('Rebuilding the search index.')
         self.backend.recreate_index()
         operation_data = self.backend.start_operation()
+        doc = None
         try:
             for participant in self.index_participants:
                 docs = participant.get_entries_for_index()
                 for doc in docs:
-                    self.backend.add_doc(doc, **operation_data)
+#                    if doc["id"] == u'TracFastCgi':
+                    self._add_doc(doc, **operation_data)
+            doc = None
             self.backend.commit(True, **operation_data)
-        except:
+        except Exception, ex:
+            self.log.error(ex)
+            if doc:
+                self.log.error("Doc that triggers the error: %s" % doc)
             self.backend.cancel(**operation_data)
             raise
 
@@ -209,7 +262,7 @@ class BloodhoundSearchApi(Component):
                 doc[IndexFields.TYPE],
                 old_id,
                 **operation_data)
-            self.backend.add_doc(doc, **operation_data)
+            self._add_doc(doc, **operation_data)
             self.backend.commit(False, **operation_data)
         except:
             self.backend.cancel(**operation_data)
@@ -225,27 +278,33 @@ class BloodhoundSearchApi(Component):
 
         The doc must be dictionary with obligatory "type" field
         """
+
         operation_data = self.backend.start_operation()
         try:
-            self.backend.add_doc(doc, **operation_data)
+            self._add_doc(doc, **operation_data)
             self.backend.commit(False, **operation_data)
         except:
             self.backend.cancel(**operation_data)
             raise
 
 
-    def delete_doc(self, type, id):
+    def delete_doc(self, doc_type, doc_id):
         """Add a document from underlying search backend.
 
         The doc must be dictionary with obligatory "type" field
         """
         operation_data = self.backend.start_operation()
         try:
-            self.backend.delete_doc(type, id, **operation_data)
+            self.backend.delete_doc(doc_type, doc_id, **operation_data)
             self.backend.commit(False, **operation_data)
         except:
             self.backend.cancel(**operation_data)
             raise
 
 
+    def _add_doc(self, doc, **operation_data):
+        for preprocessor in self.index_pre_processors:
+            preprocessor.pre_process(doc)
+        self.backend.add_doc(doc, **operation_data)
+
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/default-pages/BloodhoundSearch
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/default-pages/BloodhoundSearch?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/default-pages/BloodhoundSearch (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/default-pages/BloodhoundSearch Thu Jan 31 14:41:22 2013
@@ -1,3 +1,71 @@
 = Bloodhound Search =
 
-TODO: add docs here
\ No newline at end of file
+Current version is prototype phase 1. Read more about proposed functionality on [wiki:Proposals/BEP-0004 BEP-0004]
+
+== How to enable Bloodhound Search==
+ * Currently, Bloodhound Search is available only for dev configuration. Make sure that Bloodhound was installed using requirements-dev.txt:
+{{{
+pip install -r requirements-dev.txt
+}}}
+ * enable bhsearch.* components in trac.ini file
+{{{
+[components]
+...
+bhsearch.* = enabled
+}}}
+ * run initial index rebuild using trac-admin command line tool. TODO: web interface should be supported later
+{{{
+trac-admin <path_to_trac_environment> bhsearch rebuild
+}}}
+ * Bloodhound Search page will be available in Apps menu | Bloodhound Search
+
+== Search == #search
+Query syntax is described on wiki:Proposals/BEP-0004/ResourceQuery. Currently supported search queries are: 
+* List of all items: "*"
+* Free text search through indexed fields e.g. "some text"
+* Combination of free text and field specific query e.g. "test status:new keyword:starter" (default operator is AND)
+* Field specific query e.g. "status:new keyword:starter" (default operator is AND)
+* Boolean queries e.g. "test OR (status:new and keyword:starter)"
+
+Currently, only tickets and wiki pages with the following fields are indexed. Later more resources and fields will be added.
+
+Common fields for all resources:
+ * id - resource id. For ticket it is ticket id, for wiki it will wiki page name.
+ * type - resource type e.g. ticket, wiki etc.
+ * product - product name
+ * time - resource change time
+ * author - resource author. For ticket it is ticket reporter.
+ * content - ticket description, wiki page content
+ * changes - currently, only ticket comments are indexed
+
+Ticket specific fields:
+ * component
+ * status 
+ * resolution
+ * keywords
+ * milestone
+ * summary
+
+Default sort order is by boost score ASC and time DESC
+
+Default query boosting is: id = 6, type = 2, summary = 5, author = 3, milestone = 2, keywords = 2, component = 2, status = 2, content = 1, changes = 1, 
+
+== Prototype phase 2 limitations == #limitations
+ * Only tickets and wiki pages are indexed.
+ * UI does not support faceting
+ * Search  does not support meta keywords 
+ * Quick jump by ticket id and wiki name is not supported
+ * Possible bugs and inconveniences expected for prototype 
+
+== To be done ==
+Please consider below not complete list of TODOs.
+ * UI design for search results including facets and resource specific presentation. Initial proposal can be found on [wiki:Proposals/BEP-0004#resultview BEP-0004]
+ * Add facet support for search UI
+ * Improve index quality e.d. add more resources in index (milestones etc.), index more fields for ticket. Support pluggable schema generation.
+ * Improve search quality
+ * Improve index consistency. [=#consistency]  
+   * Current implementation uses ITicketChangeListener interface that triggers after DB committed. There is some chances to get inconsistency between DB and index. In this case reindex is required. One of the possible solution is contact Trac community to introduce a new within transaction  I<Resource>ChangingListener interface, save changes into additional table and index changes in separate thread.
+   * Some ticket changes are not reflected in event interfaces e.g. Version and Component renaming. One of the possible solution is to contact Trac community to introduce new interfaces.
+   * Alternative solution is triggering events from SQL proxy developed on multi-product branch
+   * Usage of DB triggers is another alternative
+ * A lot more things to do :)
\ No newline at end of file

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/query_parser.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/query_parser.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/query_parser.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/query_parser.py Thu Jan 31 14:41:22 2013
@@ -23,35 +23,51 @@ r"""Provides Bloodhound Search query par
 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
 
 class DefaultQueryParser(Component):
     implements(IQueryParser)
 
+    #todo: make field boost configurable e.g. read from config setting.
+    #This is prototype implementation ,the fields boost must be tuned later
+    field_boosts = dict(
+        id = 6,
+        type = 2,
+        summary = 5,
+        author = 3,
+        milestone = 2,
+        keywords = 2,
+        component = 2,
+        status = 2,
+        content = 1,
+        changes = 1,
+    )
+    parser = MultifieldParser(
+        field_boosts.keys(),
+        WhooshBackend.SCHEMA,
+        fieldboosts=field_boosts
+    )
+
     def parse(self, query_string):
-        #todo: make field boost configurable e.g. read from config setting
-        #this is prototype implementation ,the fields boost must be tuned later
-        field_boosts = dict(
-            id = 6,
-            type = 2,
-            summary = 5,
-            author = 3,
-            milestone = 2,
-            keywords = 2,
-            component = 2,
-            status = 2,
-            content = 1,
-            changes = 1,
-        )
-        parser = MultifieldParser(
-            field_boosts.keys(),
-            WhooshBackend.SCHEMA,
-            fieldboosts=field_boosts
-        )
+        query_string = query_string.strip()
+
+        if query_string == "" or query_string == "*" or query_string == "*:*":
+            return query.Every()
+
         query_string = unicode(query_string)
-        parsed_query = parser.parse(query_string)
+        parsed_query = self.parser.parse(query_string)
 
-        #todo: impelement pluggable mechanizem for query post processing
+        #todo: impalement pluggable mechanism for query post processing
         #e.g. meta keyword replacement etc.
         return parsed_query
 
+    def parse_filters(self, filters):
+        """Parse query filters"""
+        if not filters:
+            return None
+        parsed_filters = [self._parse_filter(filter) for filter in filters]
+        return query.And(parsed_filters).normalize()
+
+    def _parse_filter(self, filter):
+        return self.parse(unicode(filter))
\ No newline at end of file

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/templates/bhsearch.html Thu Jan 31 14:41:22 2013
@@ -25,6 +25,19 @@
       xmlns:i18n="http://genshi.edgewall.org/i18n"
       xmlns:xi="http://www.w3.org/2001/XInclude">
   <xi:include href="layout.html" />
+
+  <py:def function="display_value(value)">
+    <py:choose test="value">
+      <py:when test="None">
+        <span class="label">empty</span>
+      </py:when>
+      <py:when test="''">
+        <span class="label">empty</span>
+      </py:when>
+      <py:otherwise>${value}</py:otherwise>
+    </py:choose>
+  </py:def>
+
   <head>
     <title py:choose="">
       <py:when test="query">Bloodhound Search Results</py:when>
@@ -41,48 +54,93 @@
     </script>
   </head>
   <body>
-    <div id="content" class="search">
+    <div id="content" class="row">
 
       <h1>This page provides prototype functionality.</h1>
-      <h1><label for="q">Search</label></h1>
       <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}" />
+          </py:for>
           <input type="submit" value="${_('Search')}" />
         </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>
-        <ul class="nav nav-tabs" id="mainnav">
+        <!--Render type tabs: All, Ticket, Wiki, etc.-->
+        <ul class="nav nav-tabs" id="search_types">
           <li py:for="idx, item in enumerate(i for i in types)"
-              class="${classes(first_last(idx, types), active=item.active)}"><a href="${item.href}">${item.label}</a></li>
+              class="${classes(first_last(idx, types), active=item.active)}">
+                <a href="${item.href}">${item.label}</a>
+          </li>
 
         </ul>
       </div>
 
-      <py:if test="results"><hr />
-        <h2 py:if="results">
-          Results <small>(${results.displayed_items()})</small>
-        </h2>
-        <div>
-          <dl id="results">
-
-            <!--This just a prototype implementation. Should be replaced by proper UI mocks-->
-            <py:for each="result in results">
-              <dt><a href="${result.href}" class="searchable">${result.title}</a></dt>
-              <dd class="searchable">${result.excerpt}</dd>
-              <dd>
-                <py:if test="result.author"><span class="author" i18n:msg="author">By ${format_author(result.author)}</span> &mdash;</py:if>
-                <span class="date">${result.date}</span>
-              </dd>
-            </py:for>
-          </dl>
+      <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>
+
+      <py:if test="results">
+        <div class="span8">
+          <h2 py:if="results">
+            Results <small>(${results.displayed_items()})</small>
+          </h2>
+          <div>
+            <dl id="results">
+
+              <py:for each="result in results">
+                <dt><a href="${result.href}" class="searchable">${result.title}</a></dt>
+                <dd class="searchable">${result.excerpt}</dd>
+                <dd>
+                  <py:if test="result.author"><span class="author" i18n:msg="author">By ${format_author(result.author)}</span> &mdash;</py:if>
+                  <span class="date">${result.date}</span>
+                </dd>
+              </py:for>
+            </dl>
+          </div>
+
+          <xi:include py:with="paginator = results" href="bh_page_index.html" />
+        </div>
+
+        <div class="span4">
+          <py:if test="facet_counts">
+            <!--Render facet counts-->
+            <h3>Facets</h3>
+            <ul id="facet_counts">
+              <li py:for="field, per_value_counts in facet_counts.iteritems()">
+                <h4 style="display: inline;">${field}</h4>
+                <ul id="facet_counts_value">
+                  <li py:for="value, item in per_value_counts.iteritems()">
+                    <a href="${item.href}"><strong>${display_value(value)}</strong></a>
+                    <span class="badge badge-info">${item.count}</span>
+                  </li>
+                  </ul>
+              </li>
+
+            </ul>
+          </py:if>
         </div>
-        <xi:include py:with="paginator = results" href="bh_page_index.html" />
+
       </py:if>
 
+
       <div class="span12"
           py:if="query and not (results or quickjump)">
         <p id="notfound" class="alert">
@@ -97,5 +155,4 @@
 
     </div>
   </body>
-</html>
-
+</html>
\ No newline at end of file

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py Thu Jan 31 14:41:22 2013
@@ -18,7 +18,8 @@
 #  specific language governing permissions and limitations
 #  under the License.
 import unittest
-from bhsearch.tests import whoosh_backend, index_with_whoosh, web_ui, ticket_search, api, wiki_search
+from bhsearch.tests import whoosh_backend, index_with_whoosh, web_ui, \
+    ticket_search, api, wiki_search
 
 def suite():
     suite = unittest.TestSuite()

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/api.py Thu Jan 31 14:41:22 2013
@@ -20,10 +20,10 @@
 import unittest
 import tempfile
 import shutil
-from bhsearch.api import BloodhoundSearchApi
+from bhsearch.api import BloodhoundSearchApi, ASC
 from bhsearch.query_parser import DefaultQueryParser
 from bhsearch.tests.utils import BaseBloodhoundSearchTest
-from bhsearch.ticket_search import TicketSearchParticipant
+from bhsearch.search_resources.ticket_search import TicketSearchParticipant
 
 from bhsearch.whoosh_backend import WhooshBackend
 from trac.test import EnvironmentStub
@@ -100,6 +100,26 @@ class ApiQueryWithWhooshTestCase(BaseBlo
         docs = results.docs
         self.assertEqual("summary1 keyword", docs[0]["summary"])
 
+    def test_that_filter_queries_applied(self):
+        #arrange
+        self.insert_ticket("t1", status="closed", component = "c1")
+        self.insert_ticket("t2", status="closed", component = "c1")
+        self.insert_ticket("t3", status="closed",
+            component = "NotInFilterCriteria")
+        #act
+        results = self.search_api.query(
+            "*",
+            filter= ['status:"closed"', 'component:"c1"'],
+            sort= [("id", ASC)]
+        )
+        self.print_result(results)
+        #assert
+        self.assertEqual(2, results.hits)
+        docs = results.docs
+        self.assertEqual("t1", docs[0]["summary"])
+        self.assertEqual("t2", docs[1]["summary"])
+
+
 #TODO: check this later
 #    @unittest.skip("Check with Whoosh community")
 #    def test_can_search_id_and_summary(self):

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py Thu Jan 31 14:41:22 2013
@@ -17,17 +17,17 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
-from datetime import datetime
 
 import unittest
 import tempfile
 import shutil
 from bhsearch.api import BloodhoundSearchApi
+from bhsearch.milestone_search import MilestoneIndexer
 from bhsearch.tests.utils import BaseBloodhoundSearchTest
-from bhsearch.ticket_search import TicketIndexer
+from bhsearch.search_resources.ticket_search import TicketIndexer
 
 from bhsearch.whoosh_backend import WhooshBackend
-from bhsearch.wiki_search import WikiIndexer
+from bhsearch.search_resources.wiki_search import WikiIndexer
 from trac.test import EnvironmentStub
 from trac.ticket.api import TicketSystem
 
@@ -41,6 +41,7 @@ class IndexWhooshTestCase(BaseBloodhound
         self.search_api = BloodhoundSearchApi(self.env)
         self.ticket_indexer = TicketIndexer(self.env)
         self.wiki_indexer = WikiIndexer(self.env)
+        self.milestone_indexer = MilestoneIndexer(self.env)
         self.ticket_system = TicketSystem(self.env)
 
     def tearDown(self):
@@ -110,6 +111,17 @@ class IndexWhooshTestCase(BaseBloodhound
         self.print_result(results)
         self.assertEqual(2, results.hits)
 
+    def test_can_reindex_milestones(self):
+        self.insert_milestone("M1")
+        self.insert_milestone("M2")
+        self.whoosh_backend.recreate_index()
+        #act
+        self.search_api.rebuild_index()
+        #assert
+        results = self.search_api.query("*:*")
+        self.print_result(results)
+        self.assertEqual(2, results.hits)
+
 
 def suite():
     return unittest.makeSuite(IndexWhooshTestCase, 'test')

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/ticket_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/ticket_search.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/ticket_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/ticket_search.py Thu Jan 31 14:41:22 2013
@@ -17,12 +17,11 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
-from datetime import datetime
-
 import unittest
 import tempfile
+
 from bhsearch.tests.utils import BaseBloodhoundSearchTest
-from bhsearch.ticket_search import TicketIndexer
+from bhsearch.search_resources.ticket_search import TicketIndexer
 
 from trac.test import EnvironmentStub
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/utils.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/utils.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/utils.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/utils.py Thu Jan 31 14:41:22 2013
@@ -21,15 +21,16 @@
 r"""
 Test utils methods
 """
-import pprint
+from pprint import pprint
 import unittest
-from trac.ticket import Ticket
+from bhsearch.web_ui import BloodhoundSearchModule
+from trac.ticket import Ticket, Milestone
 from trac.wiki import WikiPage
 
 class BaseBloodhoundSearchTest(unittest.TestCase):
     def print_result(self, result):
         print "Received result:"
-        pprint.pprint(result.__dict__)
+        pprint(result.__dict__)
 
     def create_dummy_ticket(self, summary = None):
         if not summary:
@@ -57,8 +58,42 @@ class BaseBloodhoundSearchTest(unittest.
         return page
 
     def insert_wiki(self, name, text = None, **kw):
-        """Helper for inserting a ticket into the database"""
         text = text or "Dummy text"
         page = self.create_wiki(name, text, **kw)
         return page.save("dummy author", "dummy comment", "::1")
 
+    def insert_milestone(self, name, description = None):
+        milestone = self.create_milestone(
+            name = name,
+            description = description)
+        return milestone.insert()
+
+    def create_milestone(self, name, description = None):
+        milestone = Milestone(self.env)
+        milestone.name = name
+        if description is not None:
+            milestone.description = description
+        return milestone
+
+    def change_milestone(self, name_to_change, name=None, description=None):
+        milestone = Milestone(self.env, name_to_change)
+        if name is not None:
+            milestone.name = name
+        if description is not None:
+            milestone.description = description
+        milestone.update()
+        return milestone
+
+    def process_request(self):
+        response = BloodhoundSearchModule(self.env).process_request(self.req)
+        url, data, x = response
+        print "Received url: %s data:" % url
+        pprint(data)
+        if data.has_key("results"):
+            print "results :"
+            pprint(data["results"].__dict__)
+        return data
+
+
+
+

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py Thu Jan 31 14:41:22 2013
@@ -17,76 +17,69 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
-from pprint import pprint
-
 import unittest
 import tempfile
 import shutil
-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 urllib import urlencode, unquote
+
+from bhsearch.tests.utils import BaseBloodhoundSearchTest
+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
+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.perm = PermissionSystem(self.env)
-        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.ticket_system = TicketSystem(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"),
-            args={},
+            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()
 
-    def _process_request(self):
-        response = self.web_ui.process_request(self.req)
-        url, data, x = response
-        print "Received url: %s data:" % url
-        pprint(data)
-        if data.has_key("results"):
-            print "results :"
-            pprint(data["results"].__dict__)
-        return data
-
     def test_can_process_empty_request(self):
-        data = self._process_request()
+        data = self.process_request()
         self.assertEqual("", data["query"])
 
     def test_can_process_query_empty_data(self):
         self.req.args[RequestParameters.QUERY] = "bla"
-        data = self._process_request()
+        data = self.process_request()
         self.assertEqual("bla", data["query"])
         self.assertEqual([], data["results"].items)
 
     def test_can_process_first_page(self):
         self._insert_tickets(5)
         self.req.args[RequestParameters.QUERY] = "summary:test"
-        data = self._process_request()
+        data = self.process_request()
         self.assertEqual("summary:test", data["query"])
         self.assertEqual(5, len(data["results"].items))
 
@@ -97,12 +90,14 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
         ticket_time = ticket.time_changed
         #act
         self.req.args[RequestParameters.QUERY] = "*:*"
-        data = self._process_request()
+        data = self.process_request()
         result_items = data["results"].items
         #assert
         self.assertEqual(1, len(result_items))
         result_datetime = result_items[0]["date"]
-        print "Ticket time: %s, Returned time: %s" % (ticket_time, result_datetime)
+        print "Ticket time: %s, Returned time: %s" % (
+            ticket_time,
+            result_datetime)
         self.assertEqual(format_datetime(ticket_time), result_items[0]["date"])
 
     def test_can_return_user_time(self):
@@ -113,7 +108,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
         #act
         self.req.tz = FixedOffset(60, 'GMT +1:00')
         self.req.args[RequestParameters.QUERY] = "*:*"
-        data = self._process_request()
+        data = self.process_request()
         result_items = data["results"].items
         #asset
         self.assertEqual(1, len(result_items))
@@ -126,7 +121,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
     def test_ticket_href(self):
         self._insert_tickets(1)
         self.req.args[RequestParameters.QUERY] = "*:*"
-        data = self._process_request()
+        data = self.process_request()
         docs = data["results"].items
         self.assertEqual(1, len(docs))
         self.assertEqual("/main/ticket/1", docs[0]["href"])
@@ -134,31 +129,12 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
     def test_page_href(self):
         self._insert_tickets(DEFAULT_DOCS_PER_PAGE+1)
         self.req.args[RequestParameters.QUERY] = "*:*"
-        data = self._process_request()
+        data = self.process_request()
         shown_pages =  data["results"].shown_pages
         second_page_href = shown_pages[1]["href"]
         self.assertIn("page=2", second_page_href)
         self.assertIn("q=*%3A*", second_page_href)
 
-    def test_facets_ticket_only(self):
-        self.insert_ticket("summary1 keyword", status="closed")
-        self.insert_ticket("summary2 keyword", status="new")
-        self.req.args[RequestParameters.QUERY] = "*:*"
-        data = self._process_request()
-        facets =  data["facets"]
-        pprint(facets)
-        self.assertEqual({'ticket': 2}, facets["type"])
-
-    def test_facets_ticket_and_wiki(self):
-        self.insert_ticket("summary1 keyword", status="closed")
-        self.insert_ticket("summary2 keyword", status="new")
-        self.insert_wiki("dummyTitle", "Some text")
-        self.req.args[RequestParameters.QUERY] = "*"
-        data = self._process_request()
-        facets =  data["facets"]
-        pprint(facets)
-        self.assertEqual({'ticket': 2, 'wiki': 1}, facets["type"])
-
     def test_can_apply_type_parameter(self):
         #arrange
         self.insert_ticket("summary1 keyword", status="closed")
@@ -167,8 +143,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
         self.req.args[RequestParameters.QUERY] = "*"
         self.req.args[RequestParameters.TYPE] = "ticket"
         #act
-        data = self._process_request()
-        docs = data["results"].items
+        data = self.process_request()
         #assert
         active_type = data["active_type"]
         self.assertEquals("ticket", active_type)
@@ -191,7 +166,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
         self.req.args[RequestParameters.TYPE] = "ticket"
         self.req.args[RequestParameters.PAGELEN] = "4"
         self.req.args[RequestParameters.PAGE] = "2"
-        data = self._process_request()
+        data = self.process_request()
         results = data["results"]
         docs = results.items
         self.assertEquals(4, len(docs))
@@ -211,7 +186,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
 
     def test_type_grouping(self):
         self.req.args[RequestParameters.QUERY] = "*:*"
-        data = self._process_request()
+        data = self.process_request()
         resource_types =  data["types"]
 
         all = resource_types[0]
@@ -232,16 +207,249 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
         self.req.args[RequestParameters.QUERY] = "*"
         self.req.args[RequestParameters.PAGELEN] = "4"
         self.req.args[RequestParameters.PAGE] = "2"
-        data = self._process_request()
+        data = self.process_request()
         #assert
         resource_types =  data["types"]
 
         all = resource_types[0]
-        self.assertIn("page=2", all["href"])
+        self.assertNotIn("page=2", all["href"])
 
         ticket = resource_types[1]
         self.assertNotIn("page=", ticket["href"])
 
+    def test_that_there_are_filters_in_type_links(self):
+        #arrange
+#        self._insert_tickets(2)
+        #act
+        self.req.args[RequestParameters.QUERY] = "*"
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.FILTER_QUERY] = "status:new"
+        data = self.process_request()
+        #assert
+        for type in data["types"]:
+            self.assertNotIn("fq=", type["href"])
+
+    def test_that_type_facet_is_in_default_search(self):
+        #arrange
+        self._insert_tickets(2)
+        #act
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        self.assertEquals(1, len(data["facet_counts"]))
+
+    def test_can_return_facets_counts_for_tickets(self):
+        #arrange
+        self.insert_ticket("T1", status="new", milestone="m1")
+        self.insert_ticket("T2", status="closed")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        facet_counts =  data["facet_counts"]
+        status_counts = facet_counts["status"]
+        self.assertEquals(1, status_counts["new"]["count"])
+        self.assertEquals(1, status_counts["closed"]["count"])
+
+    def test_can_create_href_for_facet_counts(self):
+        #arrange
+        self.insert_ticket("T1", status="new")
+        self.insert_ticket("T2", status="closed")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        facet_counts =  data["facet_counts"]
+        status_counts = facet_counts["status"]
+        self.assertEquals(1, status_counts["new"]["count"])
+        self.assertIn("fq=status%3A%22new%22", status_counts["new"]["href"])
+
+    def test_can_handle_none_in_facet_counts(self):
+        #arrange
+        self.insert_ticket("T1")
+        self.insert_ticket("T2")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        facet_counts =  data["facet_counts"]
+        status_counts = facet_counts["status"]
+        empty_status_count = status_counts[None]
+        self.assertEquals(2, empty_status_count["count"])
+        self.assertIn(
+            'fq=NOT+(status:*)',
+            unquote(empty_status_count["href"]))
+
+    def test_can_return_empty_facets_result_for_wiki_pages(self):
+        #arrange
+        self.insert_wiki("W1","Some text")
+        #act
+        self.req.args[RequestParameters.TYPE] = "wiki"
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        facet_counts =  data["facet_counts"]
+        self.assertEquals({}, facet_counts)
+
+    def test_can_accept_multiple_filter_query_parameters(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new")
+        self.insert_ticket("T2", component="c1", status="new")
+        self.insert_ticket("T3",)
+        self._insert_wiki_pages(2)
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.QUERY] = "*"
+        self.req.args[RequestParameters.FILTER_QUERY] = [
+            'component:"c1"', 'status:"new"']
+        data = self.process_request()
+        #assert
+        page_href = data["page_href"]
+        self.assertIn(urlencode({'fq':'component:"c1"'}), page_href)
+        self.assertIn(urlencode({'fq':'status:"new"'}), page_href)
+
+        docs = data["results"].items
+        self.assertEqual(2, len(docs))
+
+
+    def test_can_handle_empty_facet_result(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new")
+        self.insert_ticket("T2", component="c1", status="new")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.QUERY] = "*"
+        self.req.args[RequestParameters.FILTER_QUERY] = ['component:"c1"']
+        data = self.process_request()
+        #assert
+        facet_counts = data["facet_counts"]
+
+        milestone_facet_count = facet_counts["milestone"]
+        print unquote(milestone_facet_count[None]["href"])
+
+    def test_can_handle_multiple_same(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new")
+        self.insert_ticket("T2", component="c1", status="new")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.QUERY] = "*"
+        self.req.args[RequestParameters.FILTER_QUERY] = ['component:"c1"']
+        data = self.process_request()
+        #assert
+        facet_counts = data["facet_counts"]
+
+        component_facet_count = facet_counts["component"]
+        c1_href = component_facet_count["c1"]["href"]
+        print unquote(c1_href)
+        self.assertEquals(
+            1,
+            self._count_parameter_in_url(c1_href, "fq", 'component:"c1"'))
+
+    def test_can_return_current_filter_queries(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new")
+        self.insert_ticket("T2", component="c1", status="new")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.QUERY] = "*"
+        self.req.args[RequestParameters.FILTER_QUERY] = [
+            'component:"c1"',
+            'status:"new"']
+        data = self.process_request()
+        #assert
+        current_filter_queries = data["active_filter_queries"]
+        self.assertEquals(2, len(current_filter_queries))
+
+        component_filter =  current_filter_queries[0]
+        self.assertEquals('component:"c1"', component_filter["label"])
+        self.assertNotIn("fq=", component_filter["href"])
+
+        status_filter =  current_filter_queries[1]
+        self.assertEquals('status:"new"', status_filter["label"])
+        self.assertIn('fq=component:"c1"', unquote(status_filter["href"]))
+        self.assertNotIn('fq=status:"new"', unquote(status_filter["href"]))
+
+    def test_can_return_missing_milestone(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new")
+        self.insert_ticket("T2", component="c1", status="new", milestone="A")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.FILTER_QUERY] = ["NOT (milestone:*)"]
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        items = data["results"].items
+        self.assertEquals(1, len(items))
+
+    def test_can_return_no_results_for_missing_milestone(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new", milestone="A")
+        self.insert_ticket("T2", component="c1", status="new", milestone="A")
+        #act
+        self.req.args[RequestParameters.TYPE] = "ticket"
+        self.req.args[RequestParameters.FILTER_QUERY] = ["NOT (milestone:*)"]
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        items = data["results"].items
+        self.assertEquals(0, len(items))
+
+    def test_that_type_facet_has_href_to_type(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new", milestone="A")
+        #act
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        ticket_facet_href = data["facet_counts"]["type"]["ticket"]["href"]
+        ticket_facet_href = unquote(ticket_facet_href)
+        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)
+        i = 0
+        for parameter in parsed_parameters:
+            if parameter == parameter_to_find:
+                i += 1
+
+        return i
 
     def _assertResourceType(self, type, label, active, href_contains = None):
         self.assertEquals(label, type["label"])
@@ -252,6 +460,11 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
     def _insert_tickets(self, n):
         for i in range(1, n+1):
             self.insert_ticket("test %s" % i)
+
+    def _insert_wiki_pages(self, n):
+        for i in range(1, n+1):
+            self.insert_wiki("test %s" % i)
+
 def suite():
     return unittest.makeSuite(WebUiTestCaseWithWhoosh, 'test')
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py Thu Jan 31 14:41:22 2013
@@ -17,7 +17,7 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
-from datetime import datetime, timedelta
+from datetime import datetime
 
 import unittest
 import tempfile
@@ -25,12 +25,14 @@ import shutil
 from bhsearch.api import ASC, DESC, SCORE
 from bhsearch.query_parser import DefaultQueryParser
 from bhsearch.tests.utils import BaseBloodhoundSearchTest
-from bhsearch.whoosh_backend import WhooshBackend
+from bhsearch.whoosh_backend import WhooshBackend, \
+    WhooshEmptyFacetErrorWorkaround
 from trac.test import EnvironmentStub
 from trac.util.datefmt import FixedOffset, utc
 from whoosh import index, sorting, query
 from whoosh.fields import Schema, ID, TEXT, KEYWORD
-from whoosh.qparser import MultifieldPlugin, QueryParser, WhitespacePlugin, PhrasePlugin
+from whoosh.qparser import MultifieldPlugin, QueryParser, WhitespacePlugin, \
+    PhrasePlugin
 
 
 class WhooshBackendTestCase(BaseBloodhoundSearchTest):
@@ -39,7 +41,7 @@ class WhooshBackendTestCase(BaseBloodhou
         self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
         self.whoosh_backend = WhooshBackend(self.env)
         self.whoosh_backend.recreate_index()
-        self.default_parser = DefaultQueryParser(self.env)
+        self.parser = DefaultQueryParser(self.env)
 
     def tearDown(self):
         shutil.rmtree(self.env.path)
@@ -92,7 +94,7 @@ class WhooshBackendTestCase(BaseBloodhou
         result = whoosh_backend2.query(query.Every())
         self.assertEqual(2, result.hits)
 
-    def test_can_multi_sort_asc(self):
+    def test_can_apply_multiple_sort_conditions_asc(self):
         self.whoosh_backend.add_doc(dict(id="2", type="ticket2"))
         self.whoosh_backend.add_doc(dict(id="3", type="ticket1"))
         self.whoosh_backend.add_doc(dict(id="4", type="ticket3"))
@@ -109,7 +111,7 @@ class WhooshBackendTestCase(BaseBloodhou
                           {'type': 'ticket3', 'id': '4'}],
             result.docs)
 
-    def test_can_multi_sort_desc(self):
+    def test_can_apply_multiple_sort_conditions_desc(self):
         self.whoosh_backend.add_doc(dict(id="2", type="ticket2"))
         self.whoosh_backend.add_doc(dict(id="3", type="ticket1"))
         self.whoosh_backend.add_doc(dict(id="4", type="ticket3"))
@@ -159,7 +161,7 @@ class WhooshBackendTestCase(BaseBloodhou
             time=the_third_date,
         ))
 
-        parsed_query = self.default_parser.parse("summary:texttofind")
+        parsed_query = self.parser.parse("summary:texttofind")
 
         result = self.whoosh_backend.query(
             parsed_query,
@@ -215,7 +217,8 @@ class WhooshBackendTestCase(BaseBloodhou
         self.assertEqual(0, result.hits)
 
     def test_can_search_time_with_utc_tzinfo(self):
-        time = datetime(2012, 12, 13, 11, 8, 34, 711957, tzinfo=FixedOffset(0, 'UTC'))
+        time = datetime(2012, 12, 13, 11, 8, 34, 711957,
+            tzinfo=FixedOffset(0, 'UTC'))
         self.whoosh_backend.add_doc(dict(id="1", type="ticket", time=time))
         result = self.whoosh_backend.query(query.Every())
         self.print_result(result)
@@ -245,14 +248,13 @@ class WhooshBackendTestCase(BaseBloodhou
         self.whoosh_backend.add_doc(dict(id="2", type="wiki" ))
         result = self.whoosh_backend.query(
             query.Every(),
-            filter=[("type", "ticket")],
+            filter=query.Term("type", "ticket"),
             facets=["type"]
         )
         self.print_result(result)
         self.assertEqual(1, result.hits)
         self.assertEqual("ticket", result.docs[0]["type"])
 
-
     @unittest.skip("TODO clarify behavior on Whoosh mail list")
     def test_can_search_id_and_summary_TODO(self):
         #arrange
@@ -275,6 +277,65 @@ class WhooshBackendTestCase(BaseBloodhou
         self.print_result(result)
         self.assertEqual(2, result.hits)
 
+    def test_no_index_error_when_counting_facet_on_missing_field(self):
+        """
+        Whoosh 2.4.1 raises "IndexError: list index out of range"
+        when search contains facets on field that is missing in at least one
+        document in the index. The error manifests only when index contains
+        more than one segment
+
+        Introduced workaround should solve this problem.
+        """
+        #add more tickets to make sure we have more than one segment in index
+        count = 20
+        for i in range(count):
+            self.insert_ticket("test %s" % (i))
+
+        result = self.whoosh_backend.query(
+            query.Every(),
+            facets=["milestone"]
+        )
+        self.assertEquals(count, result.hits)
+
+    def test_can_query_missing_field_and_type(self):
+        self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+        self.whoosh_backend.add_doc(dict(id="2", type="ticket", milestone="A"))
+        self.whoosh_backend.add_doc(dict(id="3", type="wiki"))
+        filter = self.parser.parse_filters(["NOT (milestone:*)", "type:ticket"])
+        result = self.whoosh_backend.query(
+            query.Every(),
+            filter=filter,
+        )
+        self.print_result(result)
+        self.assertEqual(1, result.hits)
+        self.assertEqual("1", result.docs[0]["id"])
+
+
+    def test_can_query_missing_field(self):
+        self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+        self.whoosh_backend.add_doc(dict(id="2", type="ticket", milestone="A"))
+        filter = self.parser.parse_filters(["NOT (milestone:*)"])
+        result = self.whoosh_backend.query(
+            query.Every(),
+            filter=filter,
+        )
+        self.print_result(result)
+        self.assertEqual(1, result.hits)
+        self.assertEqual("1", result.docs[0]["id"])
+
+
+    @unittest.skip("TODO clarify behavior on Whoosh mail list")
+    def test_can_query_missing_field_and_type_with_no_results(self):
+        self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+        self.whoosh_backend.add_doc(dict(id="3", type="wiki"))
+        filter = self.parser.parse_filters(["NOT (milestone:*)", "type:ticket"])
+        result = self.whoosh_backend.query(
+            query.Every(),
+            filter=filter,
+        )
+        self.print_result(result)
+        self.assertEqual(0, result.hits)
+
 class WhooshFunctionalityTestCase(unittest.TestCase):
     def setUp(self):
         self.index_dir = tempfile.mkdtemp('whoosh_index')
@@ -293,10 +354,10 @@ class WhooshFunctionalityTestCase(unitte
 
         ix = index.create_in(self.index_dir, schema=schema)
         with ix.writer() as w:
-            w.add_document(unique_id="1",type="type1")
-            w.add_document(unique_id="2",type="type2", status="New")
+            w.add_document(unique_id=u"1", type=u"type1")
+            w.add_document(unique_id=u"2", type=u"type2", status=u"New")
 
-        facet_fields = ("type", "status" )
+        facet_fields = (u"type", u"status" )
         groupedby = facet_fields
         with ix.searcher() as s:
             r = s.search(
@@ -311,7 +372,7 @@ class WhooshFunctionalityTestCase(unitte
             {'status': {None: 1, 'New': 1}, 'type': {'type1': 1, 'type2': 1}},
             facets)
 
-    def test_groupedby_empty_field(self):
+    def test_can_use_query_and_groupedby_empty_field(self):
         """
         Whoosh 2.4 raises an error when simultaneously using filters and facets
         in search:
@@ -331,8 +392,8 @@ class WhooshFunctionalityTestCase(unitte
 
         ix = index.create_in(self.index_dir, schema=schema)
         with ix.writer() as w:
-            w.add_document(unique_id=u"1",type=u"type1")
-            w.add_document(unique_id=u"2",type=u"type2")
+            w.add_document(unique_id=u"1", type=u"type1")
+            w.add_document(unique_id=u"2", type=u"type2")
 
         with ix.searcher() as s:
             with self.assertRaises(AttributeError):
@@ -343,13 +404,42 @@ class WhooshFunctionalityTestCase(unitte
                     filter=query.Term("type", "type1")
                 )
 
-#    def _prepare_groupedby(self, facets):
-#        if not facets:
-#            return None
-#        groupedby = sorting.Facets()
-#        for facet_name in facets:
-#            groupedby.add_field(facet_name, allow_overlap=True, maptype=sorting.Count)
-#        return groupedby
+    def test_out_of_range_on_empty_facets(self):
+        """
+        Whoosh raises exception IndexError: list index out of range
+        when search contains facets on field that is missing in at least one
+        document in the index. The error manifests only when index contains
+        more than one segment
+
+        The problem expected to be fixed in the next release.
+
+        For the time of being, whoosh-backend have to introduce workaround in
+        order to fix the problem. This unit-test is just a reminder to remove
+        workaround when the fixed version of Whoosh is applied.
+        """
+        schema = Schema(
+                unique_id=ID(stored=True, unique=True),
+                status=ID(stored=True),
+                )
+
+#        ix = RamStorage().create_index(schema)
+        ix = index.create_in(self.index_dir, schema=schema)
+        def insert_docs():
+            with ix.writer() as w:
+                for i in range(10):
+                    w.add_document(unique_id=unicode(i))
+
+        #the problem occurs only when index contains more than one segment
+        insert_docs()
+        insert_docs()
+
+        with ix.searcher() as s:
+            with self.assertRaises(IndexError):
+                s.search(
+                    query.Every(),
+                    groupedby=(u"status"),
+                    maptype=sorting.Count,
+                )
 
     def _load_facets(self, non_paged_results):
         facet_names = non_paged_results.facet_names()
@@ -361,11 +451,58 @@ class WhooshFunctionalityTestCase(unitte
         return facets_result
 
 
+class WhooshEmptyFacetErrorWorkaroundTestCase(BaseBloodhoundSearchTest):
+    def setUp(self):
+        self.env = EnvironmentStub(enable=['bhsearch.*'])
+        self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+        self.whoosh_backend = WhooshBackend(self.env)
+        self.whoosh_backend.recreate_index()
+        self.parser = DefaultQueryParser(self.env)
+        self.empty_facet_workaround = WhooshEmptyFacetErrorWorkaround(self.env)
+
+    def tearDown(self):
+        shutil.rmtree(self.env.path)
+        self.env.reset_db()
+
+    def test_set_should_not_be_empty_fields(self):
+        self.insert_ticket("test x")
+        result = self.whoosh_backend.query(query.Every())
+        self.print_result(result)
+        doc = result.docs[0]
+        null_marker = WhooshEmptyFacetErrorWorkaround.NULL_MARKER
+        self.assertEqual(null_marker, doc["component"])
+        self.assertEqual(null_marker, doc["status"])
+        self.assertEqual(null_marker, doc["milestone"])
+
+    def test_can_fix_query_filter(self):
+        parsed_filter = self.parser.parse_filters(
+            ["type:ticket", "NOT (milestone:*)"])
+        query_parameters = dict(filter=parsed_filter)
+        self.empty_facet_workaround.query_pre_process(
+            query_parameters)
+
+        result_filter = query_parameters["filter"]
+        print 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(
+            ["type:ticket", "milestone:aaa"])
+        query_parameters = dict(filter=parsed_filter)
+        self.empty_facet_workaround.query_pre_process(
+            query_parameters)
+
+        result_filter = query_parameters["filter"]
+        print result_filter
+        self.assertEquals('(type:ticket AND milestone:aaa)', str(result_filter))
 
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(WhooshBackendTestCase, 'test'))
     suite.addTest(unittest.makeSuite(WhooshFunctionalityTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(WhooshEmptyFacetErrorWorkaroundTestCase,
+        'test'))
     return suite
 
 if __name__ == '__main__':

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/wiki_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/wiki_search.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/wiki_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/wiki_search.py Thu Jan 31 14:41:22 2013
@@ -18,14 +18,14 @@
 #  specific language governing permissions and limitations
 #  under the License.
 import shutil
-
 import unittest
 import tempfile
+
 from bhsearch.api import BloodhoundSearchApi
 from bhsearch.query_parser import DefaultQueryParser
 from bhsearch.tests.utils import BaseBloodhoundSearchTest
 from bhsearch.whoosh_backend import WhooshBackend
-from bhsearch.wiki_search import WikiIndexer, WikiSearchParticipant
+from bhsearch.search_resources.wiki_search import WikiIndexer, WikiSearchParticipant
 
 from trac.test import EnvironmentStub
 from trac.wiki import WikiSystem, WikiPage
@@ -70,7 +70,7 @@ class WikiIndexerEventsTestCase(BaseBloo
         shutil.rmtree(self.env.path)
         self.env.reset_db()
 
-    def test_can_index_on_new_page(self):
+    def test_can_add_new_wiki_page_to_index(self):
         #arrange
         self.insert_wiki(self.DUMMY_PAGE_NAME, "dummy text")
         #act
@@ -83,7 +83,7 @@ class WikiIndexerEventsTestCase(BaseBloo
         self.assertEqual("dummy text", doc["content"])
         self.assertEqual("wiki", doc["type"])
 
-    def test_can_delete_page(self):
+    def test_can_delete_wiki_page_from_index(self):
         #arrange
         self.insert_wiki(self.DUMMY_PAGE_NAME)
         WikiPage(self.env, self.DUMMY_PAGE_NAME).delete()
@@ -119,17 +119,6 @@ class WikiIndexerEventsTestCase(BaseBloo
         self.assertEqual(1, results.hits)
         self.assertEqual("NewPageName", results.docs[0]["id"])
 
-    def test_can_index_deleted_event(self):
-        #arrange
-        self.insert_wiki(self.DUMMY_PAGE_NAME)
-        page = WikiPage(self.env, self.DUMMY_PAGE_NAME)
-        page.delete()
-        #act
-        results = self.search_api.query("*:*")
-        #assert
-        self.print_result(results)
-        self.assertEqual(0, results.hits)
-
     def test_can_index_version_deleted_event(self):
         #arrange
         self.insert_wiki(self.DUMMY_PAGE_NAME, "version1")
@@ -144,11 +133,10 @@ class WikiIndexerEventsTestCase(BaseBloo
         self.assertEqual(1, results.hits)
         self.assertEqual("version1", results.docs[0]["content"])
 
-
-
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(WikiIndexerSilenceOnExceptionTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(
+        WikiIndexerSilenceOnExceptionTestCase, 'test'))
     suite.addTest(unittest.makeSuite(WikiIndexerEventsTestCase, 'test'))
     return suite
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py?rev=1440987&r1=1440986&r2=1440987&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/web_ui.py Thu Jan 31 14:41:22 2013
@@ -19,23 +19,26 @@
 #  under the License.
 
 r"""Bloodhound Search user interface."""
+import copy
 
 import pkg_resources
 import re
 
-from trac.core import *
+from trac.core import Component, implements, TracError
 from genshi.builder import tag
 from trac.perm import IPermissionRequestor
 from trac.search import shorten_result
-from trac.config import OrderedExtensionsOption
+from trac.config import OrderedExtensionsOption, ListOption
 from trac.util.presentation import Paginator
 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
@@ -50,48 +53,72 @@ class RequestParameters(object):
     """
     QUERY = "q"
     PAGE = "page"
-    FILTER = "fl"
     TYPE = "type"
     NO_QUICK_JUMP = "noquickjump"
     PAGELEN = "pagelen"
+    FILTER_QUERY = "fq"
 
     def __init__(self, req):
         self.req = req
 
-        self.query = req.args.get(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'))
 
-        #TODO: add filters support
-        self.filters = []
+        self.filter_queries = req.args.getlist(self.FILTER_QUERY)
+        self.filter_queries = self._remove_possible_duplications(
+                        self.filter_queries)
 
         #TODO: retrieve sort from query string
         self.sort = DEFAULT_SORT
 
-        self.pagelen = int(req.args.get(
+        self.pagelen = int(req.args.getfirst(
             RequestParameters.PAGELEN,
             DEFAULT_RESULTS_PER_PAGE))
-        self.page = int(req.args.get(RequestParameters.PAGE, '1'))
-        self.type = req.args.get(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:
-            self.params[self.PAGELEN]=self.pagelen
+            self.params[self.PAGELEN] = self.pagelen
         if self.page > 1:
-            self.params[self.PAGE]=self.page
+            self.params[self.PAGE] = self.page
         if self.type:
             self.params[self.TYPE] = self.type
+        if self.filter_queries:
+            self.params[RequestParameters.FILTER_QUERY] = self.filter_queries
+
+    def _remove_possible_duplications(self, parameters_list):
+        seen = set()
+        return [parameter for parameter in parameters_list
+                if parameter not in seen and not seen.add(parameter)]
+
+    def create_href(
+            self,
+            page = None,
+            type=None,
+            skip_type = False,
+            skip_page = False,
+            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
 
-    def create_href(self, page = None, type=None, skip_type = False,
-                    skip_page = False):
-        params = dict(self.params)
         if page:
             params[self.PAGE] = page
 
@@ -102,11 +129,19 @@ class RequestParameters(object):
             params[self.TYPE] = type
 
         if skip_type and self.TYPE in params:
-            #show all does not require type parameter
             del(params[self.TYPE])
 
+        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
+
         return self.req.href.bhsearch(**params)
 
+    def is_show_all_mode(self):
+        return self.type is None
+
 class BloodhoundSearchModule(Component):
     """Main search page"""
 
@@ -123,6 +158,10 @@ class BloodhoundSearchModule(Component):
     )
 
 
+    default_facets_all = ListOption('bhsearch', 'default_facets_all',
+        doc="""Default facets applied to search through all resources""")
+
+
     # INavigationContributor methods
     def get_active_navigation_item(self, req):
         return 'bhsearch'
@@ -144,33 +183,44 @@ class BloodhoundSearchModule(Component):
     def process_request(self, req):
         req.perm.assert_permission(SEARCH_PERMISSION)
         parameters = RequestParameters(req)
-
-        #TODO add quick jump support
-
         allowed_participants = self._get_allowed_participants(req)
         data = {
             'query': parameters.query,
             }
+        self._prepare_allowed_types(allowed_participants, parameters, data)
+        self._prepare_active_filter_queries(
+            parameters,
+            data,
+        )
+        working_query_string = parameters.query.strip()
 
-        #todo: filters check, tickets etc
-        if not any((parameters.query, )):
-            return self._return_data(req, data)
+
+        #TBD: should search return results on empty query?
+#        if not any((
+#            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.type,
-            parameters.filters,
+            parameters,
             allowed_participants)
 
-        #todo: add proper facets functionality
-        facets = self._prepare_facets(req)
+        facets = self._prepare_facets(parameters, allowed_participants)
 
-        querySystem = BloodhoundSearchApi(self.env)
-        query_result = querySystem.query(
-            parameters.query,
-            pagenum = parameters.page,
-            pagelen = parameters.pagelen,
-            sort = parameters.sort,
-            facets = facets,
+        query_system = BloodhoundSearchApi(self.env)
+        query_result = query_system.query(
+            working_query_string,
+            pagenum=parameters.page,
+            pagelen=parameters.pagelen,
+            sort=parameters.sort,
+            facets=facets,
             filter=query_filter)
 
         ui_docs = [self._process_doc(doc, req, allowed_participants)
@@ -199,66 +249,188 @@ class BloodhoundSearchModule(Component):
             prev_href = parameters.create_href(page = parameters.page - 1)
             add_link(req, 'prev', prev_href, _('Previous Page'))
 
+
         data['results'] = results
-        self._prepare_type_grouping(
-            allowed_participants,
+
+        self._prepare_result_facet_counts(
             parameters,
-            data)
+            query_result,
+            data,
+        )
 
-        #TODO:add proper facet links
-        data['facets'] = query_result.facets
         data['page_href'] = parameters.create_href()
         return self._return_data(req, data)
 
-    def _prepare_query_filter(self, type, filters, allowed_participants):
-        query_filters = []
-
-        if type in allowed_participants:
-            query_filters.append((IndexFields.TYPE, type))
+    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:
-            self.log.debug("Unsupported type in web request: %s", type)
+            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)
+
 
-        #TODO: handle other filters
-        return query_filters
 
-    def _prepare_type_grouping(self, allowed_participants, parameters, data):
+    def _prepare_allowed_types(self, allowed_participants, parameters, data):
         active_type = parameters.type
         if active_type and active_type not in allowed_participants:
             raise TracError(_("Unsupported resource type: '%(name)s'",
                 name=active_type))
-        all_is_active = (active_type is None)
-        grouping = [
+        allowed_types = [
             dict(
                 label=_("All"),
-                active=all_is_active,
+                active=(active_type is None),
                 href=parameters.create_href(
                     skip_type=True,
-                    skip_page=not all_is_active)
+                    skip_page=True,
+                    force_filters=[],
+                ),
             )
         ]
 
-        #we want to obtain the same order as specified in search_participants
-        # option
+        #we want obtain the same order as in search participants options
         participant_with_type = dict((participant, type)
             for type, participant in allowed_participants.iteritems())
         for participant in self.search_participants:
             if participant in participant_with_type:
                 type = participant_with_type[participant]
-                is_active = (type == active_type)
-                grouping.append(dict(
+                allowed_types.append(dict(
                     label=_(participant.get_title()),
-                    active=is_active,
+                    active=(type ==active_type),
                     href=parameters.create_href(
                         type=type,
-                        skip_page=not is_active
-                    )
+                        skip_page=True,
+                        force_filters=[],
+                    ),
                 ))
-        data["types"] =  grouping
-        data["active_type"] = active_type
+        data["types"] =  allowed_types
+        data["active_type"] =  active_type
+
+
+
+    def _prepare_active_filter_queries(
+            self,
+            parameters,
+            data):
+        active_filter_queries = []
+        for filter_query in parameters.filter_queries:
+            active_filter_queries.append(dict(
+                href=parameters.create_href(
+                    force_filters=self._cut_filters(
+                        parameters.filter_queries,
+                        filter_query)),
+                label=filter_query,
+                query=filter_query,
+            ))
+        data['active_filter_queries'] = active_filter_queries
+
+    def _cut_filters(self, filter_queries, filer_to_cut_from):
+        return filter_queries[:filter_queries.index(filer_to_cut_from)]
+
+
+    def _prepare_result_facet_counts(self, parameters, query_result, data):
+        """
+
+        Sample query_result.facets content returned by query
+        {
+           'component': {None:2},
+           'milestone': {None:1, 'm1':1},
+        }
+
+        returned facet_count contains href parameters:
+        {
+           'component': {None: {'count':2, href:'...'},
+           'milestone': {
+                            None: {'count':1,, href:'...'},
+                            'm1':{'count':1, href:'...'}
+                        },
+        }
 
-    def _prepare_facets(self, req):
-        facets = [IndexFields.TYPE]
-        #TODO: add type specific default facets
+        """
+        result_facets = query_result.facets
+        facet_counts = dict()
+        if result_facets:
+            for field, facets_dict in result_facets.iteritems():
+                per_field_dict = dict()
+                for field_value, count in facets_dict.iteritems():
+                    if field == IndexFields.TYPE:
+                        href = parameters.create_href(
+                            skip_page=True,
+                            force_filters=[],
+                            type=field_value)
+                    else:
+                        href = parameters.create_href(
+                            skip_page=True,
+                            additional_filter=self._create_term_expression(
+                                    field,
+                                    field_value)
+                        )
+                    per_field_dict[field_value] = dict(
+                        count=count,
+                        href=href
+                    )
+                facet_counts[_(field)] = per_field_dict
+
+        data['facet_counts'] = facet_counts
+
+    def _create_term_expression(self, field, field_value):
+        if field_value is None:
+            query = "NOT (%s:*)" % field
+        elif isinstance(field_value, basestring):
+            query = '%s:"%s"' % (field, field_value)
+        else:
+            query = '%s:%s' % (field, field_value)
+        return query
+
+    def _prepare_query_filter(self, parameters, allowed_participants):
+        query_filters = list(parameters.filter_queries)
+        type = parameters.type
+        if type in allowed_participants:
+            query_filters.append(
+                self._create_term_expression(IndexFields.TYPE, type))
+        else:
+            self.log.debug("Unsupported type in web request: %s", type)
+        return query_filters
+
+    def _prepare_facets(self, parameters, allowed_participants):
+        #TODO: add possibility of specifying facets in query parameters
+        if parameters.is_show_all_mode():
+            facets = [IndexFields.TYPE]
+            facets.extend(self.default_facets_all)
+        else:
+            type_participant = allowed_participants[parameters.type]
+            facets = type_participant.get_default_facets()
         return facets
 
     def _get_allowed_participants(self, req):



Mime
View raw message