Subclassing new portlets

Description

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 background 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 assignment 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" />