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.