Updated views and view content portlet to use iterators and allow merging of several views
--- a/src/pyams_content/shared/view/__init__.py Mon Jul 02 17:10:37 2018 +0200
+++ b/src/pyams_content/shared/view/__init__.py Mon Jul 02 17:11:41 2018 +0200
@@ -36,7 +36,7 @@
from pyams_catalog.query import CatalogResultSet, or_
from pyams_content.shared.common import WfSharedContent, register_content_type, SharedContent, IWfSharedContentFactory
from pyams_utils.adapter import adapter_config, ContextAdapter
-from pyams_utils.list import unique
+from pyams_utils.list import unique_iter
from pyams_utils.registry import get_utility, get_global_registry
from pyams_utils.timezone import tztime
from pyams_workflow.interfaces import IWorkflow
@@ -86,7 +86,7 @@
content_types |= set(self.selected_content_types)
return list(content_types)
- def get_results(self, context):
+ def get_results(self, context, sort_index=None, reverse=None, limit=None):
views_cache = get_cache(VIEWS_CACHE_REGION, VIEWS_CACHE_NAME)
if self.is_using_context:
cache_key = VIEW_CONTEXT_CACHE_KEY.format(view=ICacheKeyValue(self),
@@ -100,7 +100,10 @@
adapter = registry.queryAdapter(self, IViewQuery, name='es')
if adapter is None:
adapter = registry.getAdapter(self, IViewQuery)
- results = adapter.get_results(context, self.limit)
+ results = adapter.get_results(context,
+ sort_index or self.order_by,
+ reverse if reverse is not None else self.reversed_order,
+ limit or self.limit)
intids = get_utility(IIntIds)
views_cache.set_value(cache_key, [intids.queryId(item) for item in results])
logger.debug("Storing view items to cache key {0}".format(cache_key))
@@ -152,19 +155,19 @@
params &= new_params
return params
- def get_results(self, context, limit=None):
+ def get_results(self, context, sort_index, reverse, limit):
view = self.context
catalog = get_utility(ICatalog)
registry = get_current_registry()
params = self.get_params(context)
items = CatalogResultSet(CatalogQuery(catalog).query(params,
- sort_index=view.order_by,
- reverse=view.reversed_order,
+ sort_index=sort_index,
+ reverse=reverse,
limit=limit))
for name, adapter in sorted(registry.getAdapters((view,), IViewQueryFilterExtension),
key=lambda x: x[1].weight):
items = adapter.filter(context, items)
- return unique(items)
+ return unique_iter(items)
@subscriber(IObjectModifiedEvent, context_selector=IWfView)
--- a/src/pyams_content/shared/view/interfaces/__init__.py Mon Jul 02 17:10:37 2018 +0200
+++ b/src/pyams_content/shared/view/interfaces/__init__.py Mon Jul 02 17:11:41 2018 +0200
@@ -37,13 +37,15 @@
PUBLICATION_DATE_ORDER = 'publication_date'
FIRSTPUBLICATION_DATE_ORDER = 'first_publication_date'
-VIEW_ORDER = {CREATION_DATE_ORDER: _("Creation date"),
- UPDATE_DATE_ORDER: _("Last update date"),
- PUBLICATION_DATE_ORDER: _("Current publication date"),
- FIRSTPUBLICATION_DATE_ORDER: _("First publication date")}
+VIEW_ORDERS = (
+ {'id': CREATION_DATE_ORDER, 'title': _("Creation date")},
+ {'id': UPDATE_DATE_ORDER,'title': _("Last update date")},
+ {'id': PUBLICATION_DATE_ORDER, 'title': _("Current publication date")},
+ {'id': FIRSTPUBLICATION_DATE_ORDER, 'title': _("First publication date")}
+)
-VIEW_ORDER_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t)
- for v, t in VIEW_ORDER.items()])
+VIEW_ORDER_VOCABULARY = SimpleVocabulary([SimpleTerm(item['id'], title=item['title'])
+ for item in VIEW_ORDERS])
class IViewsManager(ISharedTool):
@@ -87,7 +89,7 @@
is_using_context = Attribute("Check if view is using context settings")
- def get_results(self, context):
+ def get_results(self, context, sort_index=None, reverse=True, limit=None):
"""Get results of catalog query"""
@@ -108,7 +110,7 @@
class IViewQuery(Interface):
"""View query interface"""
- def get_results(self, context, limit=None):
+ def get_results(self, context, sort_index, reverse, limit):
"""Get results of catalog query"""
@@ -171,7 +173,7 @@
class IViewThemesSettings(IViewSettings):
- """View themess ettings"""
+ """View themes ettings"""
select_context_themes = Bool(title=_("Select context themes?"),
description=_("If 'yes', themes will be extracted from context"),
@@ -186,3 +188,16 @@
def get_themes_index(self, context):
"""Get all themes index values for given context"""
+
+
+VIEWS_MERGERS_VOCABULARY = 'pyams_content.views.mergers'
+
+
+class IViewsMerger(Interface):
+ """Interface used to define views mergers
+
+ Mergers are used to merge results of several views.
+ """
+
+ def get_results(self, views, context):
+ """Merge results of several views together"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/view/merge.py Mon Jul 02 17:11:41 2018 +0200
@@ -0,0 +1,135 @@
+#
+# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from heapq import merge
+from random import shuffle
+from itertools import chain, zip_longest
+
+# import interfaces
+from pyams_content.shared.view.interfaces import VIEWS_MERGERS_VOCABULARY, IViewsMerger, CREATION_DATE_ORDER, \
+ UPDATE_DATE_ORDER, PUBLICATION_DATE_ORDER, FIRSTPUBLICATION_DATE_ORDER
+
+# import packages
+from pyams_utils.registry import utility_config
+from pyams_utils.request import check_request
+from pyams_utils.vocabulary import vocabulary_config
+from zope.componentvocabulary.vocabulary import UtilityVocabulary, UtilityTerm
+
+from pyams_content import _
+
+
+@vocabulary_config(name=VIEWS_MERGERS_VOCABULARY)
+class ViewsMergersVocabulary(UtilityVocabulary):
+ """Views mergers vocabulary"""
+
+ interface = IViewsMerger
+ nameOnly = True
+
+ def __init__(self, context, **kw):
+ request = check_request()
+ registry = request.registry
+ translate = request.localizer.translate
+ utils = [(name, translate(util.label))
+ for (name, util) in registry.getUtilitiesFor(self.interface)]
+ self._terms = dict((title, UtilityTerm(name, title)) for name, title in utils)
+
+
+CONCAT_VIEWS_MERGE_MODE = 'concat'
+
+
+@utility_config(name=CONCAT_VIEWS_MERGE_MODE, provides=IViewsMerger)
+class ViewsConcatenateMergeMode(object):
+ """Views concatenate merge mode"""
+
+ label = _("Concatenate views items in order")
+
+ @classmethod
+ def get_results(cls, views, context):
+ results = (view.get_results(context) for view in views)
+ yield from chain(*results)
+
+
+@utility_config(name='zip', provides=IViewsMerger)
+class ViewsZipMergeMode(object):
+ """Views zip merge mode"""
+
+ label = _("Take items from views one by one, in views order")
+
+ @classmethod
+ def get_results(cls, views, context):
+ results = (view.get_results(context) for view in views)
+ for array in zip_longest(*results):
+ yield from filter(lambda x: x is not None, array)
+
+
+@utility_config(name='zip_random', provides=IViewsMerger)
+class ViewsRandomZipMergeMode(object):
+ """Views random zip merge mode"""
+
+ label = _("Take items from views one by one, in random order")
+
+ @classmethod
+ def get_results(cls, views, context):
+ results = [view.get_results(context) for view in views]
+ shuffle(results)
+ for array in zip_longest(*results):
+ yield from filter(lambda x: x is not None, array)
+
+
+class SortedMergeMode(object):
+ """Sorted merge mode base class"""
+
+ sort_index = None
+
+ @classmethod
+ def get_results(cls, views, context):
+ results = (view.get_results(context,
+ sort_index=cls.sort_index,
+ reverse=True)
+ for view in views)
+ yield from merge(*results)
+
+
+@utility_config(name='{0}.sort'.format(CREATION_DATE_ORDER), provides=IViewsMerger)
+class CreationDateSortedMergeMode(SortedMergeMode):
+ """Merge pre-sorted views by creation date"""
+
+ label = _("Sort all results by creation date")
+ sort_index = CREATION_DATE_ORDER
+
+
+@utility_config(name='{0}.sort'.format(UPDATE_DATE_ORDER), provides=IViewsMerger)
+class UpdateDateSortedMergeMode(SortedMergeMode):
+ """Merge pre-sorted views by last update date"""
+
+ label = _("Sort all results by last update date")
+ sort_index = UPDATE_DATE_ORDER
+
+
+@utility_config(name='{0}.sort'.format(PUBLICATION_DATE_ORDER), provides=IViewsMerger)
+class PublicationDateSortedMergeMode(SortedMergeMode):
+ """Merge pre-sorted views by publication date"""
+
+ label = _("Sort all results by current publication date")
+ sort_index = PUBLICATION_DATE_ORDER
+
+
+@utility_config(name='{0}.sort'.format(FIRSTPUBLICATION_DATE_ORDER), provides=IViewsMerger)
+class FirstPublicationDateSortedMergeMode(SortedMergeMode):
+ """Merge pre-sorted views by first publication date"""
+
+ label = _("Sort all results by first publication date")
+ sort_index = FIRSTPUBLICATION_DATE_ORDER
--- a/src/pyams_content/shared/view/portlet/__init__.py Mon Jul 02 17:10:37 2018 +0200
+++ b/src/pyams_content/shared/view/portlet/__init__.py Mon Jul 02 17:11:41 2018 +0200
@@ -14,16 +14,21 @@
# import standard library
+from itertools import islice
# import interfaces
-from pyams_content.shared.view.portlet.interfaces import IViewItemsPortletSettings
+from pyams_content.shared.view.interfaces import IViewsMerger
+from pyams_content.shared.view.portlet.interfaces import IViewItemsPortletSettings, VIEW_DISPLAY_CONTEXT
from pyams_utils.interfaces import VIEW_PERMISSION
+from pyams_utils.interfaces.url import DISPLAY_CONTEXT
# import packages
from pyams_content.workflow import PUBLISHED_STATES
from pyams_portal.portlet import PortletSettings, portlet_config, Portlet
from pyams_sequence.utility import get_sequence_target
from pyams_utils.factory import factory_config
+from pyams_utils.list import unique_iter
+from pyams_utils.request import check_request
from zope.interface import implementer
from zope.schema.fieldproperty import FieldProperty
@@ -38,16 +43,37 @@
class ViewItemsPortletSettings(PortletSettings):
"""View items portlet settings"""
- view = FieldProperty(IViewItemsPortletSettings['view'])
+ views = FieldProperty(IViewItemsPortletSettings['views'])
+ views_context = FieldProperty(IViewItemsPortletSettings['views_context'])
+ views_merge_mode = FieldProperty(IViewItemsPortletSettings['views_merge_mode'])
+ limit = FieldProperty(IViewItemsPortletSettings['limit'])
+
+ def get_merger(self, request=None):
+ if request is None:
+ request = check_request()
+ return request.registry.queryUtility(IViewsMerger, name=self.views_merge_mode)
+
+ def get_views(self):
+ for oid in self.views or ():
+ view = get_sequence_target(oid, state=PUBLISHED_STATES)
+ if view is not None:
+ yield view
- def get_view(self):
- if self.view is not None:
- return get_sequence_target(self.view, state=PUBLISHED_STATES)
-
- def get_items(self, context):
- view = self.get_view()
- if view is not None:
- return view.get_results(context)
+ def get_items(self, request=None):
+ if request is None:
+ request = check_request()
+ if self.views_context == VIEW_DISPLAY_CONTEXT:
+ context = request.annotations.get(DISPLAY_CONTEXT, request.root)
+ else:
+ context = request.context
+ if len(self.views or ()) < 2: # single view
+ for view in self.get_views():
+ if view is not None:
+ yield from islice(view.get_results(context), self.limit)
+ else: # several views merged together
+ merger = self.get_merger(request)
+ if merger is not None:
+ yield from islice(unique_iter(merger.get_results(self.get_views(), context)), self.limit)
@portlet_config(permission=VIEW_PERMISSION)
--- a/src/pyams_content/shared/view/portlet/interfaces.py Mon Jul 02 17:10:37 2018 +0200
+++ b/src/pyams_content/shared/view/portlet/interfaces.py Mon Jul 02 17:11:41 2018 +0200
@@ -16,19 +16,68 @@
# import standard library
# import interfaces
+from pyams_content.shared.view.interfaces import VIEWS_MERGERS_VOCABULARY
from pyams_portal.interfaces import IPortletSettings
# import packages
from pyams_content.shared.view import WfView
-from pyams_sequence.schema import InternalReference
+from pyams_content.shared.view.merge import CONCAT_VIEWS_MERGE_MODE
+from pyams_sequence.schema import InternalReferencesList
+from zope.schema import Choice, Int
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
from pyams_content import _
+#
+# Views display contexts
+#
+
+VIEW_DISPLAY_CONTEXT = 'display'
+VIEW_CONTENT_CONTEXT = 'content'
+
+VIEW_CONTEXTS = (
+ {'id': VIEW_DISPLAY_CONTEXT, 'title': _("Display context")},
+ {'id': VIEW_CONTENT_CONTEXT, 'title': _("Content context")}
+)
+
+VIEW_CONTEXT_VOCABULARY = SimpleVocabulary([SimpleTerm(item['id'], title=item['title'])
+ for item in VIEW_CONTEXTS])
+
+
+#
+# Views merge modes
+#
+
class IViewItemsPortletSettings(IPortletSettings):
"""View items portlet settings interface"""
- view = InternalReference(title=_("Selected view"),
- description=_("Reference to the view from which items are extracted"),
- content_type=WfView.content_type,
- required=True)
+ views = InternalReferencesList(title=_("Selected views"),
+ description=_("Reference to the view(s) from which items are extracted; "
+ "you can combine several views together and specify in which order "
+ "they should be mixed"),
+ content_type=WfView.content_type,
+ required=True)
+
+ views_context = Choice(title=_("Views context"),
+ description=_("When searching for items, a view receives a \"context\" which is the object "
+ "from which settings can be extracted; this context can be the \"display\" "
+ "context or the \"content\" context: when the portlet is used to display the "
+ "site root, a site manager or a site folder, both are identical; when the "
+ "portlet is used to display a shared content, the \"content\" context is the "
+ "displayed content, while the \"display\" context is the container (site "
+ "root, site manager or site folder) into which content is displayed"),
+ vocabulary=VIEW_CONTEXT_VOCABULARY,
+ default=VIEW_DISPLAY_CONTEXT,
+ required=True)
+
+ views_merge_mode = Choice(title=_("Views merge mode"),
+ description=_("If you select several views, you can select \"merge\" mode, which is "
+ "the way used to merge items from several views"),
+ vocabulary=VIEWS_MERGERS_VOCABULARY,
+ default=CONCAT_VIEWS_MERGE_MODE,
+ required=True)
+
+ limit = Int(title=_("Results count limit"),
+ description=_("Maximum number of results that the component may extract from merged views"),
+ required=False)
--- a/src/pyams_content/shared/view/portlet/zmi/templates/view-items-list-preview.pt Mon Jul 02 17:10:37 2018 +0200
+++ b/src/pyams_content/shared/view/portlet/zmi/templates/view-items-list-preview.pt Mon Jul 02 17:11:41 2018 +0200
@@ -1,12 +1,9 @@
-<tal:var define="settings view.settings">
- <tal:var define="items settings.get_items(context)">
- <tal:if condition="items">
- <div tal:repeat="item items">
- <tal:var content="i18n:item.title" />
- </div>
- </tal:if>
- <tal:if condition="not:items" i18n:translate="">
- No result found
- </tal:if>
- </tal:var>
+<tal:var define="settings view.settings; global count 0;" i18n:domain="pyams_content">
+ <div tal:repeat="item settings.get_items(request)">
+ <tal:var content="i18n:item.title" />
+ <tal:var define="global count count+1" />
+ </div>
+ <tal:if condition="not:count" i18n:translate="">
+ No result found
+ </tal:if>
</tal:var>
--- a/src/pyams_content/shared/view/reference.py Mon Jul 02 17:10:37 2018 +0200
+++ b/src/pyams_content/shared/view/reference.py Mon Jul 02 17:11:41 2018 +0200
@@ -9,7 +9,6 @@
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
-from pyams_sequence.interfaces import ISequentialIdInfo
__docformat__ = 'restructuredtext'
@@ -21,6 +20,7 @@
from hypatia.interfaces import ICatalog
from pyams_content.shared.view.interfaces import IWfView, IViewSettings, IViewInternalReferencesSettings, \
IViewQueryParamsExtension, IViewQueryFilterExtension, VIEW_REFERENCES_SETTINGS_KEY, ALWAYS_REFERENCE_MODE
+from pyams_sequence.interfaces import ISequentialIdInfo
# import packages
from hypatia.catalog import CatalogQuery
@@ -62,13 +62,15 @@
weight = 50
def get_params(self, context):
- catalog = get_utility(ICatalog)
settings = IViewInternalReferencesSettings(self.context)
params = None
# check themes
if settings.exclude_context:
- oid = ISequentialIdInfo(context).hex_oid
- params = and_(params, NotEq(catalog['oid'], oid))
+ sequence = ISequentialIdInfo(context, None)
+ if sequence is not None:
+ oid = sequence.hex_oid
+ catalog = get_utility(ICatalog)
+ params = and_(params, NotEq(catalog['oid'], oid))
return params