Added search results portlet renderer
authorThierry Florac <tflorac@ulthar.net>
Wed, 25 Sep 2019 10:05:57 +0200
changeset 461 0aebe2d107ac
parent 460 77cec5c9ef84
child 462 042afdb6a0d1
Added search results portlet renderer
src/pyams_default_theme/features/search/portlet/__init__.py
src/pyams_default_theme/features/search/portlet/interfaces.py
src/pyams_default_theme/features/search/portlet/templates/search-panel.pt
src/pyams_default_theme/features/search/portlet/templates/search-panels.pt
src/pyams_default_theme/features/search/portlet/templates/search-result.pt
src/pyams_default_theme/features/search/portlet/templates/search-results.pt
src/pyams_default_theme/interfaces.py
--- a/src/pyams_default_theme/features/search/portlet/__init__.py	Tue Sep 17 12:00:26 2019 +0200
+++ b/src/pyams_default_theme/features/search/portlet/__init__.py	Wed Sep 25 10:05:57 2019 +0200
@@ -10,8 +10,6 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
 from persistent import Persistent
 from zope.container.contained import Contained
 from zope.interface import Interface, implementer
@@ -21,9 +19,11 @@
 from pyams_content.features.search import ISearchFolder
 from pyams_content.features.search.portlet import ISearchResultsPortletSettings
 from pyams_content.shared.common import IWfSharedContent
-from pyams_default_theme.features.search.portlet.interfaces import ISearchResultHeader, ISearchResultRenderer, \
-    ISearchResultTarget, ISearchResultTitle, ISearchResultsPortletDefaultRendererSettings
-from pyams_default_theme.interfaces import ISearchResultsView
+from pyams_default_theme.features.search.portlet.interfaces import ISearchResultHeader, \
+    ISearchResultRenderer, ISearchResultTarget, ISearchResultTitle, \
+    ISearchResultsPortletDefaultRendererSettings, ISearchResultsPortletPanelsRendererSettings, \
+    ISearchResultsPortletRendererBaseSettings
+from pyams_default_theme.interfaces import ISearchResultsPanelView, ISearchResultsView
 from pyams_default_theme.shared.common.interfaces import ISharedContentHeadViewletManager
 from pyams_i18n.interfaces import II18n
 from pyams_portal.interfaces import IPortalContext, IPortletRenderer
@@ -36,6 +36,9 @@
 from pyams_utils.url import canonical_url, relative_url
 from pyams_viewlet.viewlet import ViewContentProvider, Viewlet, viewlet_config
 
+
+__docformat__ = 'restructuredtext'
+
 from pyams_default_theme import _
 
 
@@ -43,45 +46,43 @@
 # Search folder custom head specificities renderer
 #
 
-@viewlet_config(name='search-folder-head', context=ISearchFolder, layer=IPyAMSUserLayer, view=Interface,
-                manager=ISharedContentHeadViewletManager, weight=1)
+@viewlet_config(name='search-folder-head', context=ISearchFolder, layer=IPyAMSUserLayer,
+                view=Interface, manager=ISharedContentHeadViewletManager, weight=1)
 @template_config(template='templates/folder-head-specificities.pt', layer=IPyAMSUserLayer)
 class SearchFolderHeadViewlet(Viewlet):
     """Search folder head specificities viewlet"""
 
 
 #
-# Search results portlet renderers
+# Search results portlet base settings and renderers
 #
 
-@factory_config(provided=ISearchResultsPortletDefaultRendererSettings)
-class SearchResultsPortletDefaultRendererSettings(Persistent, Contained):
-    """Search results portlet default renderer settings"""
+@implementer(ISearchResultsPortletRendererBaseSettings)
+class SearchResultsPortletRendererBaseSettings(Persistent, Contained):
+    """Search results portlet base renderer settings"""
 
-    display_results_count = FieldProperty(ISearchResultsPortletDefaultRendererSettings['display_results_count'])
-    allow_sorting = FieldProperty(ISearchResultsPortletDefaultRendererSettings['allow_sorting'])
-    allow_pagination = FieldProperty(ISearchResultsPortletDefaultRendererSettings['allow_pagination'])
+    display_results_count = FieldProperty(
+        ISearchResultsPortletRendererBaseSettings['display_results_count'])
+    allow_sorting = FieldProperty(ISearchResultsPortletRendererBaseSettings['allow_sorting'])
+    allow_pagination = FieldProperty(ISearchResultsPortletRendererBaseSettings['allow_pagination'])
 
 
-@adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, ISearchResultsPortletSettings),
-                provides=IPortletRenderer)
-@template_config(template='templates/search-results.pt', layer=IPyAMSLayer)
-@implementer(ISearchResultsView)
-class SearchResultsPortletDefaultRenderer(PortletRenderer):
-    """Search results portlet default renderer"""
+class SearchResultsPortletBaseRenderer(PortletRenderer):
+    """Search results portlet base renderer"""
 
-    label = _("Default search results")
-
-    settings_interface = ISearchResultsPortletDefaultRendererSettings
+    default_page_length = 10
 
     def update(self):
         settings = self.renderer_settings
         if not settings.allow_pagination:
             self.request.GET['length'] = '999'
-        super(SearchResultsPortletDefaultRenderer, self).update()
+        elif 'length' not in self.request.params:
+            self.request.GET['length'] = str(self.default_page_length)
+        super(SearchResultsPortletBaseRenderer, self).update()
 
     def render_item(self, item):
-        renderer = self.request.registry.queryMultiAdapter((item, self.request, self), ISearchResultRenderer)
+        renderer = self.request.registry.queryMultiAdapter((item, self.request, self),
+                                                           ISearchResultRenderer)
         if renderer is not None:
             renderer.update()
             return renderer.render()
@@ -89,24 +90,79 @@
             return ''
 
 
+#
+# Search results portlet default renderer
+#
+
+@factory_config(provided=ISearchResultsPortletDefaultRendererSettings)
+class SearchResultsPortletDefaultRendererSettings(SearchResultsPortletRendererBaseSettings):
+    """Search results portlet default renderer settings"""
+
+
+@adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, ISearchResultsPortletSettings),
+                provides=IPortletRenderer)
+@template_config(template='templates/search-results.pt', layer=IPyAMSLayer)
+@implementer(ISearchResultsView)
+class SearchResultsPortletDefaultRenderer(SearchResultsPortletBaseRenderer):
+    """Search results portlet default renderer"""
+
+    label = _("Default search results")
+
+    settings_interface = ISearchResultsPortletDefaultRendererSettings
+
+
+#
+# Search results portlet panels renderer
+#
+
+@factory_config(provided=ISearchResultsPortletPanelsRendererSettings)
+class SearchResultsPortletPanelsRendererSettings(SearchResultsPortletRendererBaseSettings):
+    """Search results portlet panel renderer settings"""
+
+    button_title = FieldProperty(ISearchResultsPortletPanelsRendererSettings['button_title'])
+
+
+@adapter_config(name='panels',
+                context=(IPortalContext, IPyAMSLayer, Interface, ISearchResultsPortletSettings),
+                provides=IPortletRenderer)
+@template_config(template='templates/search-panels.pt', layer=IPyAMSLayer)
+@implementer(ISearchResultsPanelView)
+class SearchResultsPortletPanelsRenderer(SearchResultsPortletBaseRenderer):
+    """Search results portlet panels renderer"""
+
+    label = _("Paneled search results")
+
+    settings_interface = ISearchResultsPortletPanelsRendererSettings
+    default_page_length = 9
+
+    weight = 20
+
+
+#
+# Search results adapters
+#
+
 @adapter_config(context=(ILocation, IPyAMSUserLayer, ISearchResultsView), provides=IBreadcrumbs)
 class BreadcrumbsAdapter(NullAdapter):
     """Disable breadcrumbs in search results view"""
 
 
-@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView), provides=ISearchResultTitle)
+@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView),
+                provides=ISearchResultTitle)
 def shared_content_result_title_adapter(context, request, view):
     """Shared content result title adapter"""
     return II18n(context).query_attribute('title', request=request)
 
 
-@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView), provides=ISearchResultHeader)
+@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView),
+                provides=ISearchResultHeader)
 def shared_content_result_header_adapter(context, request, view):
     """Shared content result header adapter"""
     return II18n(context).query_attribute('header', request=request)
 
 
-@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView), provides=ISearchResultTarget)
+@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView),
+                provides=ISearchResultTarget)
 def shared_content_result_target_adapter(context, request, view):
     """Shared content result target URL adapter"""
     if view.settings.force_canonical_url:
@@ -115,7 +171,8 @@
         return relative_url(context, request)
 
 
-@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView), provides=ISearchResultRenderer)
+@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsView),
+                provides=ISearchResultRenderer)
 @template_config(template='templates/search-result.pt', layer=IPyAMSUserLayer)
 @implementer(ISearchResultsView)
 class WfSharedContentSearchResultRenderer(ViewContentProvider):
@@ -123,12 +180,22 @@
 
     @property
     def url(self):
-        return self.request.registry.queryMultiAdapter((self.context, self.request, self.view), ISearchResultTarget)
+        return self.request.registry.queryMultiAdapter((self.context, self.request, self.view),
+                                                       ISearchResultTarget)
 
     @property
     def title(self):
-        return self.request.registry.queryMultiAdapter((self.context, self.request, self.view), ISearchResultTitle)
+        return self.request.registry.queryMultiAdapter((self.context, self.request, self.view),
+                                                       ISearchResultTitle)
 
     @property
     def header(self):
-        return self.request.registry.queryMultiAdapter((self.context, self.request, self.view), ISearchResultHeader)
+        return self.request.registry.queryMultiAdapter((self.context, self.request, self.view),
+                                                       ISearchResultHeader)
+
+
+@adapter_config(context=(IWfSharedContent, IPyAMSUserLayer, ISearchResultsPanelView),
+                provides=ISearchResultRenderer)
+@template_config(template='templates/search-panel.pt', layer=IPyAMSUserLayer)
+class WfSharedContentSearchResultPanelRenderer(WfSharedContentSearchResultRenderer):
+    """Shared content search result panel renderer"""
--- a/src/pyams_default_theme/features/search/portlet/interfaces.py	Tue Sep 17 12:00:26 2019 +0200
+++ b/src/pyams_default_theme/features/search/portlet/interfaces.py	Wed Sep 25 10:05:57 2019 +0200
@@ -9,6 +9,8 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
+from pyams_i18n.schema import I18nTextLineField
+
 
 __docformat__ = 'restructuredtext'
 
@@ -39,8 +41,8 @@
     header = Attribute("Search result header")
 
 
-class ISearchResultsPortletDefaultRendererSettings(Interface):
-    """Search results portlet default renderer settings interface"""
+class ISearchResultsPortletRendererBaseSettings(Interface):
+    """Search results portlet renderer base settings interface"""
 
     display_results_count = Bool(title=_("Display results count?"),
                                  description=_("If 'no', results count will not be displayed"),
@@ -56,3 +58,18 @@
                             description=_("If 'no', results will not be paginated"),
                             required=True,
                             default=True)
+
+
+class ISearchResultsPortletDefaultRendererSettings(ISearchResultsPortletRendererBaseSettings):
+    """Search results portlet default renderer settings interface"""
+
+
+class ISearchResultsPortletPanelsRendererSettings(ISearchResultsPortletRendererBaseSettings):
+    """Search results portlet panels renderer settings interface"""
+
+    button_title = I18nTextLineField(title=_("Button's title"),
+                                     description=_("Navigation button's title is normally defined "
+                                                   "based on target's content type; you can "
+                                                   "override this label by giving a custom title "
+                                                   "here"),
+                                     required=False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/features/search/portlet/templates/search-panel.pt	Wed Sep 25 10:05:57 2019 +0200
@@ -0,0 +1,33 @@
+<tal:var define="target view.url;
+				 renderer_settings view.view.renderer_settings;"
+		 i18n:domain="pyams_default_theme">
+	<div class="thumbnail hidden-xs"
+		 tal:define="illustration tales:pyams_illustration(context)"
+		 tal:condition="illustration">
+		<a href="${target}">
+			<tal:if define="image i18n:illustration.data;
+							alt i18n:illustration.alt_title;"
+					condition="image">
+				${structure:tales:picture(image, lg_thumb='pano', lg_width=3, md_thumb='pano', md_width=3, sm_thumb='pano',
+										  sm_width=4, xs_thumb='pano', xs_width=0, alt=alt, css_class='result_media')}
+			</tal:if>
+		</a>
+	</div>
+	<div tal:define="button_title i18n:renderer_settings.button_title">
+		<a href="${target}">
+			<h3>${view.title}</h3>
+		</a>
+		<div class="header"
+			 tal:define="header view.header"
+			 tal:condition="header">
+			${structure:tales:html(header)}
+		</div>
+		<div class="action"
+			 tal:condition="button_title">
+			<a class="btn btn-default"
+			   href="${target}">
+				<span i18n:translate="">${button_title}</span>
+			</a>
+		</div>
+	</div>
+</tal:var>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/features/search/portlet/templates/search-panels.pt	Wed Sep 25 10:05:57 2019 +0200
@@ -0,0 +1,105 @@
+<div tal:define="settings view.settings;
+				 renderer_settings view.renderer_settings;
+				 start request.params.get('start', 0);
+				 length request.params.get('length', 9);"
+	 tal:condition="settings.allow_empty_query or settings.has_user_query(request)"
+	 i18n:domain="pyams_default_theme">
+	<h2>${i18n:settings.title}</h2>
+	<form action="${tales:absolute_url(context)}"
+		  id="search-results">
+		<input type="hidden" name="user_search" value="${request.params.get('user_search')}" />
+		<input type="hidden" name="tag" value="${request.params.get('tag')}" />
+		<input type="hidden" name="order_by" value="${request.params.get('order_by') or context.order_by}" />
+		<input type="hidden" name="start" value="${start}" />
+		<input type="hidden" name="length" value="${length}" />
+	</form>
+	<div class="search-results"
+		 tal:define="(items, count) settings.get_items(request, start, length)">
+		<header>
+			<div tal:condition="renderer_settings.display_results_count">
+				<tal:if condition="count" i18n:translate="">
+					<i18n:var name="count">${count}</i18n:var> result(s) found
+				</tal:if>
+				<tal:if condition="not:count" i18n:translate="">
+					No result found!
+				</tal:if>
+			</div>
+			<div tal:condition="count and renderer_settings.allow_sorting">
+				<select class="form-control"
+						data-ams-change-handler="PyAMS_default.search.updateSort"
+						tal:define="order_by request.params.get('order_by') or context.order_by">
+					<option value="relevance"
+							selected="${'selected' if order_by == 'relevance' else None}"
+							i18n:translate="">Sort by relevance</option>
+					<option value="visible_publication_date"
+							selected="${'selected' if order_by == 'visible_publication_date' else None}"
+							i18n:translate="">Sort by publication date</option>
+					<option value="modified_date"
+							selected="${'selected' if order_by == 'modified_date' else None}"
+							i18n:translate="">Sort by last modification date</option>
+				</select>
+			</div>
+			<div tal:condition="count and renderer_settings.allow_pagination">
+				<span i18n:translate="">Page length:</span>&nbsp;
+				<select class="form-control"
+						data-ams-change-handler="PyAMS_default.search.updatePageLength"
+						tal:define="length request.params.get('length', 9)">
+					<option tal:repeat="value ('9', '21', '45')"
+							value="${value}"
+							selected="${'selected' if value == length else None}"
+							i18n:translate="">${value}</option>
+				</select>
+			</div>
+		</header>
+		<hr />
+		<div class="summary">
+			<tal:loop repeat="item items">
+				<div class="result col-md-4 col-sm-6">
+					${structure:view.render_item(item)}
+				</div>
+				<div class="clearfix visible-md-block visible-lg-block"
+					 tal:condition="not:repeat['item'].number % 3"></div>
+				<div class="clearfix visible-sm-block"
+					 tal:condition="not:repeat['item'].number % 2"></div>
+			</tal:loop>
+		</div>
+		<div class="clearfix"></div>
+		<div class="col-md-12 text-center">
+			<nav role="navigation"
+				 aria-label="Pagination" i18n:attributes="aria-label">
+				<ol class="pagination"
+					tal:define="(current, total) settings.get_pages(start, length, count)"
+					data-ams-current-page="${current}">
+					<tal:if condition="renderer_settings.allow_pagination and (total > 1)">
+						<tal:if condition="current > 1">
+							<li class="prev">
+								<a href="#" i18n:translate=""
+								   data-ams-click-handler="PyAMS_default.search.previousPage">Previous page</a>
+							</li>
+						</tal:if>
+						<li tal:repeat="page range(current)">
+							<a tal:condition="current != page+1"
+							   href="#"
+							   data-ams-click-handler="PyAMS_default.search.gotoPage">${page+1}</a>
+							<span tal:condition="current == page+1"
+								  class="current">${page+1}</span>
+						</li>
+						<tal:if condition="current < total">
+							<li tal:condition="current < total-1">
+								<a class="disabled">…</a>
+							</li>
+							<li>
+								<a href="#"
+								   data-ams-click-handler="PyAMS_default.search.gotoPage">${total}</a>
+							</li>
+							<li class="next">
+								<a href="#" i18n:translate=""
+								   data-ams-click-handler="PyAMS_default.search.nextPage">Next page</a>
+							</li>
+						</tal:if>
+					</tal:if>
+				</ol>
+			</nav>
+		</div>
+	</div>
+</div>
--- a/src/pyams_default_theme/features/search/portlet/templates/search-result.pt	Tue Sep 17 12:00:26 2019 +0200
+++ b/src/pyams_default_theme/features/search/portlet/templates/search-result.pt	Wed Sep 25 10:05:57 2019 +0200
@@ -23,4 +23,4 @@
 			${structure:tales:html(header)}
 		</div>
 	</div>
-</tal:var>
+</tal:var>
\ No newline at end of file
--- a/src/pyams_default_theme/features/search/portlet/templates/search-results.pt	Tue Sep 17 12:00:26 2019 +0200
+++ b/src/pyams_default_theme/features/search/portlet/templates/search-results.pt	Wed Sep 25 10:05:57 2019 +0200
@@ -1,5 +1,7 @@
 <div tal:define="settings view.settings;
-				 renderer_settings view.renderer_settings;"
+				 renderer_settings view.renderer_settings;
+				 start request.params.get('start', 0);
+				 length request.params.get('length', 10);"
 	 tal:condition="settings.allow_empty_query or settings.has_user_query(request)"
 	 i18n:domain="pyams_default_theme">
 	<h2>${i18n:settings.title}</h2>
@@ -8,11 +10,11 @@
 		<input type="hidden" name="user_search" value="${request.params.get('user_search')}" />
 		<input type="hidden" name="tag" value="${request.params.get('tag')}" />
 		<input type="hidden" name="order_by" value="${request.params.get('order_by') or context.order_by}" />
-		<input type="hidden" name="start" value="${request.params.get('start', 0)}" />
-		<input type="hidden" name="length" value="${request.params.get('length', 10)}" />
+		<input type="hidden" name="start" value="${start}" />
+		<input type="hidden" name="length" value="${length}" />
 	</form>
 	<div class="search-results"
-		 tal:define="(items, count) settings.get_items(request)">
+		 tal:define="(items, count) settings.get_items(request, start, length)">
 		<header>
 			<div tal:condition="renderer_settings.display_results_count">
 				<tal:if condition="count" i18n:translate="">
@@ -59,7 +61,7 @@
 			<nav role="navigation"
 				 aria-label="Pagination" i18n:attributes="aria-label">
 				<ol class="pagination"
-					tal:define="(current, total) settings.get_pages(request, count)"
+					tal:define="(current, total) settings.get_pages(start, length, count)"
 					data-ams-current-page="${current}">
 					<tal:if condition="renderer_settings.allow_pagination and (total > 1)">
 						<tal:if condition="current > 1">
--- a/src/pyams_default_theme/interfaces.py	Tue Sep 17 12:00:26 2019 +0200
+++ b/src/pyams_default_theme/interfaces.py	Wed Sep 25 10:05:57 2019 +0200
@@ -57,3 +57,7 @@
 
 class ISearchResultsView(Interface):
     """Search results view marker interface"""
+
+
+class ISearchResultsPanelView(ISearchResultsView):
+    """Search results panel view marker interface"""