An introduction to Plone portlets
Under the hood of the Plone 3 portlets machinery
Plone 3 shipped with a powerful portlets machinery that superseded the simple template-only approach of earlier versions. Plone portlets are often perceived as quite complex. That's probably true. This complexity comes from trying to come up with an approach that's flexible enough to meet future UI needs, and pluggable enough to support customisation without having to throw away the whole framework. Unfortunately, not many people have a good understanding of how the machinery works. Hopefully, this article will help.
Why do we need this type of portlet at all?
Prior to Plone 3, a portlet was just a template with a special macro that was looked up in a ZMI property. This was very simple, but it made it hard to write portlets with any kind of persistent configuration (there was no standard place and no UI to edit portlets). There were few re-usable portlets that were distributed as add-on products. There was also not much in the way of a programming model to introspect and manipulate portlets.
WHEre did this come from?
The plone.portlets package took its inspiration firstly from zope.viewlet. In many ways, a portlet is a viewlet with persistent configuration and persistent, run-time-changeable assignment of "viewlets" (portlets) to "viewlet managers" (portlet managers). It is also based loosely on the Java "JSR 168" standard for portlets.
WHAT Is a portlet?
A "portlet" consists of:
- A schema interface (optional, but almost always used). By convention, this derives from IPortletDataProvider, which is a marker interface.
- A persistent "content" class for the portlet Assignment. The Assignment stores the persistent configuration data (if any) of the portlet. Even when a portlet is not configurable, it needs to have an Assignment class, because the presence of an Assignment instance in various places is what determines what portlets show up where. The Assignment class will implement the portlet's schema interface.
- An add form. This typically uses zope.formlib and the portlet schema. If the portlet is not configurable, this can use a special "NullAddForm", which is just a view that creates the portlet and then redirects back to the portlet management screen.
- An edit form, which is similar. It can be omitted if the portlet is not editable, of course.
- A "view". The "view" is called a Portlet Renderer. This is just a content provider (in the zope.contentprovider sense), in that it has an update() and a render() method.
Here is an example:
from zope import schema from zope.component import getMultiAdapter from zope.formlib import form from zope.interface import implements from plone.app.portlets.portlets import base from plone.memoize.instance import memoize from plone.portlets.interfaces import IPortletDataProvider from plone.app.portlets.cache import render_cachekey from Acquisition import aq_inner from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.CMFPlone import PloneMessageFactory as _ class IRecentPortlet(IPortletDataProvider): count = schema.Int(title=_(u'Number of items to display'), description=_(u'How many items to list.'), required=True, default=5) class Assignment(base.Assignment): implements(IRecentPortlet) def __init__(self, count=5): self.count = count @property def title(self): return _(u"Recent items") class Renderer(base.Renderer): _template = ViewPageTemplateFile('recent.pt') def __init__(self, *args): base.Renderer.__init__(self, *args) context = aq_inner(self.context) portal_state = getMultiAdapter((context, self.request), name=u'plone_portal_state') self.anonymous = portal_state.anonymous() self.portal_url = portal_state.portal_url() self.typesToShow = portal_state.friendly_types() plone_tools = getMultiAdapter((context, self.request), name=u'plone_tools') self.catalog = plone_tools.catalog() def render(self): return xhtml_compress(self._template()) @property def available(self): return not self.anonymous and len(self._data()) def recent_items(self): return self._data() def recently_modified_link(self): return '%s/recently_modified' % self.portal_url @memoize def _data(self): limit = self.data.count return self.catalog(portal_type=self.typesToShow, sort_on='modified', sort_order='reverse', sort_limit=limit)[:limit] class AddForm(base.AddForm): form_fields = form.Fields(IRecentPortlet) label = _(u"Add Recent Portlet") description = _(u"This portlet displays recently modified content.") def create(self, data): return Assignment(count=data.get('count', 5)) class EditForm(base.EditForm): form_fields = form.Fields(IRecentPortlet) label = _(u"Edit Recent Portlet") description = _(u"This portlet displays recently modified content.")
Things to note include:
- We have base classes for the assignment, renderer, add view and edit view. These just provide "Ploneish" policy.
- The Assignment class has a 'title' attribute that is used in the portlet management UI to distinguish different instances of the portlet.
- The Renderer has an "update" and a "render" method. Here, we use the default (empty) update() method from the base class.
- The Renderer uses a page template to render itself.
- The Renderer has an available property, which is used to determine whether this portlet should be shown or not.
The Renderer is a multi-adapter that takes a number of parameters:
- context - the current content object
- request - the current request
- view - the current (full page) view
- manager - the portlet manager where this portlet was rendered (for now, think of a portlet manager as a column)
- data - this is the portlet data, which is basically an instance of the portlet assignment class
People sometimes balk at the number of adapted things here. The base class alleviates us from having to remember all of them, of course. The idea of using this many parameters, though, is that it makes it possible to vary the rendering of a portlet depending on a number of things: the type of content object that's being shown (context); the current theme/browser layer (request); the current view, and whether or not this is the canonical view of the object (as indicated by the IViewView marker interface) or a particular view, like the manage-portlets view (view); where in the page the portlet was rendered (manager); and, of course, the type of portlet assignment (data);
To save us from making a lot of ZCML registrations, there is a convenient ZCML directive. It looks like this:
<plone:portlet name="portlets.Recent" interface=".recent.IRecentPortlet" assignment=".recent.Assignment" renderer=".recent.Renderer" addview=".recent.AddForm" editview=".recent.EditForm" />
There is also a <plone:portletRenderer /> directive to override the renderer for a particular context/layer/view/manager.
HOw are portlets installed?
The components and registration above create a new type of portlet. To install the portlet into a particular Plone site, we use GenericSetup, with a portlets.xml file:
<?xml version="1.0"?> <portlets> <portlet addview="portlets.Recent" title="Recent items" description="A portlet which can render a listing of recently changed items." i18n:attributes="title title_recent_portlet; description description_recent_portlet"> <for interface="plone.app.portlets.interfaces.IColumn" /> <for interface="plone.app.portlets.interfaces.IDashboard" /> </portlet> </portlets>
When this is run, it will create a local utility in the Plone site of the IPortletType. This just holds some metadata about the portlet for UI purposes:
class IPortletType(Interface): """A registration for a portlet type. Each new type of portlet should register a utility with a unique name providing IPortletType, so that UI can find them. """ title = schema.TextLine( title = u'Title', required = True) description = schema.Text( title = u'Description', required = False) addview = schema.TextLine( title = u'Add view', description = u'The name of the add view for assignments for this portlet type', required = True) for_ = Attribute('An interface a portlet manager must have to allow this type of portlet. ' \ 'May be None if there are no restrictions.')
Title and description should be self-explanatory. The addview is the name of the view used to add the portlet, which helps the UI to invoke the right form when the user asks to add the portlet. This should match the portlet name. for_ is an interface or list of interfaces that describe the type of portlet managers that this portlet is suitable for. This means that we can install a portlet that's suitable for the dashboard, say, but not for the general columns. Again, this is primarily about helping the UI construct appropriate menus.
HOW are portlets stored?
When a new portlet is added, it will result in a new instance of the particular Assignment class. This instance is stored in what's called an Assignment Mapping. This is an ordered container with a dict-like interface. The keys are unique string names, and the values are instances of the assignment class.
Assignment mappings are found in portlet managers. A portlet manager defines a column or other area that can be filled with portlets, and is analogous to the viewlet manager for viewlets. Each portlet manager is a persistent, named local utility. You can look one up like this:
manager = getUtility(IPortletManager, name=u"plone.leftcolumn")
By default, there are two standard portlet managers, plone.leftcolumn and plone.rightcolumn, as well as four portlet managers for the four columns on the dashboard (yes, this is suboptimal). You can create your owns in portlets.xml like this:
<portletmanager name="plone.leftcolumn" type="plone.app.portlets.interfaces.ILeftColumn" />
The "type" is a marker interface that can be used to install particular portlets only for particular types of portlet managers, as explained above.
Portlet assignments can either be assigned to global categories - like "group" portlets or "content type" portlets - or they can be assigned to specific content objects. Portlets in global categories are stored directly inside the IPortletManager utility, under a particular category - e.g. "group" - a category-specific key - e.g. the group id - and finally a unique portlet id. Putting this together, we could access a particular portlet assignment like this:
from plone.portlet.constants import GROUP_CATEGORY manager = getUtility(IPortletManager, name=u"plone.leftcolumn") recent_assignment = manager[GROUP_CATEGORY][u"Administrators"][u"recent"]
Each of the lookups here is has a dict interface, so you can iterate, call keys() and so on.
Assignments associated with content objects are a little different. They are stored in annotations on that content object for easy lookup. To get hold of the assignment, we multi-adapt the content object and the manager instance to the IPortletAssignment interface, like so:
manager = getUtility(IPortletManager, name=u"plone.leftcolumn") assignment_mapping = getMultiAdapter((context, manager), IPortletAssignmentMapping) news_portlet = assignment_mapping[u"news"]
There are two functions in plone.app.portlets.utils to make it easier to find the appropriate mapping for a portlet, or get a portlet assignment directly: assignment_mapping_from_key() and assignment_from_key().
HOw are portlets traversable?
In order to render portlet add and edit forms, we need to be able to traverse to portlets. This is achieved using namespace traversal adapters in plone.app.portlets. For example, a URL like http://site.com/++groupportlets++plone.leftcolumn+Administrators/portlet1/@@edit will render the edit view for the portlet with id "portlet1" under the "Administrators" key of the "group" category in the "plone.leftcolumn" portlet manager.
There are similar traversal adapters for ++contenttypeportlets++, ++dashboard++ (user portlets) and ++contextportlets++.
The traversal adapters locate up the appropriate IPortletAssignmentMapping and return it so that traversal can continue from this. For convenience, they will also create new assignment mappings on the fly if they do not already exist.
HOw are portlets rendered?
Portlets are always rendered inside a portlet manager. From a template, we can ask a portlet manager to render itself and all its portlets. This is achieved using a zope.contentprovider 'provider:' expression. In Plone's main_template, for example, you will find:
<tal:block replace="structure provider:plone.leftcolumn" />
Behind the scenes, this will look up a local adapter on (context, request, view) with name u"plone.leftcolumn" (this is just how the provider expression works).
As it happens, this local adapter factory was registered when the portlet manager was installed (via portlets.xml), and is a callable that returns an IPortletManagerRenderer. The portlet manager renderer is the "view" of the portlet manager.
The default implementation will simply output each portlet wrapped in a div with some helpful attributes to support AJAX via KSS. You can of course register your own portlet manager renderers. A portlet manager renderer is a multi-adapter on (context, request, view, manager). The @@manage-portlets view, for example, relies on a portlet manager renderer override for this particular view that renders the add/move/delete operations. For most people, the standard renderer will suffice, though.
The portlet manager renderer asks an IPortletRetriever to fetch and order the portlet assignments that it should render. This is a multi-adapter on (context, manager), which means that the fetch algorithm can be overridden either based on the type of content object being viewed, or the particular manager. There are two default implementations - one for "placeful" portlet managers (those which know about contextual portlets, such as the standard left/right column ones) and one for "placeless" ones that only deal in global categories. This latter retriever is used by the dashboard, which stores its portlets in a global "user" category.
The IPortletRetriever algorithm is reasonably complex, especially when contextual blacklisting is taken into account (see below). To make it possible to re-use this algorithm across multiple configurations, it is written in terms of an IPortletContext. The context content object will be adapted to this interface. The portlet context provides:
- A UID for the current context (usually just the physical path)
- A way to obtain the parent object (for acquiring portlets and blacklist information in a placeful retriever)
- A list of global portlet categories to look up, and the keys to look under.
The last parameter is best described by an example. Let's say we were looking at a Folder, logged in as a user called "testuser" that was a member of two groups - "Administators" and "Reviewers". The return value of globalPortletCategories() would then be:
[("content_type", "Folder",), ("group", "Administators",), ("group", "Reviewers",), ("user", "testuser",)]
This informs the retriever that it should first look up any portlets in the current portlet manager in the "content_type" category under the "Folder" key, and then portlets in the "group" category under the "Administators" and "Reviewers" key, and finally portlets in the "user" category under the "testuser" key, all in that order. Thus, if we wanted to add a new category, or change the order of categories, we could override the IPortletContext, either everywhere or just for one particular type of context.
Once the IPortletRetriever has retrieved the assignments that should be shown for the current portlet manager, the portlet manager renderer will look up the portlet renderer for each assignment, ensure that it should indeed be rendered by checking its 'available' property, and finally call update() and render(), placing the output in the reponse.
How does blacklisting work?
The portlet retriever can ignore whole categories of portlets (group, content type) or stop acquiring contextual portlets from higher levels, using a blacklist. The blacklist is stored in an annotation. To manipulate the blacklist for a particular content object and portlet manager combination, you can look up the ILocalPortletAssignmentManager multi-adapter:
blacklist = getMultiAdapter((context, manager), ILocalPortletAssignmentManager) blacklist.setBlacklistStatus(GROUP_CATEGORY, True)
The second parameter is True if the category is to be blocked (blacklisted), False if it is to be un-blacklisted, and None if you want to use the parent object's settings.
Where can I learn more?
The portlet infrastructure is relatively well documented in its main doctest, which can be found in the plone.portlet package. This package is usable outside Plone, and has been successfully used in Grok and Vudo. You may also want to look at the default Plone portlets and the Plone portlet GUI, which is found in plone.app.portlets.