You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by ju...@apache.org on 2013/02/11 14:05:19 UTC

svn commit: r1444754 [1/2] - in /incubator/bloodhound/branches/bep_0003_multiproduct: ./ bloodhound_dashboard/ bloodhound_multiproduct/ bloodhound_search/ bloodhound_search/bhsearch/ bloodhound_search/bhsearch/search_resources/ bloodhound_search/bhsear...

Author: jure
Date: Mon Feb 11 13:05:18 2013
New Revision: 1444754

URL: http://svn.apache.org/r1444754
Log:
sync merge from trunk


Added:
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/
      - copied from r1444753, incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/search_resources/
Removed:
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/milestone_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/ticket_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/wiki_search.py
Modified:
    incubator/bloodhound/branches/bep_0003_multiproduct/   (props changed)
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/setup.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/__init__.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/api.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.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/base.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/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/web_ui.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_repositories.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_search.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_box.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bloodhound_theme.html
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/setup.py

Propchange: incubator/bloodhound/branches/bep_0003_multiproduct/
------------------------------------------------------------------------------
  Merged /incubator/bloodhound/trunk:r1442601-1444753

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/setup.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/setup.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/setup.py Mon Feb 11 13:05:18 2013
@@ -23,7 +23,7 @@ try:
 except ImportError:
     from distutils.core import setup
 
-DESC = """Project dashboard for Apache(TM) Bloodhound
+DESC = """Project dashboard for Apache(TM) Bloodhound.
 
 Add custom dashboards in multiple pages of Bloodhound sites.
 """
@@ -131,6 +131,9 @@ setup(
     name=DIST_NM,
     version=latest,
     description=DESC.split('\n', 1)[0],
+    author = "Apache Bloodhound",
+    license = "Apache License v2",
+    url = "http://incubator.apache.org/bloodhound/",
     requires = ['trac'],
     tests_require = ['dutest>=0.2.4', 'TracXMLRPC'],
     install_requires = [

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py Mon Feb 11 13:05:18 2013
@@ -22,6 +22,10 @@ from setuptools import setup
 setup(
     name = 'BloodhoundMultiProduct',
     version = '0.4.0',
+    description = "Multiproduct support for Apache(TM) Bloodhound.",
+    author = "Apache Bloodhound",
+    license = "Apache License v2",
+    url = "http://incubator.apache.org/bloodhound/",
     packages = ['multiproduct', 'multiproduct.ticket', 'tests',],
     package_data = {'multiproduct' : ['templates/*.html',]},
     entry_points = {'trac.plugins': [

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/__init__.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/__init__.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/__init__.py Mon Feb 11 13:05:18 2013
@@ -29,6 +29,7 @@ from trac.core import TracError
 TracError.__str__ = lambda self: unicode(self).encode('ascii', 'ignore')
 
 try:
+    # pylint: disable=wildcard-import
     from bhsearch import *
     msg = 'Ok'
 except Exception, exc:

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=1444754&r1=1444753&r2=1444754&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 Mon Feb 11 13:05:18 2013
@@ -124,10 +124,13 @@ class ISearchParticipant(Interface):
     def format_search_results(contents):
         """Called to see if the module wants to format the search results."""
 
-    def get_search_filters(req):
+    def is_allowed(req):
         """Called when we want to build the list of components with search.
         Passes the request object to do permission checking."""
 
+    def get_type():
+        """Return type of search participant"""
+
     def get_title():
         """Return resource title."""
 
@@ -245,6 +248,7 @@ class BloodhoundSearchApi(Component):
         for post_processor in self.result_post_processors:
             post_processor.post_process(query_result)
 
+        query_result.debug["api_parameters"] = query_parameters
         return query_result
 
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py Mon Feb 11 13:05:18 2013
@@ -34,6 +34,8 @@ class BaseSearchParticipant(Component):
     default_view = None
     default_grid_fields = None
     default_facets = None
+    participant_type = None
+    required_permission = None
 
     def get_default_facets(self):
         return self.default_facets
@@ -45,3 +47,9 @@ class BaseSearchParticipant(Component):
         if view == "grid":
             return self.default_grid_fields
         return None
+
+    def is_allowed(self, req=None):
+        return (not req or self.required_permission in req.perm)
+
+    def get_participant_type(self):
+        return self.participant_type
\ No newline at end of file

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py Mon Feb 11 13:05:18 2013
@@ -117,6 +117,9 @@ class MilestoneIndexer(BaseIndexer):
 class MilestoneSearchParticipant(BaseSearchParticipant):
     implements(ISearchParticipant)
 
+    participant_type = MILESTONE_TYPE
+    required_permission = 'MILESTONE_VIEW'
+
     default_facets = []
     default_grid_fields = [
         MilestoneFields.ID, MilestoneFields.DUE, MilestoneFields.COMPLETED]
@@ -141,10 +144,6 @@ class MilestoneSearchParticipant(BaseSea
         doc="""Default fields for grid view for specific resource""")
 
     #ISearchParticipant members
-    def get_search_filters(self, req=None):
-        if not req or 'MILESTONE_VIEW' in req.perm:
-            return MILESTONE_TYPE
-
     def get_title(self):
         return "Milestone"
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py Mon Feb 11 13:05:18 2013
@@ -62,6 +62,7 @@ class TicketIndexer(BaseIndexer):
 
     def ticket_changed(self, ticket, comment, author, old_values):
         """Reindex a recently modified ticket."""
+        # pylint: disable=unused-argument
         self._index_ticket(ticket)
 
     def ticket_deleted(self, ticket):
@@ -123,6 +124,9 @@ class TicketIndexer(BaseIndexer):
 class TicketSearchParticipant(BaseSearchParticipant):
     implements(ISearchParticipant)
 
+    participant_type = TICKET_TYPE
+    required_permission = 'TICKET_VIEW'
+
     default_facets = [
         TicketFields.STATUS,
         TicketFields.MILESTONE,
@@ -156,10 +160,6 @@ class TicketSearchParticipant(BaseSearch
         doc="""Default fields for grid view for specific resource""")
 
     #ISearchParticipant members
-    def get_search_filters(self, req=None):
-        if not req or 'TICKET_VIEW' in req.perm:
-            return TICKET_TYPE
-
     def get_title(self):
         return "Ticket"
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py Mon Feb 11 13:05:18 2013
@@ -37,8 +37,10 @@ class WikiIndexer(BaseIndexer):
         """Index a recently created ticket."""
         self._index_wiki(page)
 
+
     def wiki_page_changed(self, page, version, t, comment, author, ipnr):
         """Reindex a recently modified ticket."""
+        # pylint: disable=too-many-arguments, unused-argument
         self._index_wiki(page)
 
     def wiki_page_deleted(self, page):
@@ -110,11 +112,15 @@ class WikiIndexer(BaseIndexer):
 class WikiSearchParticipant(BaseSearchParticipant):
     implements(ISearchParticipant)
 
+    participant_type = WIKI_TYPE
+    required_permission = 'WIKI_VIEW'
+
     default_facets = []
     default_grid_fields = [
         IndexFields.ID,
         IndexFields.TIME,
-        IndexFields.AUTHOR
+        IndexFields.AUTHOR,
+        IndexFields.CONTENT,
     ]
     prefix = WIKI_TYPE
 
@@ -137,10 +143,6 @@ class WikiSearchParticipant(BaseSearchPa
         doc="""Default fields for grid view for specific resource""")
 
     #ISearchParticipant members
-    def get_search_filters(self, req=None):
-        if not req or 'WIKI_VIEW' in req.perm:
-            return WIKI_TYPE
-
     def get_title(self):
         return "Wiki"
 

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=1444754&r1=1444753&r2=1444754&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 Mon Feb 11 13:05:18 2013
@@ -55,152 +55,159 @@
   </head>
   <body>
     <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}" />
+            <py:for each="active_filter in active_filter_queries">
+              <input type="hidden" name="fq" value="${active_filter.query}" />
+            </py:for>
 
-      <h1>Advanced search</h1>
-      <form id="fullsearch" action="${href.bhsearch()}" method="get">
-        <p>
-          <input type="text" id="q" name="q" size="60" value="${query}" />
-          <!--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 py:if="active_view" type="hidden" name="view" value="${active_view}" />
-          <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>
-        <!--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>
-
-        </ul>
+            <div class="input-append">
+              <input type="text" id="q" name="q" class="span4" value="${query}" />
+              <button type="submit" class="btn btn-warning">
+                <span class="hidden-phone">${_('Search')}</span>
+                <i class="icon-search icon-white"></i>
+              </button>
+            </div>
+          </form>
+        </div>
       </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 class="span12">
+        <py:if test="quickjump">
+          <dt id="quickjump">
+            <a href="${quickjump.href}" i18n:msg="name">Quickjump to ${quickjump.name}</a>
+          </dt>
+          <dd>${quickjump.description}</dd>
+        </py:if>
+
+        <!--This just a prototype implementation. Should be replaced by proper UI mocks-->
+        <div>
+          <!--Render type tabs: All, Ticket, Wiki, etc.-->
+          <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>
+
+          </ul>
         </div>
-      </py:if>
 
-      <py:if test="results">
-        <div class="span8">
-          <h2>
-            Results <small>(${results.displayed_items()})</small>
-          </h2>
-
-          <div class="pull-right" py:if="all_views">
-            <!--TODO: change presentation. Current implementation is very basic. -->
-            View as:
-            <py:for each="idx, view in enumerate(all_views)">
-              <b py:if="view.is_active">${view.label}</b>
-              <a href="${view.href}" py:if="not view.is_active">${view.label}</a>
-              <py:if test="idx+1!=len(all_views)"> | </py:if>
+        <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>
 
-          <div>
-            <py:if test="view=='grid'">
-              <!--todo: implement rendering in pluggable manner-->
-              <!--Rendering results in grid view-->
-              <table class="listing tickets table table-bordered table-condensed query" style="border-radius: 0px 0px 4px 4px">
-                <!--render table header-->
-                <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 ''}">
-                        <?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>
-                      </th>
-                  </tr>
-                </thead>
-
-                <tbody>
-                  <!--render table rows-->
-                  <py:for each="idx, result in enumerate(results)">
-                    <tr class="${'odd' if idx % 2 else 'even'} prio${result.priority_value}${
-                      ' added' if 'added' in result else ''}${
-                      ' changed' if 'changed' in result else ''}${
-                      ' removed' if 'removed' in result else ''}">
-                      <py:for each="idx, header in enumerate(headers)" py:choose="">
-                        <py:with vars="name = header.name; value = result[name];  title = _('View ')+ result['type']">
-                          <td py:when="name == 'id'" class="id"><a href="$result.href" title="${title}"
-                              class="${classes(closed=result.status == 'closed')}">#$result.id</a></td>
-                          <td py:otherwise="" class="$name" py:choose="">
-                            <a py:when="name == 'summary'" href="$result.href" title="title">$value</a>
-                            <py:when test="isinstance(value, datetime)">${pretty_dateinfo(value, dateonly=True)}</py:when>
-                            <py:when test="name == 'reporter'">${authorinfo(value)}</py:when>
-                            <py:when test="name == 'cc'">${format_emails(ticket_context, value)}</py:when>
-                            <py:when test="name == 'owner' and value">${authorinfo(value)}</py:when>
-                            <py:when test="name == 'milestone'"><a py:if="value" title="View milestone" href="${href.milestone(value)}">${value}</a></py:when>
-                            <!--<py:when test="name == 'content'">${wiki_to_oneliner(ticket_context, value)}</py:when>-->
-                            <py:when test="header.wikify">${wiki_to_oneliner(ticket_context, value)}</py:when>
-                            <py:otherwise>$value</py:otherwise>
-                          </td>
-                        </py:with>
-                      </py:for>
-                    </tr>
-                  </py:for>
-                </tbody>
-              </table>
-            </py:if>
+        <div py:if="results" class="row">
+          <div class="span3 facets">
+            <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>
 
-            <!--<py:if test="not headers">-->
-            <py:if test="not view">
-              <!--Rendering results in free form-->
-              <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>
+              </ul>
             </py:if>
           </div>
 
-          <xi:include py:with="paginator = results" href="bh_page_index.html" />
-        </div>
+          <div class="${'span12' if not facet_counts else 'span9 search_results'}">
+            <h2>
+              Results <small>(${results.displayed_items()})</small>
+            </h2>
+
+            <div class="pull-right" py:if="all_views">
+              <!--TODO: change presentation. Current implementation is very basic. -->
+              View as:
+              <py:for each="idx, view in enumerate(all_views)">
+                <b py:if="view.is_active">${view.label}</b>
+                <a href="${view.href}" py:if="not view.is_active">${view.label}</a>
+                <py:if test="idx+1!=len(all_views)"> | </py:if>
+              </py:for>
+            </div>
+
+            <div>
+              <py:if test="view=='grid'">
+                <!--todo: implement rendering in pluggable manner-->
+                <!--Rendering results in grid view-->
+                <table class="listing tickets table table-bordered table-condensed query" style="border-radius: 0px 0px 4px 4px">
+                  <!--render table header-->
+                  <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 ''}">
+                          <?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>
+                        </th>
+                    </tr>
+                  </thead>
 
-        <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>
+                  <tbody>
+                    <!--render table rows-->
+                    <py:for each="idx, result in enumerate(results)">
+                      <tr class="${'odd' if idx % 2 else 'even'} prio${result.priority_value}${
+                        ' added' if 'added' in result else ''}${
+                        ' changed' if 'changed' in result else ''}${
+                        ' removed' if 'removed' in result else ''}">
+                        <py:for each="idx, header in enumerate(headers)" py:choose="">
+                          <py:with vars="name = header.name; value = result[name];  title = _('View ')+ result['type']">
+                            <td py:when="name == 'id'" class="id"><a href="$result.href" title="${title}"
+                                class="${classes(closed=result.status == 'closed')}">#$result.id</a></td>
+                            <td py:otherwise="" class="$name" py:choose="">
+                              <a py:when="name == 'summary'" href="$result.href" title="title">$value</a>
+                              <py:when test="isinstance(value, datetime)">${pretty_dateinfo(value, dateonly=True)}</py:when>
+                              <py:when test="name == 'reporter'">${authorinfo(value)}</py:when>
+                              <py:when test="name == 'cc'">${format_emails(ticket_context, value)}</py:when>
+                              <py:when test="name == 'owner' and value">${authorinfo(value)}</py:when>
+                              <py:when test="name == 'milestone'"><a py:if="value" title="View milestone" href="${href.milestone(value)}">${value}</a></py:when>
+                              <!--<py:when test="header.wikify">${wiki_to_oneliner(ticket_context, value)}</py:when>-->
+                              <py:otherwise>$value</py:otherwise>
+                            </td>
+                          </py:with>
+                        </py:for>
+                      </tr>
+                    </py:for>
+                  </tbody>
+                </table>
+              </py:if>
+
+              <!--<py:if test="not headers">-->
+              <py:if test="not view">
+                <!--Rendering results in free form-->
+                <dl id="results">
+                  <py:for each="result in results">
+                    <dt><a href="${result.href}" class="searchable">${result.title}</a></dt>
+                    <dd class="searchable">${result.content}</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>
+            </div>
 
-            </ul>
-          </py:if>
+            <xi:include py:with="paginator = results" href="bh_page_index.html" />
+          </div>
         </div>
-
-      </py:if>
+      </div>
 
 
       <div class="span12"

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=1444754&r1=1444753&r2=1444754&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 Mon Feb 11 13:05:18 2013
@@ -18,18 +18,21 @@
 #  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,
+                            api)
+from bhsearch.tests.search_resources import (ticket_search, wiki_search,
+                                             milestone_search)
 
 def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(index_with_whoosh.suite())
-    suite.addTest(whoosh_backend.suite())
-    suite.addTest(web_ui.suite())
-    suite.addTest(api.suite())
-    suite.addTest(ticket_search.suite())
-    suite.addTest(wiki_search.suite())
-    return suite
+    test_suite = unittest.TestSuite()
+    test_suite.addTest(index_with_whoosh.suite())
+    test_suite.addTest(whoosh_backend.suite())
+    test_suite.addTest(web_ui.suite())
+    test_suite.addTest(api.suite())
+    test_suite.addTest(ticket_search.suite())
+    test_suite.addTest(wiki_search.suite())
+    test_suite.addTest(milestone_search.suite())
+    return test_suite
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
\ No newline at end of file

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py Mon Feb 11 13:05:18 2013
@@ -101,8 +101,9 @@ class BaseBloodhoundSearchTest(unittest.
         return milestone
 
     def process_request(self):
-        response = BloodhoundSearchModule(self.env).process_request(self.req)
-        url, data, x = response
+        # pylint: disable=unused-variable
+        url, data, x = BloodhoundSearchModule(self.env).process_request(
+            self.req)
         print "Received url: %s data:" % url
         pprint(data)
         if data.has_key("results"):

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=1444754&r1=1444753&r2=1444754&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 Mon Feb 11 13:05:18 2013
@@ -19,7 +19,6 @@
 #  under the License.
 
 import unittest
-import tempfile
 import shutil
 from bhsearch.api import BloodhoundSearchApi
 from bhsearch.search_resources.milestone_search import MilestoneIndexer
@@ -27,13 +26,11 @@ from bhsearch.tests.base import BaseBloo
 from bhsearch.search_resources.ticket_search import TicketIndexer
 
 from bhsearch.whoosh_backend import WhooshBackend
-from trac.test import EnvironmentStub
 
 
 class IndexWhooshTestCase(BaseBloodhoundSearchTest):
     def setUp(self):
-        self.env = EnvironmentStub(enable=['bhsearch.*'])
-        self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+        super(IndexWhooshTestCase, self).setUp()
         self.whoosh_backend = WhooshBackend(self.env)
         self.whoosh_backend.recreate_index()
         self.search_api = BloodhoundSearchApi(self.env)

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=1444754&r1=1444753&r2=1444754&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 Mon Feb 11 13:05:18 2013
@@ -18,17 +18,17 @@
 #  specific language governing permissions and limitations
 #  under the License.
 import unittest
-import tempfile
-import shutil
 
 from urllib import urlencode, unquote
+from bhsearch.api import ASC, DESC
 
 from bhsearch.tests.base import BaseBloodhoundSearchTest
 from bhsearch.web_ui import RequestParameters
 from bhsearch.whoosh_backend import WhooshBackend
 
-from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.test import Mock, MockPerm
 from trac.ticket import Ticket
+from trac.core import TracError
 from trac.util.datefmt import FixedOffset
 from trac.util import format_datetime
 from trac.web import Href, arg_list_to_args, parse_arg_list, RequestDone
@@ -514,6 +514,31 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
         #assert
         self.assertEqual("grid", data["active_view"])
 
+    def test_can_apply_sorting(self):
+        #arrange
+        self.insert_ticket("T1", component="c1", status="new", milestone="A")
+        self.insert_ticket("T2", component="c1", status="new", milestone="B")
+        self.insert_ticket("T3", component="c3", status="new", milestone="C")
+        #act
+        self.req.args[RequestParameters.QUERY] = "*"
+        self.req.args[RequestParameters.SORT] = "component, milestone desc"
+        data = self.process_request()
+        #assert
+        api_sort = data["debug"]["api_parameters"]["sort"]
+        self.assertEqual([("component", ASC), ("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")
+        #act
+        self.req.args[RequestParameters.QUERY] = "*"
+        data = self.process_request()
+        #assert
+        self.assertIn("title", data["results"].items[0])
+
 
     def _count_parameter_in_url(self, url, parameter_name, value):
         parameter_to_find = (parameter_name, value)
@@ -539,8 +564,53 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
         for i in range(1, n+1):
             self.insert_wiki("test %s" % i)
 
+class RequestParametersTest(unittest.TestCase):
+    def setUp(self):
+        self.req = Mock(
+            perm=MockPerm(),
+            chrome={'logo': {}},
+            href=Href("/main"),
+            base_path=BASE_PATH,
+            args=arg_list_to_args([]),
+        )
+
+    def test_can_parse_multiple_sort_terms(self):
+        self.assertEqual(
+            None,
+            self._evaluate_sort("  "))
+        self.assertEqual(
+            None,
+            self._evaluate_sort(" ,  , "))
+        self.assertEqual(
+            [("f1", ASC),],
+            self._evaluate_sort(" f1 "))
+        self.assertEqual(
+            [("f1", ASC),],
+            self._evaluate_sort(" f1 asc"))
+        self.assertEqual(
+            [("f1", DESC),],
+            self._evaluate_sort("f1  desc"))
+        self.assertEqual(
+            [("f1", ASC), ("f2", DESC)],
+            self._evaluate_sort("f1, f2 desc"))
+
+    def test_can_raise_error_on_invalid_sort_term(self):
+        self.assertRaises(
+            TracError,
+            self._evaluate_sort,
+            "f1  desc bb")
+
+    def _evaluate_sort(self, sort_condition):
+        self.req.args[RequestParameters.SORT] = sort_condition
+        parameters = RequestParameters(self.req)
+        return parameters.sort
+
+
 def suite():
-    return unittest.makeSuite(WebUiTestCaseWithWhoosh, 'test')
+    test_suite = unittest.TestSuite()
+    test_suite.addTest(unittest.makeSuite(WebUiTestCaseWithWhoosh, 'test'))
+    test_suite.addTest(unittest.makeSuite(RequestParametersTest, 'test'))
+    return test_suite
 
 if __name__ == '__main__':
     unittest.main()

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=1444754&r1=1444753&r2=1444754&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 Mon Feb 11 13:05:18 2013
@@ -27,7 +27,6 @@ from bhsearch.query_parser import Defaul
 from bhsearch.tests.base import BaseBloodhoundSearchTest
 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
@@ -37,8 +36,7 @@ from whoosh.qparser import MultifieldPlu
 
 class WhooshBackendTestCase(BaseBloodhoundSearchTest):
     def setUp(self):
-        self.env = EnvironmentStub(enable=['bhsearch.*'])
-        self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+        super(WhooshBackendTestCase, self).setUp()
         self.whoosh_backend = WhooshBackend(self.env)
         self.whoosh_backend.recreate_index()
         self.parser = DefaultQueryParser(self.env)
@@ -453,8 +451,7 @@ class WhooshFunctionalityTestCase(unitte
 
 class WhooshEmptyFacetErrorWorkaroundTestCase(BaseBloodhoundSearchTest):
     def setUp(self):
-        self.env = EnvironmentStub(enable=['bhsearch.*'])
-        self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+        super(WhooshEmptyFacetErrorWorkaroundTestCase, self).setUp()
         self.whoosh_backend = WhooshBackend(self.env)
         self.whoosh_backend.recreate_index()
         self.parser = DefaultQueryParser(self.env)
@@ -498,12 +495,12 @@ class WhooshEmptyFacetErrorWorkaroundTes
         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
+    test_suite = unittest.TestSuite()
+    test_suite.addTest(unittest.makeSuite(WhooshBackendTestCase, 'test'))
+    test_suite.addTest(unittest.makeSuite(WhooshFunctionalityTestCase, 'test'))
+    test_suite.addTest(
+        unittest.makeSuite(WhooshEmptyFacetErrorWorkaroundTestCase, 'test'))
+    return test_suite
 
 if __name__ == '__main__':
     unittest.main()

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=1444754&r1=1444753&r2=1444754&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 Mon Feb 11 13:05:18 2013
@@ -43,8 +43,6 @@ from trac.wiki.formatter import extract_
 
 SEARCH_PERMISSION = 'SEARCH_VIEW'
 DEFAULT_RESULTS_PER_PAGE = 10
-DEFAULT_SORT = [(SCORE, ASC), ("time", DESC)]
-#VIEW_GRID = "grid"
 
 class RequestParameters(object):
     """
@@ -59,6 +57,7 @@ class RequestParameters(object):
     PAGELEN = "pagelen"
     FILTER_QUERY = "fq"
     VIEW = "view"
+    SORT = "sort"
 
     def __init__(self, req):
         self.req = req
@@ -76,14 +75,14 @@ class RequestParameters(object):
         self.filter_queries = self._remove_possible_duplications(
             self.filter_queries)
 
-        #TODO: retrieve sort from query string
-        self.sort = DEFAULT_SORT
+        sort_string = req.args.getfirst(self.SORT)
+        self.sort = self._parse_sort(sort_string)
 
         self.pagelen = int(req.args.getfirst(
             RequestParameters.PAGELEN,
             DEFAULT_RESULTS_PER_PAGE))
         self.page = int(req.args.getfirst(self.PAGE, '1'))
-        self.type = req.args.getfirst(self.TYPE, None)
+        self.type = req.args.getfirst(self.TYPE)
 
         self.params = {
             RequestParameters.FILTER_QUERY: []
@@ -103,6 +102,42 @@ 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
+
+    def _parse_sort(self, sort_string):
+        if not sort_string:
+            return None
+        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:
+                continue
+            term_parts = term.split()
+            parts_count = len(term_parts)
+            if parts_count == 1:
+                sort.append((term_parts[0], ASC))
+            elif parts_count == 2:
+                sort.append((term_parts[0], parse_sort_order(term_parts[1])))
+            else:
+                raise TracError("Invalid sort term %s " % term)
+
+        return sort if sort else None
+
+
 
     def _remove_possible_duplications(self, parameters_list):
         seen = set()
@@ -152,26 +187,8 @@ class RequestParameters(object):
         if name in params:
             del params[name]
 
-    def is_show_all_mode(self):
-        return self.type is None
-
-
 class BloodhoundSearchModule(Component):
     """Main search page"""
-    DATA_HEADERS = "headers"
-    DATA_ALL_VIEWS = "all_views"
-    DATA_ACTIVE_VIEW = "active_view"
-    DATA_VIEW = "view"
-    DATA_VIEW_GRID = "grid"
-    #bhsearch may support more pluggable views later
-    VIEWS_SUPPORTED = {
-        None: "Free text",
-        DATA_VIEW_GRID: "Grid"
-    }
-    VIEWS_WITH_KNOWN_FIELDS = [DATA_VIEW_GRID]
-
-    OBLIGATORY_FIELDS_TO_SELECT = [IndexFields.ID, IndexFields.TYPE]
-
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
         ITemplateProvider,
         #           IWikiSyntaxProvider #todo: implement later
@@ -196,6 +213,7 @@ class BloodhoundSearchModule(Component):
     default_facets = ListOption(
         BHSEARCH_CONFIG_SECTION,
         prefix + '_default_facets',
+        default=",".join([IndexFields.TYPE]),
         doc="""Default facets applied to search view of all resources""")
 
     default_view = Option(
@@ -204,7 +222,7 @@ class BloodhoundSearchModule(Component):
         doc="""If true, show grid as default view for specific resource in
             Bloodhound Search results""")
 
-    default_grid_fields = ListOption(
+    all_grid_fields = ListOption(
         BHSEARCH_CONFIG_SECTION,
         prefix + '_default_grid_fields',
         default=",".join(default_grid_fields),
@@ -213,6 +231,7 @@ class BloodhoundSearchModule(Component):
 
     # INavigationContributor methods
     def get_active_navigation_item(self, req):
+        # pylint: disable=unused-argument
         return 'bhsearch'
 
     def get_navigation_items(self, req):
@@ -231,175 +250,181 @@ class BloodhoundSearchModule(Component):
 
     def process_request(self, req):
         req.perm.assert_permission(SEARCH_PERMISSION)
-        parameters = RequestParameters(req)
-        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,
+        request_context = RequestContext(
+            self.env,
+            req,
+            self.search_participants,
+            self.default_view,
+            self.all_grid_fields,
+            self.default_facets
         )
 
-        #TBD: should search return results on empty query?
-        #        if not any((
-        #            query,
-        #            parameters.type,
-        #            parameters.filter_queries,
-        #            )):
-        #            return self._return_data(req, data)
-
-        self._prepare_quick_jump(
-            parameters,
-            data)
-        fields = self._prepare_fields_and_view(
-            allowed_participants, parameters, data)
-
-        query_filter = self._prepare_query_filter(
-            parameters, allowed_participants)
-
-        facets = self._prepare_facets(parameters, allowed_participants)
-
-        query_system = BloodhoundSearchApi(self.env)
-        query_result = query_system.query(
-            parameters.query,
-            pagenum=parameters.page,
-            pagelen=parameters.pagelen,
-            sort=parameters.sort,
-            fields=fields,
-            facets=facets,
-            filter=query_filter,
+        query_result =  BloodhoundSearchApi(self.env).query(
+            request_context.parameters.query,
+            pagenum=request_context.page,
+            pagelen=request_context.pagelen,
+            sort=request_context.sort,
+            fields=request_context.fields,
+            facets=request_context.facets,
+            filter=request_context.query_filter,
         )
 
-        is_free_text_view = (self.DATA_VIEW not in data)
-        ui_docs = [self._process_doc(
-                        doc, req, allowed_participants, is_free_text_view)
-                   for doc in query_result.docs]
+        request_context.process_results(query_result)
+        return self._return_data(req, request_context.data)
 
-        results = Paginator(
-            ui_docs,
-            parameters.page - 1,
-            parameters.pagelen,
-            query_result.hits)
-
-        results.shown_pages = self._prepare_shown_pages(
-            parameters,
-            shown_pages=results.get_shown_pages(parameters.pagelen))
+    def _return_data(self, req, data):
+        add_stylesheet(req, 'common/css/search.css')
+        return 'bhsearch.html', data, None
 
-        results.current_page = {'href': None,
-                                'class': 'current',
-                                'string': str(results.page + 1),
-                                'title': None}
+    # ITemplateProvider methods
+    def get_htdocs_dirs(self):
+    #        return [('bhsearch',
+    #                 pkg_resources.resource_filename(__name__, 'htdocs'))]
+        return []
 
-        if results.has_next_page:
-            next_href = parameters.create_href(page=parameters.page + 1)
-            add_link(req, 'next', next_href, _('Next Page'))
+    def get_templates_dirs(self):
+        return [pkg_resources.resource_filename(__name__, 'templates')]
 
-        if results.has_previous_page:
-            prev_href = parameters.create_href(page=parameters.page - 1)
-            add_link(req, 'prev', prev_href, _('Previous Page'))
+class RequestContext(object):
+    DATA_ACTIVE_FILTER_QUERIES = 'active_filter_queries'
+    DATA_ACTIVE_TYPE = "active_type"
+    DATA_TYPES = "types"
+    DATA_HEADERS = "headers"
+    DATA_ALL_VIEWS = "all_views"
+    DATA_ACTIVE_VIEW = "active_view"
+    DATA_VIEW = "view"
+    DATA_VIEW_GRID = "grid"
+    DATA_FACET_COUNTS = 'facet_counts'
+    DATA_DEBUG = 'debug'
+    DATA_PAGE_HREF = 'page_href'
+    DATA_RESULTS = 'results'
+    DATA_QUICK_JUMP = "quickjump"
 
-        data['results'] = results
 
-        self._prepare_result_facet_counts(
-            parameters,
-            query_result,
-            data,
-        )
+    #bhsearch may support more pluggable views later
+    VIEWS_SUPPORTED = {
+        None: "Free text",
+        DATA_VIEW_GRID: "Grid"
+    }
 
-        data['page_href'] = parameters.create_href()
-        return self._return_data(req, data)
+    VIEWS_WITH_KNOWN_FIELDS = [DATA_VIEW_GRID]
+    OBLIGATORY_FIELDS_TO_SELECT = [IndexFields.ID, IndexFields.TYPE]
+    DEFAULT_SORT = [(SCORE, ASC), ("time", DESC)]
 
+    def __init__(
+            self,
+            env,
+            req,
+            search_participants,
+            default_view,
+            all_grid_fields,
+            default_facets,
+            ):
+        self.env = env
+        self.req = req
+        self.parameters = RequestParameters(req)
+        self.search_participants = search_participants
+        self.default_view = default_view
+        self.all_grid_fields = all_grid_fields
+        self.default_facets = default_facets
+        self.view = None
+        self.page = self.parameters.page
+        self.pagelen = self.parameters.pagelen
+        self.allowed_participants, self.sorted_participants = \
+            self._get_allowed_participants(req)
+
+        if self.parameters.type in self.allowed_participants:
+            self.active_type = self.parameters.type
+            self.active_participant = self.allowed_participants[
+                                      self.active_type]
+        else:
+            self.active_type = None
+            self.active_participant = None
 
-    def _prepare_fields_and_view(self, allowed_participants, parameters, data):
-        self._add_views_selector(parameters, data)
-        active_participant = self._get_active_participant(
-            parameters, allowed_participants)
-        view = self._get_view(parameters, active_participant)
-        if view:
-            data[self.DATA_VIEW] = view
-        fields_to_select = None
-        if view in self.VIEWS_WITH_KNOWN_FIELDS:
-            if active_participant:
-                fields_in_view = active_participant.get_default_view_fields(
-                    view)
-            elif view == self.DATA_VIEW_GRID:
-                fields_in_view = self.default_grid_fields
-            else:
-                raise TracError("Unsupported view: %s" % view)
-            data[self.DATA_HEADERS] = [self._create_headers_item(field)
-                                        for field in fields_in_view]
-            fields_to_select = self._optionally_add_obligatory_fields(
-                fields_in_view)
-        return fields_to_select
+        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()
 
-    def _add_views_selector(self, parameters, data):
-        active_view = parameters.view
-        all_views = []
-        for view, label in self.VIEWS_SUPPORTED.iteritems():
-            all_views.append(dict(
-                label=_(label),
-                href=parameters.create_href(
-                    view=view, skip_view=(view is None)),
-                is_active = (view == active_view)
-            ))
-        data[self.DATA_ALL_VIEWS] = all_views
-        if parameters.view:
-            data[self.DATA_ACTIVE_VIEW] = parameters.view
+        self.sort = self.parameters.sort if self.parameters.sort \
+            else self.DEFAULT_SORT
 
-    def _optionally_add_obligatory_fields(self, fields_in_view):
-        fields_to_select = list(fields_in_view)
-        for obligatory_field in self.OBLIGATORY_FIELDS_TO_SELECT:
-            if obligatory_field is not fields_to_select:
-                fields_to_select.append(obligatory_field)
-        return fields_to_select
 
-    def _create_headers_item(self, field):
-        return dict(
-            name=field,
-            href="",
-            #TODO:add translated column label. Now it is really temporary
-            #workaround
-            label=field,
-            sort=None,
-        )
 
-    def _get_view(self, parameters, active_participant):
-        view = parameters.view
-        if view is None:
-            if active_participant is not None:
-                view = active_participant.get_default_view()
-            else:
-                view = self.default_view
-        if view is not None:
-            view =  view.strip().lower()
-        return view
+    def _get_allowed_participants(self, req):
+        allowed_participants = {}
+        ordered_participants = []
+        for participant in self.search_participants:
+            if participant.is_allowed(req):
+                allowed_participants[
+                    participant.get_participant_type()] = participant
+                ordered_participants.append(participant)
+        return allowed_participants, ordered_participants
 
 
-    def _get_active_participant(self, parameters, allowed_participants):
-        active_type = parameters.type
-        if active_type is not None and \
-           active_type in allowed_participants:
-            return allowed_participants[active_type]
-        else:
-            return None
+    def _prepare_allowed_types(self):
+        active_type = self.parameters.type
+        if active_type and active_type not in self.allowed_participants:
+            raise TracError(_("Unsupported resource type: '%(name)s'",
+                name=active_type))
+        allowed_types = [
+            dict(
+                label=_("All"),
+                active=(active_type is None),
+                href=self.parameters.create_href(
+                    skip_type=True,
+                    skip_page=True,
+                    force_filters=[],
+                ),
+            )
+        ]
+        #we want obtain the same order as in search participants options
+        for participant in self.sorted_participants:
+            allowed_types.append(dict(
+                label=_(participant.get_title()),
+                active=(participant.get_participant_type() == active_type),
+                href=self.parameters.create_href(
+                    type=participant.get_participant_type(),
+                    skip_page=True,
+                    force_filters=[],
+                ),
+            ))
+        self.data[self.DATA_TYPES] = allowed_types
+        self.data[self.DATA_ACTIVE_TYPE] = active_type
 
+    def _prepare_active_filter_queries(self):
+        current_filters = self.parameters.filter_queries
+
+        def remove_filters_from_list(filer_to_cut_from):
+            return current_filters[:current_filters.index(filer_to_cut_from)]
+
+        active_filter_queries = [
+            dict(
+                href=self.parameters.create_href(
+                    force_filters=remove_filters_from_list(filter_query)
+                ),
+                label=filter_query,
+                query=filter_query,
+            ) for filter_query in self.parameters.filter_queries
+        ]
+        self.data[self.DATA_ACTIVE_FILTER_QUERIES] = active_filter_queries
 
-    def _prepare_quick_jump(self,
-                            parameters,
-                            data):
-        if not parameters.query:
+    def _prepare_quick_jump(self):
+        if not self.parameters.query:
             return
         check_result = self._check_quickjump(
-            parameters.req,
-            parameters.query)
+            self.req,
+            self.parameters.query)
         if check_result:
-            data["quickjump"] = check_result
+            self.data[self.DATA_QUICK_JUMP] = check_result
 
     #the method below is "copy/paste" from trac search/web_ui.py
     def _check_quickjump(self, req, kwd):
         """Look for search shortcuts"""
+        # pylint: disable=maybe-no-member
         noquickjump = int(req.args.get('noquickjump', '0'))
         # Source quickjump   FIXME: delegate to ISearchSource.search_quickjump
         quickjump_href = None
@@ -425,65 +450,155 @@ class BloodhoundSearchModule(Component):
                 req.redirect(quickjump_href)
 
 
-    def _prepare_allowed_types(self, allowed_participants, parameters, data):
-        active_type = parameters.type
-        if active_type and active_type not in allowed_participants:
-            raise TracError(_("Unsupported resource type: '%(name)s'",
-                name=active_type))
-        allowed_types = [
-            dict(
-                label=_("All"),
-                active=(active_type is None),
-                href=parameters.create_href(
-                    skip_type=True,
-                    skip_page=True,
-                    force_filters=[],
-                ),
-            )
-        ]
-
-        #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]
-                allowed_types.append(dict(
-                    label=_(participant.get_title()),
-                    active=(type == active_type),
-                    href=parameters.create_href(
-                        type=type,
-                        skip_page=True,
-                        force_filters=[],
-                    ),
-                ))
-        data["types"] = allowed_types
-        data["active_type"] = active_type
+    def _prepare_fields_and_view(self):
+        self._add_views_selector()
+        self.view = self._get_view()
+        if self.view:
+            self.data[self.DATA_VIEW] = self.view
+        fields_to_select = None
+        if self.view in self.VIEWS_WITH_KNOWN_FIELDS:
+            if self.active_participant:
+                fields_in_view = self.active_participant.\
+                    get_default_view_fields(self.view)
+            elif self.view == self.DATA_VIEW_GRID:
+                fields_in_view = self.all_grid_fields
+            else:
+                raise TracError("Unsupported view: %s" % self.view)
+            self.data[self.DATA_HEADERS] = [self._create_headers_item(field)
+                                        for field in fields_in_view]
+            fields_to_select = self._add_obligatory_fields(
+                fields_in_view)
+        return fields_to_select
 
+    def _add_views_selector(self):
+        active_view = self.parameters.view
+        if active_view:
+            self.data[self.DATA_ACTIVE_VIEW] = active_view
 
-    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,
+        all_views = []
+        for view, label in self.VIEWS_SUPPORTED.iteritems():
+            all_views.append(dict(
+                label=_(label),
+                href=self.parameters.create_href(
+                    view=view, skip_view=(view is None)),
+                is_active = (view == active_view)
             ))
-        data['active_filter_queries'] = active_filter_queries
+        self.data[self.DATA_ALL_VIEWS] = all_views
 
-    def _cut_filters(self, filter_queries, filer_to_cut_from):
-        return filter_queries[:filter_queries.index(filer_to_cut_from)]
+    def _get_view(self):
+        view = self.parameters.view
+        if view is None:
+            if self.active_participant:
+                view = self.active_participant.get_default_view()
+            else:
+                view = self.default_view
+        if view is not None:
+            view =  view.strip().lower()
+        if view == "":
+            view = None
+        return view
 
+    def _add_obligatory_fields(self, fields_in_view):
+        fields_to_select = list(fields_in_view)
+        for obligatory_field in self.OBLIGATORY_FIELDS_TO_SELECT:
+            if obligatory_field is not fields_to_select:
+                fields_to_select.append(obligatory_field)
+        return fields_to_select
 
-    def _prepare_result_facet_counts(self, parameters, query_result, data):
-        """
+    def _create_headers_item(self, field):
+        return dict(
+            name=field,
+            href="",
+            #TODO:add translated column label. Now it is really temporary
+            #workaround
+            label=field,
+            sort=None,
+        )
+
+    def _prepare_query_filter(self):
+        query_filters = list(self.parameters.filter_queries)
+        if self.active_type:
+            query_filters.append(
+                self._create_term_expression(
+                    IndexFields.TYPE, self.active_type))
+        return query_filters
+
+    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_facets(self):
+        #TODO: add possibility of specifying facets in query parameters
+        if self.active_participant:
+            facets =  self.active_participant.get_default_facets()
+        else:
+            facets =  self.default_facets
+        return facets
+
+    def _process_doc(self, doc):
+        ui_doc = dict(doc)
+        ui_doc["href"] = self.req.href(doc['type'], doc['id'])
+        #todo: perform content adaptation here
+        if doc.has_key('content'):
+            ui_doc['content'] = shorten_result(doc['content'])
+        if doc.has_key('time'):
+            ui_doc['date'] = user_time(self.req, format_datetime, doc['time'])
+
+        is_free_text_view = self.view is None
+        if is_free_text_view:
+            ui_doc['title'] = self.allowed_participants[
+                              doc['type']].format_search_results(doc)
+        return ui_doc
+
+    def _prepare_results(self, result_docs, hits):
+        ui_docs = [self._process_doc(doc) for doc in result_docs]
+
+        results = Paginator(
+            ui_docs,
+            self.page - 1,
+            self.pagelen,
+            hits)
+
+        self._prepare_shown_pages(results)
+        results.current_page = {'href': None,
+                                'class': 'current',
+                                'string': str(results.page + 1),
+                                'title': None}
+
+        parameters = self.parameters
+        if results.has_next_page:
+            next_href = parameters.create_href(page=parameters.page + 1)
+            add_link(self.req, 'next', next_href, _('Next Page'))
+
+        if results.has_previous_page:
+            prev_href = parameters.create_href(page=parameters.page - 1)
+            add_link(self.req, 'prev', prev_href, _('Previous Page'))
+
+        self.data[self.DATA_RESULTS] = results
 
+    def _prepare_shown_pages(self, results):
+        shown_pages = results.get_shown_pages(self.pagelen)
+        page_data = []
+        for shown_page in shown_pages:
+            page_href = self.parameters.create_href(page=shown_page)
+            page_data.append([page_href, None, str(shown_page),
+                              'page ' + str(shown_page)])
+        fields = ['href', 'class', 'string', 'title']
+        results.shown_pages = [dict(zip(fields, p)) for p in page_data]
+
+    def process_results(self, query_result):
+        self._prepare_results(query_result.docs, query_result.hits)
+        self._prepare_result_facet_counts(query_result.facets)
+        self.data[self.DATA_DEBUG] = query_result.debug
+        self.data[self.DATA_PAGE_HREF] = self.parameters.create_href()
+
+    def _prepare_result_facet_counts(self, result_facets):
+        """
         Sample query_result.facets content returned by query
         {
            'component': {None:2},
@@ -500,19 +615,18 @@ class BloodhoundSearchModule(Component):
         }
 
         """
-        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(
+                        href = self.parameters.create_href(
                             skip_page=True,
                             force_filters=[],
                             type=field_value)
                     else:
-                        href = parameters.create_href(
+                        href = self.parameters.create_href(
                             skip_page=True,
                             additional_filter=self._create_term_expression(
                                 field,
@@ -524,81 +638,5 @@ class BloodhoundSearchModule(Component):
                     )
                 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)
-        else:
-            type_participant = allowed_participants[parameters.type]
-            facets = type_participant.get_default_facets()
-        return facets
-
-    def _get_allowed_participants(self, req):
-        allowed_participants = {}
-        for participant in self.search_participants:
-            type = participant.get_search_filters(req)
-            if type is not None:
-                allowed_participants[type] = participant
-        return allowed_participants
-
-    def _return_data(self, req, data):
-        add_stylesheet(req, 'common/css/search.css')
-        return 'bhsearch.html', data, None
-
-    def _process_doc(self, doc, req, allowed_participants, is_free_text_view):
-        ui_doc = dict(doc)
-        ui_doc["href"] = req.href(doc['type'], doc['id'])
-        #todo: perform content adaptation here
-        if doc.has_key('content'):
-            ui_doc['excerpt'] = shorten_result(doc['content'])
-        if doc.has_key('time'):
-            ui_doc['date'] = user_time(req, format_datetime, doc['time'])
-
-        if is_free_text_view:
-            ui_doc['title'] = allowed_participants[
-                              doc['type']].format_search_results(doc)
-        return ui_doc
-
-    def _prepare_shown_pages(self, parameters, shown_pages):
-        page_data = []
-        for shown_page in shown_pages:
-            page_href = parameters.create_href(page=shown_page)
-            page_data.append([page_href, None, str(shown_page),
-                              'page ' + str(shown_page)])
-        fields = ['href', 'class', 'string', 'title']
-        result_shown_pages = [dict(zip(fields, p)) for p in page_data]
-        return result_shown_pages
-
-
-    # ITemplateProvider methods
-    def get_htdocs_dirs(self):
-    #        return [('bhsearch',
-    #                 pkg_resources.resource_filename(__name__, 'htdocs'))]
-        return []
-
-    def get_templates_dirs(self):
-        return [pkg_resources.resource_filename(__name__, 'templates')]
-
+        self.data[self.DATA_FACET_COUNTS] = facet_counts
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py Mon Feb 11 13:05:18 2013
@@ -23,7 +23,7 @@ try:
 except ImportError:
     from distutils.core import setup
 
-DESC = """Search plugin for Apache(TM) Bloodhound
+DESC = """Search plugin for Apache(TM) Bloodhound.
 
 Add free text search and query functionality to Bloodhound sites.
 """
@@ -128,6 +128,9 @@ setup(
     name=DIST_NM,
     version=latest,
     description=DESC.split('\n', 1)[0],
+    author = "Apache Bloodhound",
+    license = "Apache License v2",
+    url = "http://incubator.apache.org/bloodhound/",
     requires = ['trac'],
     install_requires = [
         'setuptools>=0.6b1',

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css Mon Feb 11 13:05:18 2013
@@ -67,6 +67,10 @@ body {
   display: none
 }
 
+.nav-header-selected {
+  color: #b94a48;
+}
+
 /* @end */
 
 /* @group Heading anchors */
@@ -538,6 +542,18 @@ input[type="submit"].btn.btn-micro {
   }
 }
 
+@media (min-width: 1200px) {
+  .facets {
+      width: 220px;
+  }
+}
+
+@media (min-width: 1200px) {
+  .search_results {
+      width: 920px;
+  }
+}
+
 @media (min-width: 979px) and (max-width: 1199px) {
   .main-nav ul {
     background-color: red;
@@ -553,13 +569,13 @@ input[type="submit"].btn.btn-micro {
   #logo, #usermenu, #breadcrumbbar {
     float: left;
   }
-    
+
   #searchbox {
     float: left;
     margin-right: 10px;
   }
 
-  .breadcrumb {
+  #mainnav {
     margin-top: 7px;
   }
 
@@ -583,12 +599,6 @@ input[type="submit"].btn.btn-micro {
     margin-top: 5px;
   }
 
-  .ticket .properties div[class*="span"] {
-    float: left;
-    margin-left: 10px;
-    margin-right: 10px;
-  }
-
   #changelog .change .span2 {
     float: left;
   }
@@ -609,7 +619,7 @@ input[type="submit"].btn.btn-micro {
   .container:first-child {
     border-left: 0px solid white;
     border-right: 0px solid white;
-    padding: 0px;
+    padding: 0px 4px;
   }
 
   header, #belowStatus, #mobile-activity,

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin.html?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin.html Mon Feb 11 13:05:18 2013
@@ -25,18 +25,17 @@
       xmlns:py="http://genshi.edgewall.org/"
       py:strip="">
 
-  <py:def function="admin_nav_list()">
-    <div class="well">
-      <ul class="nav nav-list">
+  <py:def function="admin_nav_list(phone)">
+    <div class="${'btn-group' if phone else 'well'}">
+      <py:if test="phone">
+        <a class="btn btn-large dropdown-toggle" data-toggle="dropdown" href="#">Select Module&nbsp;&nbsp;<span class="caret"></span></a>
+      </py:if>
+
+      <ul class="nav nav-list${' dropdown-menu' if phone else None}">
         <py:for each="category, panels in groupby(panels, lambda p: p.category)"
             py:with="cat_is_active = category.id == active_cat">
-          <li class="nav-header">
-            <py:choose test="">
-              <span py:when="cat_is_active" class="label label-important">
-                ${category.label}
-              </span>
-              <py:otherwise>${category.label}</py:otherwise>
-            </py:choose>
+          <li class="nav-header${' nav-header-selected' if cat_is_active else None}">
+            ${category.label}
           </li>
           <li py:for="panel in panels" py:with="panel = panel.panel;
                   pan_is_active = cat_is_active and panel.id == active_panel"
@@ -49,10 +48,16 @@
   </py:def>
   <py:def function="admin_function(fix_legacy)">
     <div id="content" class="admin row">
-      <h1>Administration</h1>
-      <div id="tabs" class="span3">
-        ${admin_nav_list()}
+      <h1 class="span12">Administration</h1>
+
+      <div id="tabs" class="span3 hidden-phone">
+        ${admin_nav_list(False)}
       </div>
+
+      <div class="span9 visible-phone">
+        ${admin_nav_list(True)}
+      </div>
+
       <div id="tabcontent" class="span9">
         <py:choose>
         <py:when test="fix_legacy">

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html Mon Feb 11 13:05:18 2013
@@ -36,7 +36,7 @@
   </head>
 
   <body>
-    <h2>Manage Milestones</h2>
+    <h2>Milestone Management</h2>
 
     <py:choose test="view">
       <form py:when="'detail'" class="well form-horizontal" method="post" 
@@ -107,11 +107,51 @@
       </form>
 
       <py:otherwise>
+        <div py:if="'MILESTONE_CREATE' in req.perm" class="row">
+          <div class="span9">
+            <form class="well form-horizontal" id="addmilestone" method="post" action="">
+              <fieldset>
+                <legend>Add Milestone</legend>
+                <div class="control-group">
+                  <label class="control-label" for="name">Milestone Name</label>
+                  <div class="controls">
+                    <input type="text" name="name" id="name" class="input-xlarge" />
+                  </div>
+                </div>
+
+                <div class="control-group">
+                  <label class="control-label" for="duedate">Due Date</label>
+                  <div class="controls">
+                    <input type="text" id="duedate"
+                        name="duedate" size="${len(datetime_hint)}"
+                        class="input-xlarge"
+                        placeholder="${_('Format: %(datehint)s', datehint=datetime_hint)}" />
+                  </div>
+                </div>
+
+                <div class="control-group">
+                  <div class="controls">
+                    <input class="btn" type="submit" name="add" value="${_('Add')}" />
+                  </div>
+
+                  <p class="help-block">
+                    <span class="label label-info">Hint</span>
+                    <i18n:msg params="datehint">Format: $datetime_hint</i18n:msg>
+                  </p>
+                </div>
+              </fieldset>
+            </form>
+          </div>
+        </div>
+
         <div class="row">
-          <div class="${'MILESTONE_CREATE' in req.perm and 'span6' or 'span9'}">
+          <div class="span9">
             <py:choose>
-              <form id="milestone_table" method="post" action=""
+              <form id="milestone_table" method="post" action="" class="well"
                     py:when="milestones" py:with="can_remove = 'MILESTONE_DELETE' in req.perm">
+      
+                <legend>Modify Milestones</legend>
+
                 <table id="millist"
                     class="table table-bordered table-striped table-condensed">
                     
@@ -161,28 +201,6 @@
               </p>
             </py:choose>
           </div>
-          <div class="span3" py:if="'MILESTONE_CREATE' in req.perm">
-            <form class="well" id="addmilestone" method="post" action="">
-              <fieldset>
-                <legend>Add Milestone:</legend>
-                <label for="name">Name:</label>
-                <input class="input-medium" type="text" name="name"
-                    id="name" size="22" />
-                <label for="duedate">Due:</label>
-                <input class="input-medium" type="text" id="duedate"
-                    name="duedate" size="${len(datetime_hint)}"
-                    placeholder="${_('Format: %(datehint)s', datehint=datetime_hint)}" />
-                <p class="help-block">
-                  <span class="label label-info">Hint</span>
-                  <i18n:msg params="datehint">Format: $datetime_hint</i18n:msg>
-                </p>
-                <div class="control-group">
-                  <input class="btn" type="submit" name="add"
-                      value="${_('Add')}" />
-                </div>
-              </fieldset>
-            </form>
-          </div>
         </div>
       </py:otherwise>
     </py:choose>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html?rev=1444754&r1=1444753&r2=1444754&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html Mon Feb 11 13:05:18 2013
@@ -29,6 +29,33 @@
     <title>Plugins</title>
     <script type="text/javascript"
         src="${href.chrome('dashboard/js/bootstrap-collapse.js')}"></script>
+    <script type="text/javascript">/*<![CDATA[*/
+    jQuery(document).ready(function($) {
+      // Sets state of group toggler when component checkboxes are clicked
+      function setGroupTogglerState() {
+        var table = $(this).closest("table.table");
+        var checkboxes = $("td.sel input:checkbox:enabled", table);
+        var numSelected = checkboxes.filter(":checked").length;
+        var noneSelected = numSelected === 0;
+        var allSelected = numSelected === checkboxes.length;
+        $("th.sel input:checkbox", table)
+          .prop({"checked": allSelected,
+            "indeterminate": !(noneSelected || allSelected)});
+      }
+      // Add group event behavior for controlling state of toggler
+      $("table.table td.sel input:checkbox:enabled")
+        .click(setGroupTogglerState);
+      // Add click behavior for the group toggler and initialize its state
+      $("table.table th.sel").each(function() {
+          $(this).attr("title", $(this).text())
+      }).html('<input type="checkbox" />')
+        .children()
+        .click(function() {
+          $("td.sel input:checkbox:enabled",
+            $(this).closest("table.table")).prop("checked", this.checked);
+        }).each(setGroupTogglerState);
+    });
+    /*]]>*/</script>
   </head>
 
   <body>