Create new style portlets and override existing ones
Our experience of creating new portlets was one of "let's just keep making Classic portlets 'cos we know how to do that and it's easy". Eventually we came across a situation where it made much more sense to override the built-in login portlet than write another, so we were forced to learn how the new mechanism worked. At the same time, we needed some new portlets, so to complete our education we decided to learn how to do this the new way too. And after a bit of head scratching, we found it really wasn't too bad. Here's what we discovered...
Portlets are now much more like viewlets. They have the same sort of configure.zcml and portlets.xml wiring, and the logic is pretty similar with classes and templates plugged together in the same kind of way. If you don't understand how viewlets work, have a look here, otherwise, if you begin by thinking of portlets of working kind of like viewlets, you won't be miles out as a starting point.
Overriding an existing portlet
This is actually pretty straightforward. We're going to assume you've already got a theme product that you created with paster or something else because that was our starting point. We've got a browser folder, with a configure.zcml slug and an interfaces.py which defines the IThemeSpecific class.
Make sure you have the plone namespace defined in the configure node at the top of the page xmlns:plone="http://namespaces.plone.org/plone e.g.
<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" xmlns:plone="http://namespaces.plone.org/plone" i18n_domain="bb.networks">
Your configure.zcml should already have an interface directive. If it doesn't add one like this:
You now need to include the original plone.app.portlets package like this:
<include package="plone.app.portlets" />
Then you need to override the portlet renderer as follows (this is where you are overriding the news portlet):
In this case we're just creating a new template and putting it in the templates directory in the browser module/directory. It could also be in the root of browser, if you don't have a templates directory. You can also add a new class here instead of a new template. This is a little more involved and requires that you understand a few more of the requirements of a new-style portlet.
New portlet, new class
A portlet class contains a number of requirements which all portlets need. Many of these can be imported from the existing class when overriding a portlet, but it helps to understand what they are. Here's an example of the bit you'd put in the configure.zcml file:
Now you need to create your news.py in the browser directory, with a class called MyNewsRender. This class is where you'd do any logic to work things out you wanted to display in the template. Because you're overriding an existing portlet, you can import most of the stuff you need, leaving you with the following as the minimum required in your news.py:
from plone.app.portlets.portlets.news import Renderer from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile class MyNewsRenderer(Renderer): render = ViewPageTemplateFile('templates/mytheme_news.pt')
However if you do that, you might as well just use the template version above, and leave the portlet using the standard class, as all you've actually changed is the template being used to render the portlet. If you wanted to add some more properties or logic though, the class would be the place to do it.
Creating a new portlet in an existing theme
This process isn't too bad either, once you've overridden a portlet, with one or two added complications which aren't actually any problem if you understand the process. Before we start, you need to understand the things that a portlet needs. You have to create a module e.g. my_new_portlet.py in your browser directory. This has to have a few standard classes.
1) An interface. This isn't required but is almost always used. It is usually derived fromIPortletDataProvider, which is a marker interface:
class ILoggedInPortlet(IPortletDataProvider): """A portlet which can render the logged in data. """
2) An Assignment class to store the persistent configuration data for the portlet. It's required even if the portlet isn't configurable though.
class Assignment(base.Assignment): implements(ILoggedInPortlet) title = _(u'label_logged_in', default=u'Logged in')
3) An Add form which generally uses zope.formlib and the portlet schema, though we haven't done one of these yet. If the portlet is not configurable, like ours, it can use a special "NullAddForm", which is just a view that creates the portlet and then redirects back to the portlet management screen.
class AddForm(base.NullAddForm): def create(self): return Assignment()
4) An edit form which you can omit if your portlet isn't editable.
class EditForm(base.EditForm): form_fields = form.Fields(IRecentPortlet) label = _(u"Edit Logged in Portlet") description = _(u"This portlet displays user data for logged in members")
5) A renderer or "view" which is a content provider with a render() and update() method. You can use the default empty update method in most cases without therefore defining one yourself.
class Renderer(base.Renderer): render = ViewPageTemplateFile('templates/mytheme_portlet.pt') def __init__(self, context, request, view, manager, data): base.Renderer.__init__(self, context, request, view, manager, data) self.membership = getToolByName(self.context, 'portal_membership') self.portal_catalog = getToolByName(self.context, "portal_catalog") self.context_state = getMultiAdapter((context, request), name=u'plone_context_state') self.portal_state = getMultiAdapter((context, request), name=u'plone_portal_state') self.pas_info = getMultiAdapter((context, request), name=u'pas_info') member = self.membership.getAuthenticatedMember() self.fullname = member.getProperty('fullname','') @property def available(self): return not self.portal_state.anonymous()
You'll need to import a bit of stuff into your portlet too e.g. for the examples above:
from zope.interface import implements from zope.component import getMultiAdapter from plone.portlets.interfaces import IPortletDataProvider from plone.app.portlets.portlets import base from zope import schema from zope.formlib import form from plone.memoize.instance import memoize from Acquisition import aq_inner from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.CMFCore.utils import getToolByName from Products.CMFPlone import PloneMessageFactory as _
Once you have created your loggedin.py or new_portlet.py with the above classes as required, you need to plug it in to your configure.zcml:
<plone:portlet name="my.theme.loggedin" interface=".loggedin.ILoggedInPortlet" assignment=".loggedin.Assignment" renderer=".loggedin.Renderer" addview=".loggedin.AddForm" editview=".loggedin.EditForm" />
If your portlet isn't configurable, you can miss out the editview line.
To make it addable, you need to use genericsetup i.e. portlets.xml to install your new portlet to add it to the drop down menu in the right places.
What you can do in portlets.xml
When you're overriding a portlet, you don't need to do anything in portlets.xml, unless you wanted to add it to a new interface. Here the interfaces in question are the two columns (IColumn) and the user dashboard (IDashboard). Adding the portlet to either of these makes them addable in these two locations i.e. left and right columns or dashboard. If you had created a new interface for a new place in the template to add a portlet, you'd need to add it here.
You're going to need to use portlets.xml to install any new portlets you've made however. Here's an example adding it both to the columns interface and the dashboard interface, which is standard for most portlets:
<portlet addview="my.theme.navigation" title="My Theme navigation" description="A portlet which renders some some special navigation"> <for interface="plone.app.portlets.interfaces.IColumn" /> <for interface="plone.app.portlets.interfaces.IDashboard" /> </portlet>
You can do much more than just register your portlet in the system in genericsetup though. You can remove all portlets for a section of the site, or a content type, and install new ones. You need to "purge" if you're installing, otherwise every time your install you'll add a new version of the same portlet in the same place. You can also block portlets from appearing which are defined for the parent or content type (and probably the group too). Examples of all of these will follow.
Here's a blocking example:
<blacklist manager="plone.rightcolumn" category="context" location="/welcome" status="block" />
Here's a purge and install:
<assignment purge="True" manager="plone.rightcolumn" category="context" key="/welcome" /> <assignment insert-before="*" manager="plone.rightcolumn" category="context" key="/welcome" type="bb.beetlebrow.first_right_portlet" /> <assignment insert-after="bb.beetlebrow.first_right_portlet" manager="plone.rightcolumn" category="context" key="/welcome" type="bb.beetlebrow.second_right_portlet" />
Here are some helpful links
The info above was gleaned from these, and there's lost more info you will need if you want to make your portlet editable or configurable, or create a new interface so you can add portlets to new places in the layout.
- (Straightforward TTW management, not development)
- (Martin Aspeli explains how they work and why)
- (Martin Aspeli again, defending portlets against the charge that they've been made pointlessly more complex)
- (this is especially useful for overriding a portlet)
- (the main docs page, with lots of info, including how to create a new portlet product using paster)
- (not really relevant here, but very useful if you want to add portlets in new places in a template, and good for your general understanding of how portlets work)