Description
How to create and subclass portlets in Plone. How to look-up and modify portlets on the site programmatically.
Please read Portlets developer manual.
You can subclass a portlet to create a new portlet type with your enhanced functionality.
z3c.form is a modern form library for Plone. The out of the box Plone portlets use older zope.formlib.
Discussion related to the matter
Use <plone:portletRenderer> directive. Specify 1) layer, 2) template and/or 3) class 4) portlet interface.
You need <include package=""> directive for the package whose portlet you are going to override.
<configure
xmlns:plone="http://namespaces.plone.org/plone"
>
<include package="plone.app.portlets" />
<plone:portletRenderer
portlet="plone.app.portlets.portlets.news.INewsPortlet"
template="mytheme_news.pt"
layer=".interfaces.IThemeSpecific"
/>
</configure>
More information
These methods should honour zope.contentprovider.interfaces.IContentProvider call contract.
The portlet renderer can define available property to hint the portlet manager when the portlet should be rendered.
Example
class Renderer(base.Renderer):
@property
def available(self):
# Show this portlet for logged in users only
return not self.anonymous
Below is an simple example how to print all portlets which have been assigned to the portal root:
def check_root_portlets(self):
""" Print all portlet assignments in the portal root """
from zope.component import getUtility, getMultiAdapter
from plone.portlets.interfaces import IPortletManager
from plone.portlets.interfaces import IPortletAssignment
from plone.portlets.interfaces import IPortletAssignmentMapping
content = self.portal
for manager_name in [ "plone.leftcolumn", "plone.rightcolumn" ]:
print "Checking portlet column:" + manager_name
manager = getUtility(IPortletManager, name=manager_name, context=content)
mapping = getMultiAdapter((content, manager), IPortletAssignmentMapping)
# id is portlet assignment id
# and automatically generated
for id, assignment in mapping.items():
print "Found portlet assignment:" + id + " " + str(assignment)
The following code iterates through all portlets assigned directly to content items. This excludes dashboard, group and content type based portlets. Then it prints some info about them and renders them.
Example code:
from Products.Five.browser import BrowserView
from zope.component import getUtility, getMultiAdapter
from zope.app.component.hooks import setHooks, setSite, getSite
from plone.portlets.interfaces import IPortletType
from plone.portlets.interfaces import IPortletManager
from plone.portlets.interfaces import IPortletAssignment
from plone.portlets.interfaces import IPortletDataProvider
from plone.portlets.interfaces import IPortletRenderer
from plone.portlets.interfaces import IPortletAssignmentMapping
from plone.portlets.interfaces import ILocalPortletAssignable
from Products.CMFCore.interfaces import IContentish
class FixPortlets(BrowserView):
""" Magical portlet debugging view """
def __call__(self):
"""
"""
request = self.request
portal = getSite()
# Not sure why this is needed...
view = portal.restrictedTraverse('@@plone')
# Query all content items on the site which can get portlets assigned
# Note that this should excule special, hidden, items like tools which otherwise
# might appearn in portal_catalog queries
all_content = portal.portal_catalog(show_inactive=True, language="ALL", object_provides=ILocalPortletAssignable.__identifier__)
# Load the real object instead of index stub
all_content = [ content.getObject() for content in all_content ]
# portal itself does not show up in the query above,
# though it might contain portlet assignments
all_content = list(all_content) + [portal]
for content in all_content:
for manager_name in [ "plone.leftcolumn", "plone.rightcolumn" ]:
manager = getUtility(IPortletManager, name=manager_name, context=content)
mapping = getMultiAdapter((content, manager), IPortletAssignmentMapping)
# id is portlet assignment id
# and automatically generated
for id, assignment in mapping.items():
print "Found portlet assignment:" + id + " " + str(assignment)
renderer = getMultiAdapter((content, request, view, manager, assignment), IPortletRenderer)
# Renderer acquisition chain must be set-up so that templates
# et. al. can resolve permission inheritance
renderer = renderer.__of__(content)
# Seee http://svn.zope.org/zope.contentprovider/trunk/src/zope/contentprovider/interfaces.py?rev=98212&view=auto
renderer.update()
html = renderer.render()
print "Got HTML output:" + html
return "OK"
For more information about portlet assignments and managers, see
Example:
import Acquisition
from zope.component import getUtility, getMultiAdapter
from plone.portlets.interfaces import IPortletRetriever, IPortletManager
for column in ["plone.leftcolumn", "plone.rightcolumn"]:
manager = getUtility(IPortletManager, name=column)
retriever = getMultiAdapter((self.context, manager), IPortletRetriever)
portlets = retriever.getPortlets()
for portlet in portlets:
# portlet is {'category': 'context', 'assignment': <FacebookLikeBoxAssignment at facebook-like-box>, 'name': u'facebook-like-box', 'key': '/isleofback/sisalto/huvit-ja-harrasteet
# Identify portlet by interface provided by assignment
if IFacebookLikeBoxData.providedBy(portlet["assignment"]):
return True
return False
Below is an example how to render a portlet in Plone
How to get your portlet HTML:
import Acquisition
from zope.component import getUtility, getMultiAdapter, queryMultiAdapter
from plone.portlets.interfaces import IPortletRetriever, IPortletManager, IPortletRenderer
def get_portlet_manager(column):
""" Return one of default Plone portlet managers.
@param column: "plone.leftcolumn" or "plone.rightcolumn"
@return: plone.portlets.interfaces.IPortletManagerRenderer instance
"""
manager = getUtility(IPortletManager, name=column)
return manager
def render_portlet(context, request, view, manager, interface):
""" Render a portlet defined in external location.
.. note ::
Portlets can be idenfied by id (not user visible)
or interface (portlet class). This method supports look up
by interface and will return the first matching portlet with this interface.
@param context: Content item reference where portlet appear
@param manager: IPortletManagerRenderer instance
@param view: Current view or None if not available
@param interface: Marker interface class we use to identify the portlet. E.g. IFacebookPortlet
@return: Rendered portlet HTML as a string, or empty string if portlet not found
"""
retriever = getMultiAdapter((context, manager), IPortletRetriever)
portlets = retriever.getPortlets()
assignment = None
for portlet in portlets:
# portlet is {'category': 'context', 'assignment': <FacebookLikeBoxAssignment at facebook-like-box>, 'name': u'facebook-like-box', 'key': '/isleofback/sisalto/huvit-ja-harrasteet
# Identify portlet by interface provided by assignment
if interface.providedBy(portlet["assignment"]):
assignment = portlet["assignment"]
break
if assignment is None:
# Did not find a portlet
return ""
#- A special type of content provider, IPortletRenderer, knows how to render each
#type of portlet. The IPortletRenderer should be a multi-adapter from
#(context, request, view, portlet manager, data provider).
renderer = queryMultiAdapter((context, request, view, manager, assignment), IPortletRenderer)
# Make sure we have working acquisition chain
renderer = renderer.__of__(context)
if renderer is None:
raise RuntimeError("No portlet renderer found for portlet assignment:" + str(assignment))
renderer.update()
# Does not check visibility here... force render always
html = renderer.render()
return html
How to use this code in your own view:
def render_slope_info(self):
""" Render a portlet from another page in-line to this page
Does not render other portlets in the same portlet manager.
"""
context = self.context.aq_inner
request = self.request
view = self
column = "isleofback.app.frontpageportlets"
# Alternatively, you can directly query your custom portlet manager by interface
from isleofback.app.portlets.slopeinfo import ISlopeInfo
manager = get_portlet_manager(column)
html = render_portlet(context, request, view, manager, ISlopeInfo)
return html
How to call view helper function from page template
<div tal:replace="structure view/render_slope_info" />
More info
Example portlets.xml:
<!-- This leaves only News portlet -->
<portlet addview="portlets.Calendar" remove="true" />
<portlet addview="portlets.Classic" remove="true" />
<portlet addview="portlets.Login" remove="true" />
<portlet addview="portlets.Events" remove="true" />
<portlet addview="portlets.Recent" remove="true" />
<portlet addview="portlets.rss" remove="true" />
<portlet addview="portlets.Search" remove="true" />
<portlet addview="portlets.Language" remove="true" />
<portlet addview="plone.portlet.collection.Collection" remove="true" />
<portlet addview="plone.portlet.static.Static" remove="true" />
<!-- collective.flowplayer add-on -->
<portlet addview="collective.flowplayer.Player" remove="true" />
Portlet na,es can be found in plone.app.portlets/configure.zcml.
More info:
Sometimes, when you work with custom views and custom templates you need to disable right or left column for portlets.
This is how you do from within a template:
<metal:override fill-slot="top_slot"
tal:define="disable_column_one python:request.set('disable_plone.leftcolumn',1);
disable_column_two python:request.set('disable_plone.rightcolumn',1);"/>
And this is how you do it from within a view:
import grok
class SomeView(grok.View):
grok.context(IPloneSiteRoot)
def update(self):
super(SomeView, self).update()
self.request.set('disable_plone.rightcolumn',1)
self.request.set('disable_plone.leftcolumn',1)
Source: http://stackoverflow.com/questions/5872306/how-can-i-remove-portlets-in-edit-mode-with-plone-4
Sometimes you just want to turn off the portlets in a certain context that doesn't have a template or fancy view. To do this in code do this:
from zope.component import getMultiAdapter
from zope.component import getUtility
from plone.portlets.interfaces import IPortletManager
from plone.portlets.interfaces import ILocalPortletAssignmentManager
from plone.portlets.constants import CONTEXT_CATEGORY
# Get the proper portlet manager
manager = getUtility(IPortletManager, name=u"plone.leftcolumn")
# Get the current blacklist for the location
blacklist = getMultiAdapter((context, manager), ILocalPortletAssignmentManager)
# Turn off the manager
blacklist.setBlacklistStatus(CONTEXT_CATEGORY, True)
Or just do it using GenericSetup like a sane person:
If you need additional portlet slots at the site. In this example we use Products.ContentWellCode to provide us some facilities as a dependency.
The code skeleton works against this Plone add-on template.
Example portlet manager viewlets.py:
"""
For more information see
* http://collective-docs.readthedocs.org/en/latest/views/viewlets.html
"""
import logging
from fractions import Fraction
# Zope imports
from zope.interface import Interface
from zope.component import getMultiAdapter, getUtility, queryUtility
from five import grok
# Plone imports
from plone.portlets.interfaces import IPortletManager
from plone.app.layout.viewlets.interfaces import IPortalFooter
from Products.CMFCore.utils import getToolByName
# Local imports
from interfaces import IAddonSpecific, IThemeSpecific
grok.templatedir("templates")
grok.layer(IThemeSpecific)
# By default, set context to zope.interface.Interface
# which matches all the content items.
# You can register viewlets to be content item type specific
# by overriding grok.context() on class body level
grok.context(Interface)
logger = logging.getLogger("PortletManager")
class CustomPortletViewlet(grok.Viewlet):
""" grok viewlet base class for a custom portlet renderer based on Products.ContentWellPortlets
Orignal code from Products.ContentWellPortlets
"""
grok.baseclass()
# Id which we use to store portlets
name = ""
# Name of browser view which will render the management interface for portlets
# in this manager
manage_view = ""
# We have 5 portlet slots in this viewlet
portlet_count = 5
def update(self):
context_state = getMultiAdapter((self.context, self.request), name=u'plone_context_state')
self.manageUrl = '%s/%s' % (context_state.view_url(), self.manage_view)
## This is the way it's done in plone.app.portlets.manager, so we'll do the same
mt = getToolByName(self.context, 'portal_membership')
self.canManagePortlets = mt.checkPermission('Portlets: Manage portlets', self.context)
def showPortlets(self):
return '@@manage-portlets' not in self.request.get('URL')
def portletManagersToShow(self):
visibleManagers = []
for n in range(1,self.portlet_count):
name = '%s%s' % (self.name, n)
try:
mgr = getUtility(IPortletManager, name=name, context=self.context)
except:
# In the case we have problems to load portlet manager, do something about it
# This is graceful fallback in a situation where 1) add-on is already installed
# 2) new portlet code drops in and re-run add-on installer is
continue
if mgr(self.context, self.request, self).visible:
visibleManagers.append(name)
import pdb ; pdb.set_trace()
managers = []
numManagers = len(visibleManagers)
for counter, name in enumerate(visibleManagers):
pos = 'position-%s' % str(Fraction(counter, numManagers)).replace('/',':')
width = 'width-%s' % (str(Fraction(1, numManagers)).replace('/',':') if numManagers >1 else 'full')
managers.append((name, 'cell %s %s %s' % (name.split('.')[-1], width, pos)))
return managers
class ColophonPortlets(CustomPortletViewlet):
"""
Render a new series of portlets in colophon.
"""
# This name is used to store portlets,
# as referred in portlets.xml
name = 'PortletsColophon'
# This is custom management URL view for this,
# registered thru ZCML to point to Products.ContentWellContent manager view class.
manage_view = '@@manage-portlets-colophon'
grok.viewletmanager(IPortalFooter)
grok.template("portlets-colophon")
# Define a portlet manager declaration
from Products.ContentWellPortlets.browser.interfaces import IContentWellPortletManager
class IColphonPortlets(IContentWellPortletManager):
"""
This viewlet is a place holder to match portlets.xml and portlet management view together.
* Manager is referred by name in manage page template
* portlets.xml refers to this interface
* provider:ColophonPortlets expression is also used in template to render the actual porlets
"""
Example ZCML bit
<!-- Register new portlet management view for our portlet manager -->
<include package ="plone.app.portlets" />
<!--
The .pt file is customized for the portlet manager name (from portlets.xml)
and management link.
-->
<browser:page
name="manage-portlets-colophon"
for="plone.portlets.interfaces.ILocalPortletAssignable"
class="plone.app.portlets.browser.manage.ManageContextualPortlets"
template="templates/manage-portlets-colophon.pt"
permission="plone.app.portlets.ManagePortlets"
/>
The page template for the manager manage-portlets-colophon.pt is the following
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
metal:use-macro="context/main_template/macros/master"
>
<head>
<div metal:fill-slot="javascript_head_slot" tal:omit-tag="">
<link type="text/css" rel="kinetic-stylesheet"
tal:attributes="href string:${context/absolute_url}/++resource++manage-portlets.kss"/>
</div>
</head>
<body class="manage-portlet-well">
<metal:block fill-slot="top_slot"
tal:define="disable_column_one python:request.set('disable_plone.leftcolumn',1);
disable_column_two python:request.set('disable_plone.rightcolumn',1);" />
<div metal:fill-slot="main">
<tal:warning tal:condition="plone_view/isDefaultPageInFolder">
<dl class="portalMessage warning">
<dt i18n:translate="message_warning_above_content_area_dt">Is this really where you want to add portlets above the content?</dt>
<dd i18n:translate="message_warning_above_content_area_dd">If you add portlets here, they will only appear on this item. If instead you want portlets to appear on all items in this folder,
<a href=""
tal:attributes="href string:${plone_view/getCurrentFolderUrl}/@@manage-portlets-colophon"
i18n:name="manage-portletsinheader_link">
<span i18n:translate="add_them_to_the_folder_itself">add them to the folder itself</span>
</a>
</dd>
<dl>
</tal:warning>
<h1 class="documentFirstHeading"
i18n:translate="manage_portlets_in_header">Manage portlets in colophon
</h1>
<p>
<a href=""
class="link-parent"
tal:attributes="href string:${context/absolute_url}"
i18n:translate="return_to_view">
Return
</a>
</p>
<div class="porlet-well_manager">
<h2 i18n:translate="portlet-well-a">Colophon Portlet Well 1</h2>
<span tal:replace="structure provider:PortletsColophon1" />
</div>
<div class="porlet-well_manager">
<h2 i18n:translate="portlet-well-a">Colophon Portlet Well 2</h2>
<span tal:replace="structure provider:PortletsColophon2" />
</div>
<div class="porlet-well_manager">
<h2 i18n:translate="portlet-well-a">Colophon Portlet Well 3</h2>
<span tal:replace="structure provider:PortletsColophon3" />
</div>
<div class="porlet-well_manager">
<h2 i18n:translate="portlet-well-a">Colophon Portlet Well 4</h2>
<span tal:replace="structure provider:PortletsColophon4" />
</div>
<div class="porlet-well_manager">
<h2 i18n:translate="portlet-well-a">Colophon Portlet Well 5</h2>
<span tal:replace="structure provider:PortletsColophon5" />
</div>
</div>
</body>
</html>
Then we have portlets-colophon.pt page template for the viewlet which renders the portlets and related management link
<div id="portlets-colophon"
class="row">
<tal:block tal:condition="viewlet/showPortlets">
<tal:portletmanagers tal:repeat="manager viewlet/portletManagersToShow">
<div tal:attributes="class python:manager[1]"
tal:define="mgr python:manager[0]"
tal:content="structure provider:${mgr}" />
</tal:portletmanagers>
<div style="clear:both"><!-- --></div>
<div class="manage-portlets-link"
tal:condition="viewlet/canManagePortlets">
<a href=""
class="managePortletsFallback"
tal:attributes="href viewlet/manageUrl">
Add, edit or remove a portlet in <b tal:content="viewlet/name" />
</a>
</div>
</tal:block>
</div>
Finally there is portlets.xml which lists all the portlet managers and associates them with the used interface
<?xml version="1.0"?>
<!-- Set up all the new portlet managers we need above and below the content well -->
<portlets>
<portletmanager
name="PortletsColophon1"
type="youraddon.viewlets.IColphonPortlets"
/>
<portletmanager
name="PortletsColophon2"
type="youraddon.viewlets.IColphonPortlets"
/>
<portletmanager
name="PortletsColophon3"
type="youraddon.viewlets.IColphonPortlets"
/>
<portletmanager
name="PortletsColophon4"
type="youraddon.viewlets.IColphonPortlets"
/>
<portletmanager
name="PortletsColophon5"
type="youraddon.viewlets.IColphonPortlets"
/>
</portlets>
More info
Example (in progress):
from lxml import etree
from StringIO import StringIO
import urlparse
from lxml import html
def fix_links(content, absolute_prefix):
"""
Rewrite relative links to be absolute links based on certain URL.
@param html: HTML snippet as a string
"""
parser = etree.HTMLParser()
content = content.strip()
tree = html.fragment_fromstring(content, create_parent=True)
def join(base, url):
"""
Join relative URL
"""
if not (url.startswith("/") or "://" in url):
return urlparse.urljoin(base, url)
else:
# Already absolute
return url
for node in tree.xpath('//*[@src]'):
url = node.get('src')
url = join(absolute_prefix, url)
node.set('src', url)
for node in tree.xpath('//*[@href]'):
href = node.get('href')
url = join(absolute_prefix, href)
node.set('href', url)
data = etree.tostring(tree, pretty_print=False, encoding="utf-8")
return data
The source code of this file is hosted on GitHub. Everyone can update and fix errors in this document with few clicks - no downloads needed.
For basic information about updating this manual and Sphinx format please see Writing and updating the manual guide.