Extending a portlet's schema

Portlets? Who uses those anymore?

We don't a whole lot, but I had a need recently to add new fields to the static text and navigation portlets. This post is not a question, but an overview of how I accomplished this task (in the off chance someone might be looking to do something similar). This was done on Plone 5.2.

After much thinking and searching, I realized that in my 14 years of Plone development experience, I have never added fields to a default Plone portlet. I always created new portlets. And after discussing some with @abl123, we determined it must not be possible to add new fields to the default portlets. Plus I could not find any instances of anyone else doing this. So I started down the path of doing some subclassing.

But when I ran into some issues (because it has been a long time since I've done much of anything with portlets), I ended up digging into the plone.app.portlets code, and stumbled upon this file for tests: plone.app.portlets/test_formextender.py at master · plone/plone.app.portlets · GitHub - and it does exactly the thing I initially set out to do. My code was written in a way to make sure portlets that already exist in the site won't throw errors, and they will display default styling set in the CSS.

So here is the code I ended up writing to accomplish my tasks. It adds two fields to all portlets (I'm sure the zcml could be adjusted if you only want them on one portlet). Based on the selected choices on the fields, certain classes are added to the portlets. I'm not including the navigation.pt here, the only thing I did was add the classes to the template.

configure.zcml:

    <adapter
        factory=".portlets.PortletColorAdapter"
        for="plone.portlets.interfaces.IPortletAssignment"
        />

    <!-- update the portlet add form -->
    <adapter
        factory=".portlets.PortletColorFormExtender"
        for="zope.interface.Interface
             zope.publisher.interfaces.browser.IDefaultBrowserLayer
             plone.app.portlets.browser.interfaces.IPortletAddForm"
        provides="plone.z3cform.fieldsets.interfaces.IFormExtender"
        name="portletcolor.extender"
        />

    <!-- update the portlet edit form -->
    <adapter
        factory=".portlets.PortletColorFormExtender"
        for="zope.interface.Interface
             zope.publisher.interfaces.browser.IDefaultBrowserLayer
             plone.app.portlets.browser.interfaces.IPortletEditForm"
        provides="plone.z3cform.fieldsets.interfaces.IFormExtender"
        name="portletcolor.extender"
        />

    <!-- update css_class for the Static Text portlet --> 
    <plone:portletRenderer
        portlet="plone.portlet.static.static.IStaticPortlet"
        class=".portlets.StaticRenderer"
        layer="engineering.theme.interfaces.ICustomTheme"
        />

    <!-- override the navigation portlet template -->
    <plone:portletRenderer
        portlet="plone.app.portlets.portlets.navigation.INavigationPortlet"
        template="navigation.pt"
        layer="engineering.theme.interfaces.ICustomTheme"
        />

portlets.py:

from plone.portlet.static import PloneMessageFactory as _
from plone.portlet.static import static
from plone.portlets.interfaces import IPortletAssignment
from plone.z3cform.fieldsets.extensible import FormExtender
from zope import schema
from zope.component import adapter
from zope.interface import Interface, implementer

EXTENDER_PREFIX = 'portletcolor'


class IPortletColor(Interface):
    """ Schema for portlet color options  """

    color = schema.Choice(
        title = "Portlet Color",
        description = "Header background and border color",
        default='gray',
        vocabulary=schema.vocabulary.SimpleVocabulary([
            schema.vocabulary.SimpleTerm(
                'gray', 'gray', 'Gray'),
            schema.vocabulary.SimpleTerm(
                'gold', 'gold', 'Gold'),
        ]),
    )
    display_border = schema.Bool(
        title = "Display portlet border?",
        description = "If selected, a 1px border will display \
            around the portlet in the color selected above",
            default = False,
            required = False,
    )


class PortletColorFormExtender(FormExtender):

    def update(self):
        self.add(IPortletColor, prefix=EXTENDER_PREFIX)


@adapter(IPortletAssignment)
@implementer(IPortletColor)
class PortletColorAdapter(object):
    """adapter for hooking up the color fields to all portlets
       each portlet template needs to be updated to add the color class
       (see NavigationRenderer and StaticRenderer below)
    """
    
    def __init__(self, context):
        self.__dict__['context'] = context

    def __setattr__(self, attr, value):
        setattr(self.context, attr, value)

    def __getattr__(self, attr):
        return getattr(self.context, attr, None)


class StaticRenderer(static.Renderer):
    # the static portlet has css_class we can hook in to
    # without creating a template override

    def css_class(self):
        css_class = super(StaticRenderer, self).css_class()
        if hasattr(self.data, 'color'): 
            css_class += ' portletcolor-' + self.data.color
        if getattr(self.data, 'display_border', False): 
            css_class += ' portletborder'
        return css_class
6 Likes

Thanks for sharing! I guess it has been possible to do it this way since portlets were updated to use z3c.form sometime in the Plone 5 release series. I have some code from before that to do something similar by monkey-patching the formlib-based portlet forms, but it's probably best to not let that see the light of day any longer.

1 Like

An add'on that also extends all portlets with extra fields is collective.portletmetadata. We've been using it in some Plone 4 sites and @mauritsvanrees has updated it this year for Python 3 & Plone 5.2

2 Likes