Basic plone.portlets architecture¶
Description
This section describes the general architecture of a portlet through an example. You can checkout the example code from the collective.
The use case¶
As an example, we will develop a portlet to display the last n (where n is a positive integer ;) modified content items to logged-in users, which will be available to add it to any portlet manager (left or right column by default).
[screenshot follows]
The configuration data¶
When a portlet is first created, there are often customizations which can be made which tailor the portlet's behaviour to meet the user's needs: eg. which content type to display, how many items to list, etc... In our example, we want the person configuring the portlet to be able to specify how many of the most recent items will be displayed inside the portlet.
First, we have to describe the interface schema of the
configuration data we want to store using
zope.schema
(see
this page
for more info on schemas). By convention, this interface
derives from
IPortletDataProvider
, which is just a marker interface. In the package's
interfaces.py file, type:
from plone.portlets.interfaces import IPortletDataProvider
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)
The
PloneMessageFactory
makes our code ready to be localized using the Plone i18n
machinery.
After defining the configuration schema interface, we implement it in a class called the Assignment class. This is a persistent "content" class which 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 has a
title
attribute that is used in the portlet management UI to
distinguish different instances of the portlet.
from plone.app.portlets.portlets import base
from zope.interface import implements
from ploneexample.portlet.interfaces import IRecentPortlet
class Assignment(base.Assignment):
implements(IRecentPortlet)
def __init__(self, count=5):
self.count = count
@property
def title(self):
return _(u"Recent items")
The add and edit forms¶
To add the portlet and edit its configuration, we have to define appropiate add and edit forms.
This is typically done using zope.formlib and the
portlet schema, together with some base form classes to
save us from designing the forms template and logic
ourselves. If the portlet is not configurable, this can
use the special
base.NullAddForm
, which is just a view that creates the portlet and then
redirects back to the portlet management screen.
For more information about zope.formlib, check this tutorial.
The edit form can be omitted if the portlet configuration is not editable.
from zope.formlib import form
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.")
As it can be seen above, the add form must return an Assignment instance of the portlet.
The portlet presentation¶
Next, we define how the portlet will be rendered.
The Portlet Renderer is the "view" of the
portlet. This is just a content provider (in the
zope.contentprovider sense), in that it has an
update()
and a
render()
method, which will be called upon the rendering of the
portlet.
It's a multi-adapter that takes a number of parameters which makes it possible to vary the rendering of the portlet:
- context
- The current content object. Mind the type of content object that's being shown.
- request
- The current request. Mind the current theme/browser layer.
- view
-
The current (full page) view. Mind 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. - manager
- The portlet manager where this portlet was rendered (for now, think of a portlet manager as a column). Mind where in the page the portlet was rendered.
- data
- The portlet data, which is basically an instance of the portlet assignment class. Mind the configuration of the portlet assignment.
The Renderer base class relieves us from having to remember all these parameters.
The Renderer class must have an
available
property, which is used to determine whether this portlet
should be shown or not. Note you shouldn't include checks
for the user id, group or content-type here, since you can
perform these assignments later by registering the portlet
under a certain category (more on this later).
from plone.memoize.instance import memoize
from zope.component import getMultiAdapter
from Acquisition import aq_inner
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
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() # whether or not the current user is Anonymous
self.portal_url = portal_state.portal_url() # the URL of the portal object
# a list of portal types considered "end user" types
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 self._template()
@property
def available(self):
"""Show the portlet only if there are one or more elements."""
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]
When reading the previous code, note that:
-
plone_portal_state
andplone_tools
are helper views providing some useful attributes to gather information from. -
The
memoize
decorator is used here to cache the results of the catalog query to avoid the perfomance hit of re-generating them in each request. See the plone.memoize doctests for more information.
Registering the portlet¶
A convenient ZCML directive is provided to glue all components of the portlet in the Zope Component Architecture. In the package's configure.zcml file (or any other ZCML file included from it), write:
<configure
xmlns:five="http://namespaces.zope.org/five"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="ploneexample.portlet">
<five:registerPackage package="." initialize=".initialize" />
<include package="plone.app.portlets"/>
<plone:portlet
name="ploneexample.portlet.Recent"
interface=".recent.IRecentPortlet"
assignment=".recent.Assignment"
renderer=".recent.Renderer"
addview=".recent.AddForm"
editview=".recent.EditForm"
/>
</configure>
Note you have to define/reference the plone XML namespace
for the directive to work. There is also a
<plone:portletRenderer
/>
directive to override the renderer for a particular
context/layer/view/manager.
You can see the descriptions of all these directives together with their arguments in the metadirectives.py file of the plone.app.portlets package.
This ZCML directive is read at the Zope startup, so to register each class appropiately into the Component Architecture, but you won't be able to add your new portlet yet. You first need to install its portlet type into your Plone site, as described in the section which follows.
Installing the portlet¶
The components and registration above make a new type of portlet available for installation. To install the portlet type into a particular Plone site, use GenericSetup.
First, register a new GenericSetup extension profile using a registerProfile ZCML directive:
<configure
xmlns:five="http://namespaces.zope.org/five"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:gs="http://namespaces.zope.org/genericsetup"
i18n_domain="ploneexample.portlet">
<five:registerPackage package="." initialize=".initialize" />
<include package="plone.app.portlets"/>
<gs:registerProfile
name="ploneexample.portlet"
title="Recent Items Example"
directory="profiles/default"
description="An example portlet"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
<plone:portlet
name="ploneexample.portlet.Recent"
interface=".recent.IRecentPortlet"
assignment=".recent.Assignment"
renderer=".recent.Renderer"
addview=".recent.AddForm"
editview=".recent.EditForm"
/>
</configure>
Next, create the folder profiles/default and place a
portlets.xml
file inside with the following content:
<?xml version="1.0"?>
<portlets
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone">
<portlet
addview="ploneexample.portlet.Recent"
title="Recent items Example"
description="An example 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.
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. In this case, we're making the portlet suitable
for the dashboard and for any (either left or right)
column. Current portlet manager interfaces include
IColumn
,
ILeftColumn
,
IRightColumn
and
IDashboard
, all of them defined inside the plone.app.portlets
package.
Again, this is primarily about helping the UI construct appropriate menus.