# HG changeset patch # User Thierry Florac # Date 1530544301 -7200 # Node ID e34cc04e9786b9ed9c9f0dc53af8fb7a8a247c68 # Parent bfdfb25e2f9e771a0132b8915f1329119b22492b Updated views and view content portlet to use iterators and allow merging of several views diff -r bfdfb25e2f9e -r e34cc04e9786 src/pyams_content/shared/view/__init__.py --- 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) diff -r bfdfb25e2f9e -r e34cc04e9786 src/pyams_content/shared/view/interfaces/__init__.py --- 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""" diff -r bfdfb25e2f9e -r e34cc04e9786 src/pyams_content/shared/view/merge.py --- /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 +# 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 diff -r bfdfb25e2f9e -r e34cc04e9786 src/pyams_content/shared/view/portlet/__init__.py --- 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) diff -r bfdfb25e2f9e -r e34cc04e9786 src/pyams_content/shared/view/portlet/interfaces.py --- 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) diff -r bfdfb25e2f9e -r e34cc04e9786 src/pyams_content/shared/view/portlet/zmi/templates/view-items-list-preview.pt --- 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 @@ - - - -
- -
-
- - No result found - -
+ +
+ + +
+ + No result found +
diff -r bfdfb25e2f9e -r e34cc04e9786 src/pyams_content/shared/view/reference.py --- 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