Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ local utility providing IViewletSettingsStorage (CMFPlone does this). The
viewlet manager in manager.OrderedViewletManager can then get the filter and
order settings. These settings can be configured by 3rd party products and
TTW to customize the viewlets per skin.

Additional viewlets
===================

A manager renders more than the viewlets registered for it. Subscription
adapters for ``(context, request, view, manager)`` providing
``interfaces.IAdditionalViewlets`` contribute ``(name, viewlet)`` pairs through
their ``viewlets()`` method. This lets an add-on decide at runtime which
manager shows a viewlet, for example from configuration stored in the
database. Contributed viewlets are ordered, hidden and managed like registered
ones. A registered viewlet wins over a contributed one of the same name.
2 changes: 2 additions & 0 deletions news/+additional-viewlets.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``IAdditionalViewlets`` subscribers to contribute viewlets to ordered viewlet managers they are not registered for.
[MrTango]
10 changes: 10 additions & 0 deletions src/plone/app/viewletmanager/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,15 @@ def setHidden(name, skinname, hidden):
given viewletmanager (name) and skin name."""


class IAdditionalViewlets(Interface):
"""Contributes viewlets to a manager they are not registered for.

Looked up as subscription adapters for (context, request, view, manager).
"""

def viewlets():
"""Return an iterable of (name, viewlet) pairs."""


class IViewletManagementView(Interface):
pass
64 changes: 58 additions & 6 deletions src/plone/app/viewletmanager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from Acquisition.interfaces import IAcquirer
from logging import getLogger
from operator import itemgetter
from plone.app.viewletmanager.interfaces import IAdditionalViewlets
from plone.app.viewletmanager.interfaces import IViewletManagementView
from plone.app.viewletmanager.interfaces import IViewletSettingsStorage
from Products.Five import BrowserView
Expand All @@ -15,9 +16,13 @@
from zope.component import getUtility
from zope.component import queryMultiAdapter
from zope.component import queryUtility
from zope.component import subscribers
from zope.contentprovider.interfaces import IContentProvider
from zope.interface import implementer
from zope.interface import providedBy
from zope.interface.interfaces import ComponentLookupError
from zope.location.interfaces import ILocation
from zope.security.interfaces import Unauthorized
from zope.viewlet.interfaces import IViewlet
from ZPublisher import Retry

Expand All @@ -30,6 +35,51 @@ class BaseOrderedViewletManager:
# See https://github.com/plone/plone.app.viewletmanager/issues/5
_exceptions_handled_elsewhere = (ConflictError, KeyboardInterrupt, Retry)

def _additional_viewlets(self):
required = (self.context, self.request, self.__parent__, self)
for source in subscribers(required, IAdditionalViewlets):
yield from source.viewlets()

def available_viewlets(self):
"""All (name, viewlet) pairs of this manager, before filtering.

Registered viewlets win over contributed ones of the same name.
"""
viewlets = dict(
getAdapters(
(self.context, self.request, self.__parent__, self), IViewlet
)
)
for name, viewlet in self._additional_viewlets():
viewlets.setdefault(name, viewlet)
return list(viewlets.items())

def update(self):
# zope.viewlet's update() with available_viewlets() as the source.
self._ViewletManagerBase__updated = True
viewlets = self.sort(self.filter(self.available_viewlets()))
self.viewlets = []
for name, viewlet in viewlets:
if ILocation.providedBy(viewlet):
viewlet.__name__ = name
self.viewlets.append(viewlet)
self._updateViewlets()

def __getitem__(self, name):
try:
return super().__getitem__(name)
except ComponentLookupError:
pass
for viewlet_name, viewlet in self._additional_viewlets():
if viewlet_name != name:
continue
if not guarded_hasattr(viewlet, "render"):
raise Unauthorized(
f"You are not authorized to access the provider called `{name}`."
)
return viewlet
raise ComponentLookupError(f"No provider with name `{name}` found.")

def filter(self, viewlets):
"""Filter the viewlets.

Expand Down Expand Up @@ -131,9 +181,7 @@ def render(self):

if is_managing:
# if we are in the managing view, then fetch all viewlets again
viewlets = getAdapters(
(self.context, self.request, self.__parent__, self), IViewlet
)
viewlets = self.available_viewlets()

# sort them first
viewlets = self.sort(viewlets)
Expand Down Expand Up @@ -211,9 +259,13 @@ def _getOrder(self, manager_name):
manager = queryMultiAdapter(
(self.context, self.request, self), IContentProvider, manager_name
)
viewlets = getAdapters(
(manager.context, manager.request, manager.__parent__, manager), IViewlet
)
if isinstance(manager, BaseOrderedViewletManager):
viewlets = manager.available_viewlets()
else:
viewlets = getAdapters(
(manager.context, manager.request, manager.__parent__, manager),
IViewlet,
)
order_by_name = storage.getOrder(manager_name, skinname)
# first get the known ones
name_map = dict(viewlets)
Expand Down
75 changes: 75 additions & 0 deletions src/plone/app/viewletmanager/tests/manager.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,78 @@ We can also hide viewlets.
>>> manager.update()
>>> manager.render()
'third\nsecond'

Additional viewlets
-------------------

A manager also renders viewlets that are not registered for it. Any
subscription adapter for ``(context, request, view, manager)`` providing
``IAdditionalViewlets`` can contribute ``(name, viewlet)`` pairs, for example
viewlets whose manager assignment is kept in the database instead of ZCML.

>>> storage.setHidden('left', 'Plone Default', ())
>>> storage.setOrder('left', 'Plone Default', ())

>>> class FourthViewlet(BaseViewlet):
... name = u"fourth"

>>> from plone.app.viewletmanager.interfaces import IAdditionalViewlets
>>> from zope.component import provideSubscriptionAdapter
>>> @implementer(IAdditionalViewlets)
... class ExtraViewlets:
...
... def __init__(self, context, request, view, manager):
... self.required = (context, request, view, manager)
...
... def viewlets(self):
... yield 'fourth', FourthViewlet(*self.required)
... yield 'second', FourthViewlet(*self.required)

>>> provideSubscriptionAdapter(
... ExtraViewlets,
... (Interface, IDefaultBrowserLayer, IBrowserView, ILeftColumn),
... IAdditionalViewlets)

The contributed viewlet renders along with the registered ones. A name that is
already registered for the manager keeps its registered viewlet.

>>> manager.update()
>>> manager.render()
'first\nfourth\nsecond\nthird'

Order and visibility come from the storage, exactly as for registered
viewlets.

>>> storage.setOrder('left', 'Plone Default',
... ('third', 'fourth', 'first', 'second'))
>>> manager.update()
>>> manager.render()
'third\nfourth\nfirst\nsecond'

>>> storage.setHidden('left', 'Plone Default', ('fourth',))
>>> manager.update()
>>> manager.render()
'third\nfirst\nsecond'

>>> storage.setHidden('left', 'Plone Default', ())

The manager hands out a contributed viewlet by name, too.

>>> manager['fourth'].render()
'fourth'
>>> manager.get('missing') is None
True

The management view moves contributed viewlets like any other.

>>> from zope.contentprovider.interfaces import IContentProvider
>>> provideAdapter(
... LeftColumn,
... (Interface, IDefaultBrowserLayer, IBrowserView),
... IContentProvider, name='left')

>>> from plone.app.viewletmanager.manager import ManageViewlets
>>> manage = ManageViewlets(content, request)
>>> manage.moveBelow('left', 'fourth', 'second')
>>> storage.getOrder('left', 'Plone Default')
('third', 'first', 'second', 'fourth')
Loading