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