Portlets

« Return to page index

Plone Developer Manual is a comprehensive guide to Plone programming.

1. What's a portlet?

This manual covers what a developer needs to know to create new portlet types or customise existing ones.

Portlets are chunks of information that can be shown outside the main area of a page. They are usually boxes of different kinds which content editors can add, set properties of and policies for showing.

(Screenshot).

Differences with viewlets

A portlet is like a viewlet but with persistent configuration (i.e. persistent in the ZODB) and run-time changeable assignments.

Use a viewlet for:

  • General content which is always displayed, for example: breadcrumbs, the logo, or the footer. This is not limited to only visible elements but can also include CSS, javascript, etc. (actually, that's how ResourceRegistries work).
  • Displaying elements based on the interface provided by the current context.

Use a portlet when:

  • You need to specify the configuration data for an item. - i.e. number of entries to show.
  • You want to give the content editors a choice about when and where to display it.
  • You want to display it only from inside a specific folder.
  • You'd like to show it only to some groups or users - e.g. Review portlet only is shown to users within the Reviewers group.

 

2. Basic plone.portlets architecture

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:

  1. plone_portal_state and plone_tools are helper views providing some useful attributes to gather information from.
  2. 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.

3. Testing the portlet

Ensure everything's working as it should.

If the portlet was registered and installed correctly, it should now show up in the list of portlets available for addition into the type of portlet managers specified in the for argument of the portlet type (IColumn and IDashboard in our case), under the @@manage-portlets view (Manage Portlets link).

However, to ensure everything's working as it should without having to test it through the web, we can write some integration tests. This is recommended practice in the Plone universe. Moreover, once you've understood how the portlet infrastructure and its API work, you will be able to write tests first (you can copy&paste tests from other portlets products) and then start coding the portlet. More info on testing in the Testing in Plone tutorial.

Run them using bin/instance test -s ploneexample.portlet

 

4. How and where are Portlet Assignments stored?

When you choose a portlet to be displayed somewhere, for example, using the interface that appears when you hit the Manage Portlets button, what you're doing is storing a persistent instance of the Portlet Assignment class into your site, together with all its associated configuration data.

Portlet Assignments are 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 can be stored in two different kinds of locations depending on their type: site-wide or contextual.

Site-wide

Site-wide assigned portlets are shown in the whole site, unless blocked. They're stored in Portlet Managers. Portlet Managers define a column or other area that can be filled with portlets, and are analogous to the viewlet manager for viewlets. They are named persistent local utilites providing the IPortletManager interface.

You can look up a portlet manager 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, from plone.dashboard1 to plone.dashboard4. You can create your own in portlets.xml like this:

<portletmanager
  name="my.package.myportletmanager"
  type="my.package.interfaces.IMyPortletManagerType"
  />

The "type" is a marker interface that can be used to install particular portlets only for particular types of portlet managers, as explained above. Example: plone.app.portlets.interfaces.IDashboard.

Portlets in global categories (site-wide) 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"]

Here we look up the left column portlet manager and get the portlet assignment named recent assigned to the Administrators group.

Each of the lookups here has a dict interface, so you can iterate, call keys() and so on. You can store assignments under any string as category, but the default portlet retriever is only aware of the three site-wide assignment categories defined as constants in plone.portlet.constants, USER_CATEGORY, GROUP_CATEGORY and CONTENT_TYPE_CATEGORY, which should be enough for most use-cases. More on portlet retrievers later.

Contextual

Location-specific portlet assignments are stored on annotations on objects providing the ILocalPortletAssignable marker interface.

To get hold of the assignment in this case, 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 to get a portlet assignment directly: assignment_mapping_from_key() and assignment_from_key().

We can use GenericSetup to assign portlets to particular portlet managers upon the installation of a product. Read the Theme Reference Manual for info about how to do that. Read the Generic Setup tutorial for further info about what's GenericSetup and how it works.

5. How are portlets rendered?

The process to find, update and render portlets from the main views is rather complex. Here we describe how does it all work, step by step.

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 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 tag 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/blocking 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 universal identifier for the current context (usually just the physical path) - the uid property.
  • A way to obtain the parent object of the current context (for acquiring portlets and blacklist information in a placeful retriever) - the getParent() method.
  • A list of global portlet categories to look up, and the keys to look under (obtainable by using the globalPortletCategories() method on the adapted context).

The last parameter is best described by an example. Let's say we're logged in as "testuser", a member of both the "Administrators" and "Reviewers" groups, and were looking at a Folder. The return value of globalPortletCategories() would then be:

>>> portlet_context.globalPortletCategories()
[("content_type", "Folder",),
 ("group", "Administrators",),
 ("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.

6. Appendix: Practicals

6.1. Subclassing new portlets

This how-to briefly explains how to create new portlets based on another existing portlet class. (Mikko Ohtama)

Portlet subclassing is not trivial due to explict references between portlet engine parts. Here are short instructions minimal steps to needed to a subclass a portlet to another portlet. Instead of modifying the existing portlet, we need to create a new invariant with little changed properties. See this general briefing about Plone 3.x portlet mechanism. This example modifies the render behavior of static text portlet, by adding a grey backgroundd CSS class for it.

  1. Create a portlet interface stub and portlets Python module: To define a new portlet. Refer this in your product ZCML.
  2. Create a new assigment class: To make new portlet assignable through portlet manager
  3. Create a new add form class: To make new portlet creatable, returning your custom portlet instances
  4. Create a configure.xml ZCML entry: To make Zope to find the new portlet definition
  5. Create a portlets.xml installer entry: To make the portlet appear in the portlet manager menu

The portlet interface class is fixed to a portlet when the portlet is created. Thus, if you make changes any of above, you might need to create a new portlet to see the effect - old portlet instances don't necessarily see the changees.

Our portlet code lies in myproduct/browser/portlets/misc.py:

from zope.interface import implements
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from plone.portlet.static import PloneMessageFactory as _

# Import the base portlet module whose properties we will modify
from plone.portlet.static import static

class IGreyStaticPortlet(static.IStaticPortlet):
""" Defines a new portlet "grey static" which takes properties of the existing static text portlet. """
pass

class GreyStaticRenderer(static.Renderer):
""" Overrides static.pt in the rendering of the portlet. """
render = ViewPageTemplateFile('grey_static.pt')

class GreyStaticAssignment(static.Assignment):
""" Assigner for grey static portlet. """
implements(IGreyStaticPortlet)

class GreyStaticAddForm(static.AddForm):
""" Make sure that add form creates instances of our custom portlet instead of the base class portlet. """
def create(self, data):
return GreyStaticAssignment(**data)

myproduct/browser/portlets/configure.zcml snippet. Note that we do not need to override all (EditForm) views:

<configure xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:plone="http://namespaces.plone.org/plone">

<include package="plone.app.portlets" />

<plone:portlet
name="lsm.GreyStaticPortlet"
interface=".misc.IGreyStaticPortlet"
assignment=".misc.GreyStaticAssignment"
view_permission="zope2.View"
edit_permission="cmf.ManagePortal"
renderer=".misc.GreyStaticRenderer"
addview=".misc.GreyStaticAddForm"
editview="plone.portlet.static.static.EditForm"
/>

</configure>

myproducts/profiles/default/portlets.xml quick installer snippet:

<portlets>

<portlet
addview="lsm.GreyStaticPortlet"
title="Static portlet (grey)"
description="Portlet with light grey background"
/>

</portlets>

myproduct/browser/portlets/grey_static.pt. We have added one new CSS class (portletGrey) which has a CSS class definition defined in ploneCustom.css (through-the-web) or some of the product's CSS files:

<div tal:condition="view/data/omit_border"
tal:attributes="class string:portletStaticText ${view/css_class}"
tal:content="structure view/data/text" />
<dl tal:condition="not:view/data/omit_border"
tal:attributes="class string:portlet portletStaticText portletGrey ${view/css_class}"
i18n:domain="plone">

<dt class="portletHeader">
<span class="portletTopLeft"></span>
<span>
<a tal:omit-tag="not:view/has_link"
tal:attributes="href view/data/more_url"
tal:content="view/data/header"
/>
</span>
<span class="portletTopRight"></span>
</dt>

<dd class="portletItem odd">
<div tal:replace="structure view/data/text" />
<tal:corners condition="not:view/has_footer">
<span class="portletBottomLeft"></span>
<span class="portletBottomRight"></span>
</tal:corners>
</dd>

<dd class="portletFooter" tal:condition="view/has_footer">
<span class="portletBotomLeft"></span>
<span>
<a tal:omit-tag="not:view/has_link"
tal:attributes="href view/data/more_url"
tal:content="view/data/footer"
/>
</span>
<span class="portletBottomRight"></span>
</dd>

</dl>

myproduct/browser/portlets/__init__.py. Create empty file to a mark a Python module.

myproduct/configure.zcml. Add following snippet:

<include package=".portlets" />