Personal tools
You are here: Home Members David Convent rough for viewlets how-to
Document Actions

rough for viewlets how-to

by David Convent last modified 2007-06-04 17:59

work in progress, will move to plone.org once finished.

Plone 3 has switched to use Zope 3 viewlet components instead of the old macro include approach. In the main_template.pt code, we can see for instance that the content of the <div /> region with id portal-top contains only one line where it used to contain a whole list of macro calls (for rendering the site wide actions, the quick search box, the logo, the global sections, the personal bar and breadcrumbs, etc.):

<div id="portal-top" i18n:domain="plone">
<div tal:replace="structure provider:plone.portaltop" />
</div>

The provider TAL expression is a Zope 3 expression which looks up a content provider from a page template.

In the Zope 3 world, a content provider is a component that generates a portion of the code of an HTML page.
Like Zope 3 views, content providers are multi adapters that adapt the context and the request. In addition to views, content providers also adapt the view it is looked up from.

Viewlets are content providers, they are small components of a page, that render a small piece of HTML code.
In Plone templates, viewlets are not looked up directly though. In order to be able to organize viewlets with a maximum of flexibility, they are aggregated in viewlet managers. A viewlet manager is also a Zope 3 content provider, which renders a set of viewlets that are registered for it.

You can learn more about viewlets, viewlet managers and Zope 3 content providers by reading the corresponding section of Philipp's book (Web Component Development with Zope 3 - 2d. edition, chapter 10.4).

The advantage of this approach is that we can now organize the different regions of a Plone page without having to modify the code in main_template.pt itself.


Plone 3 viewlets registration

As mentioned in the above introduction, Plone 3 main template looks up viewlet managers only, it does not call viewlets directly. This also applies to any Plone 3 template that generates HTML code from Zope 3 components.

Those viewlet managers and their registered viewlets are defined in the plone.app.layout package, in its viewlets module.

In the configure.zcml file of the viewlets module, we see viewlet manager definitions, followed by the registration of their viewlets. We see for instance that the plone.portaltop viewlet manager provides the interface IPortalTop:

    <browser:viewletManager
name="plone.portaltop"
provides=".interfaces.IPortalTop"
permission="zope2.View"
class="plone.app.viewletmanager.manager.OrderedViewletManager"
/>

IPortalTop is a marker interface for the component that manages the plone.header, the plone.personal_bar and the plone.path_bar viewlets.
It is in the viewlet declarations that we find the management association between the viewlets and the viewlet manager, for instance:

    <browser:viewlet
name="plone.path_bar"
manager=".interfaces.IPortalTop"
class=".common.PathBarViewlet"
permission="zope2.View"
/>

We notice from its ZCML definition that the plone.header viewlet looks up a template directly (portal_header.pt), where the two other viewlets managed by IPortalTop are associated with a class:

    <browser:viewlet
name="plone.header"
manager=".interfaces.IPortalTop"
template="portal_header.pt"
permission="zope2.View"
/>

In that template, all there is is the call of another viewlet manager, which name is plone.portalheader:

<div id="portal-header">
<div tal:replace="structure provider:plone.portalheader" />
</div>

Again from configure.zcml we see that plone.portalheader, which implements IPortalHeader interface, manages the plone.skip_links, the plone.site_actions, the plone.searchbox, the plone.logo, and the plone.global_sections viewlets.

All these viewlets, with the aforementioned two ones registered for plone.portaltop, are associated with python classes defined in plone/app/layout/common.py. Those classes are Zope 3 browser views that implement zope.viewlet.interfaces.IViewlet, which means that they have a render() method that yields the HTML snippet that will be injected in the page.


We can also see in configure.zcml that all viewlet managers are instances of OrderedViewletManager (see manager.py in plone.app.viewletmanager). It means that viewlets will be looked up in a specific order. The order of the viewlets is defined in a utility that implements IViewletSettingsStorage. That utility stores the order of the viewlets in a viewlet manager for a specific skin. Its setup comes from a Generic Setup profile called viewlets.xml.
For the Plone Default skin, ordering of viewlets is described in Products/CMFPlone/profiles/default/viewlets.xml:

<?xml version="1.0"?>
<object>
<order manager="plone.portaltop" skinname="Plone Default">
<viewlet name="plone.header" />
<viewlet name="plone.personal_bar" />
<viewlet name="plone.app.i18n.locales.languageselector" />
<viewlet name="plone.path_bar" />
</order>
<order manager="plone.portalheader" skinname="Plone Default">
<viewlet name="plone.skip_links" />
<viewlet name="plone.site_actions" />
<viewlet name="plone.searchbox" />
<viewlet name="plone.logo" />
<viewlet name="plone.global_sections" />
</order>
</object>

Now that we know how Plone 3.0 sets up the defaults for viewlet managers and their viewlets in the Plone Default skin, let's see how we can customize those values from a Python product on the file system, either for the default skin or for a skin that we would be developing.

In the following paragraphs, I will refer to a product called MyTheme. Simply replace MyTheme with the name of the Python product you are developing on the file system.

Note:
For quickly generating a theme (skin) product for Plone 3.0, you can use the generator script of DIYPloneStyle (svn trunk until version 3.0 is out), or the plone3_theme template of ZopeSkel (from svn trunk).


Reordering viewlets

We saw in the previous paragraph that viewlets ordering is stored in a utility. That utility is set up from the viewlets.xml file of a GenericSetup profile.

If all you need is to reorder the viewlets in the Plone Default skin, simply copy the original viewlets.xml from CMFPlone/profiles/default/ into MyTheme/profiles/default/, and edit the copied file to make it reflect your needs.

If MyTheme is a product that creates a new skin in portal_skins, you will need to specify the order of the viewlets in the viewlet managers for that skin. First, copy the original viewlets.xml from CMFPlone into MyTheme/profiles/default/. In the copied file, replace Plone Default by the name of the skin that MyTheme adds to portal_skins. You should now have MyTheme/profiles/default/viewlets.xml that looks like this:

<?xml version="1.0"?>
<object>

<!-- We need to specify that we want to keep the viewlets in the same order
than the one defined for Plone Default. -->
<order manager="plone.portaltop" skinname="My Theme">
<viewlet name="plone.header" />
<viewlet name="plone.personal_bar" />
<viewlet name="plone.app.i18n.locales.languageselector" />
<viewlet name="plone.path_bar" />
</order>
<order manager="plone.portalheader" skinname="My Theme">
<viewlet name="plone.skip_links" />
<viewlet name="plone.site_actions" />
<viewlet name="plone.searchbox" />
<viewlet name="plone.logo" />
<viewlet name="plone.global_sections" />
</order>

</object>
Note:
It is mandatory to specify the order of viewlets in viewlet managers for every skin added to a Plone portal.
Otherwise viewlets will render in no order and the page sections will look buggy when that skin is selected.
Both DIYPloneStyle and ZopeSkel provide a correct viewlets.xml when generating a blank theme product.


Hiding a viewlet

Hiding a viewlet is also done from the viewlets.xml with the <hidden /> node which is at same level as <order />, and is done per skin selection.
For instance: if you need to remove the global sections for your skin, you'd have to add some declaration like the following one to  viewlets.xml:
  <hidden manager="plone.portalheader" skinname="My Theme">
<viewlet name="plone.global_sections" />
</hidden>
  <hidden manager="plone.portalheader" skinname="My Theme">
<viewlet name="plone.global_sections" />
</hidden>

Nothing that difficult here ;-)


Adding/Overriding a viewlet

Overriding a viewlet is simply a matter of hiding the original viewlet, and add a new one that will render at the place of the hidden one in the viewlet manager.
Hiding and ordering is something we can do, we now have to cover how to add a new one.

Lets follow the example that is shipped with DIYPloneStyle (from trunk untill version 3.0 is out):
the DotNetTheme product that is located in DIYPloneStyle/example/ replaces the portal logo template.
In the DotNetTheme code, in the viewlets/ folder, we have a set of files:

- viewlets/
- __init__.py
- configure.zcml
- interfaces.py
- logo.pt
- viewlets.py
__init__.py
This is an empty file, that is here simply to make viewlets a python module.
configure.zcml
The file where all Zope 3 configuration is defined for viewlets.
interfaces.py
We'll need this file a bit later, I leave it where it is for now.
logo.pt
The template that will replace the original HTML code in the main template.
viewlets.py
The file that stores view(let) Python classes.
In order to create a new viewlet, we'll have to write a viewlet class in viewlets.py (that renders the template), and register it for the viewlet manager we want it to show in (from configure.zcml). Then we'll have to hide the old viewlet and order the new one at the correct place from viewlets.xml (in the Generic Setup profile).

Let's edit the following files…

In viewlets/viewlets.py:
from zope.component import getMultiAdapter
from plone.app.layout.viewlets.common import ViewletBase
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

class LogoViewlet(ViewletBase):
# We override the render method of the viewlet with the template rendering
render = ViewPageTemplateFile('logo.pt')

def update(self):
# we set up in this method the variables that we'll need
# to call from the template
portal_state = getMultiAdapter((self.context, self.request),
name=u'plone_portal_state')

self.navigation_root_url = portal_state.navigation_root_url()

In viewlets/logo.pt:

<a id="portal-logo"
accesskey="1"
tal:attributes="href view/navigation_root_url"
i18n:domain="mytheme">
<img src="" tal:replace="structure here/header.jpg" />
</a>

In viewlets/configure.zcml:

<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">

<!-- The customized logo -->
<browser:viewlet
name="dotnet.logo"
manager="plone.app.layout.viewlets.interfaces.IPortalHeader"
class=".viewlets.LogoViewlet"
permission="zope2.View"
/>

</configure>

In profiles/default/viewlets.xml:

<?xml version="1.0"?>
<object>
<!-- We need to specify the order for all viewlet managers -->
<order manager="plone.portaltop" skinname="My Theme">
<viewlet name="plone.header" />
<viewlet name="plone.personal_bar" />
<viewlet name="plone.app.i18n.locales.languageselector" />
<viewlet name="plone.path_bar" />
</order>
<!-- This is where we place our newly created viewlet -->
<order manager="plone.portalheader" skinname="My Theme">
<viewlet name="plone.skip_links" />
<viewlet name="plone.site_actions" />
<viewlet name="plone.searchbox" />
<viewlet name="dotnet.logo" />
<viewlet name="plone.global_sections" />
</order>
<!-- And we hide the original one for our skin -->
<hidden manager="plone.portalheader" skinname="My Theme">
<viewlet name="plone.logo" />
</hidden>
</object>

Don't forget to put in skins/mytheme_images an image with name header.jpg. Now we can restart Zope and install our product.
No problem so far, we see that the header.jpg image is shown at the top of the portal.

Now let's go to Site Setup > Themes in the Plone interface, and let's select Plone Default as default theme.
We have an Attribute Error for header.jpg.

What happened is this: we added the dotnet.logo viewlet to plone.portalheader viewlet manager. This has been done for all skins.
So when we try to render a Plone page with Plone Default as selected theme Plone tries to render the new viewlet, which calls header.jpg. But header.jpg is only available for My Theme, as it stored is in a skin layer that has been registered for My Theme only in the portal_skins tool.

So we need to find a way to register a viewlet only for one theme (one skin selection).
This can be done thank to the plone.theme package. Thanks to that package, we can set a Zope 3 layer that corresponds to a skin selection (a theme).

Here is how we can setup this:

In viewlets/interfaces.py:

from zope.publisher.interfaces.browser import IDefaultBrowserLayer

class IMyThemeSpecific(IDefaultBrowserLayer):
"""Marker interface that defines a Zope 3 layer.
It will be used for the viewlets that we want to add to the
"My Theme" skin only.
"""

And in configure.zcml:

<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">

<interface
interface=".interfaces.IMyThemeSpecific"
type="zope.publisher.interfaces.browser.IBrowserSkinType"
name="My Theme"
/>

<!-- The customized logo -->
<browser:viewlet
name="dotnet.logo"
manager="plone.app.layout.viewlets.interfaces.IPortalHeader"
class=".viewlets.LogoViewlet"
layer=".interfaces.IMyThemeSpecific"
permission="zope2.View"
/>

</configure>

Note the interface declaration for the Zope 3 layer, and the layer parameter for the viewlet itself.

Now we have proper implementation of our viewlet overriding :-)


Moving a viewlet from a viewlet manager to another one

To achieve moving a viewlet from a viewlet manager to another one, we'll have to apply what we already covered in the previous paragraphs:

We have to hide the viewlet from the viewlet manager that we want to take it out from, and register it to the other viewlet manager for the zope 3 layer that corresponds to our theme.

Let's assume that we want to move the portal actions from top to bottom…

in viewlets/interfaces.py, we need to setup our Zope 3 layer:

from zope.publisher.interfaces.browser import IDefaultBrowserLayer

class IMyThemeSpecific(IDefaultBrowserLayer):
"""Marker interface that defines a Zope 3 layer.
It will be used for the viewlets that we want to add to the
"My Theme" skin only.
"""

in viewlets/configure.zcml:

<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">

<interface
interface=".interfaces.IMyThemeSpecific"
type="zope.publisher.interfaces.browser.IBrowserSkinType"
name="My Theme"
/>

<!-- Moved viewlet registration -->
<browser:viewlet
name="plone.site_actions"
manager="plone.app.layout.viewlets.interfaces.IPortalFooter"
class="plone.app.layout.viewlets.common.SiteActionsViewlet"
permission="zope2.View"
layer=".interfaces.IMyThemeSpecific"
/>

</configure>

in profiles/default/viewlets.xml:

<?xml version="1.0"?>
<object>
<!-- We need to specify the order for all viewlet managers -->
<order manager="plone.portaltop" skinname="My Theme">
<viewlet name="plone.personal_bar" />
<viewlet name="plone.header" />
<viewlet name="plone.app.i18n.locales.languageselector" />
<viewlet name="plone.path_bar" />
</order>
<order manager="plone.portalheader" skinname="My Theme">
<viewlet name="plone.skip_links" />
<viewlet name="plone.searchbox" />
<viewlet name="plone.logo" />
<viewlet name="plone.global_sections" />
</order>
<!-- We hide the one we want to move -->
<hidden manager="plone.portalheader" skinname="My Theme">
<viewlet name="plone.site_actions" />
</hidden>
</object>
Note

We don't need to specify the order for plone.portalfooter as it holds only one viewlet (the one we registered).
Done!
All we did, we did it without touching main_template.pt, isn't it a success?


Powered by Plone CMS, the Open Source Content Management System

This site conforms to the following standards: