This is the third post in a short series about the ideas, trials and tribulations of my first theme product. I’ll be posting the whole story in one place when the series is complete.
The Viewlet Manager
There are 4 parts to a viewlet manager. There’s a class that implements zope.viewlet.interfaces.IViewletManager, a template that renders the output of the manager, some configuration in ZCML that identifies the class as a viewlet manager, and a marker interface that facilitates the association of the viewlets with this particular viewlet manager.
All of this configuration will occur in the
The class is very simplistic in this case. Most of the code that you may be used to seeing (say, in side of a browser view class) is handled by ZCML directives.
from Products.Five.viewlet.manager import ViewletManagerBase class MyHeaderViewletManager(ViewletManagerBase): """ A custom viewlet manager to re-organize some stock plone viewlets. """
That’s it (at least for now). Note that we’re inheriting from
Products.Five.viewlet.manager.ViewletManagerBase. Generally speaking, if you see something in a zope 3 reference (like Philipp’s book), you should check for a Five equilivent, and use that if you can.
In order for the viewlet manager to be found by the
provider prefix later on, it must provide an interface that inherits from
zope.viewlet.interfaces.IViewletManager. Technically, I think there’s a deeper provider-related interface that’s required, but for our purposes IViewletManager is sufficient.
This is a marker interface, and it will be used to associate viewlets with our viewlet manager later on.
from plone.theme.interfaces import IDefaultPloneLayer from zope.viewlet.interfaces import IViewletManager class IThemeSpecific(IDefaultPloneLayer): """Marker interface that defines a Zope 3 browser layer. """ class IMyTop(IViewletManager): """ Marker interface for viewlets that can be displayed in my custom header viewlet manager """
The real heavy lifting in setting up a viewlet manager happens via ZCML, in the
... <!-- Viewlets registration --> <browser:viewletManager name="my.header" provides=".interfaces.IMyTop" class=".viewletmanager.MyHeaderViewletManager" layer=".interfaces.IThemeSpecific" permission="zope2.View" template="myheader.pt" /> ...
name designates an identifier for the viewlet manager.
provides is (more or less) equilivent to adding a
implements() call in your viewlet manager class.
class specifies the class name.
permission sets the zope permission for the viewlet manager (this would allow you to create special viewlet managers for logged in users, for example).
layer attribute is important, it helps to separate your theme configuration out from other themes (especially the default plone theme). This is accomplished by indicating a theme-specific interface, which ZopeSkel sets up for you as
template directive lets you specify a zope page template that will be used to render the viewlet manager.
This template is very simple. It’s aim is to break the header up into two columns, so my header will stretch to handle the width of the browser window (and I can put different background images on each side). It also puts the "personal bar" and the "global sections" into one line. I accomplish this with divs, which will be styled later on.
<div id="my_header"> <div id="main"> <div id="left_side"> Left-wing </div> <div id="right_side"> Right-wing </div> </div> <br /> <div> <div id="personal_bar"> Personal Bar </div> <div id="sections"> Global Tabs </div> </div> <br /> <div id="crumbtrail"> The Crumb Trail </div> </div>
I’ll root all of my css rules on that main
#my_header div, so I should avoid any major namespace collisions.
At this point, I’ve got a working custom viewlet manager. Assuming you’ve already run
bin/instance fg should run without any errors.
This is great, but if you want to know for sure that it worked (and you aren’t writing unit tests, whcih you probably should), you’ll have to add a plone site and install your product. Then reload the main page and… alas, you still won’t see anything.
As I mentioned earlier, viewlet managers aren’t dynamically pulled into the main plone template, which is aptly named
main_template. You have to add a TALES expression into that template to get your viewlet manager to render.
This can be accomplished in the short term by finding the template in the ZMI and using the "customize" feature to copy it into your
custom directory. A better way is to override
main_template in your skin.
This is jumping the gun a little bit, since I wanted to talk about resources and customizations later on, but I think it’s important to see something working at this stage in the process.
The Through The WebTM (TTW) way is a classic zope/plone customization use case. If you’ve done anything serious with Plone, you’ve probably done this before. I’m going to cover it here since I didn’t really do it much in the past, and I think it’s a great way to do quick prototyping or poke around in templates that people reference by id.
Here are the steps:
Fist make sure that your Plone instance is up and running.
Open up http://localhost:8080/plone. (I’m assuming you’ve got a plone site object already and you set up zope to listen on port 8080). Log in as an administrator or manager (if you used the buildout above log in with username/password admin/admin).
Click on the ‘site setup’ link in the upper right hand corner (http://localhost:8080/plone/plone_control_panel,
and click on the "Zope Management Interface" link (http://localhost:8080/plone/manage_main
Click on ‘portal_skins’ (http://localhost:8080/plone/portal_skins/manage_main), and then click on the ‘Find’ tab at the top (http://localhost:8080/plone/portal_skins/manage_findForm).
Enter ‘main_template’ in the field labeled "with ids:". Click the ‘Find’ button, and you should see a link to plone_templates/main_template. Click on it.
Click the "Customize" button. This will copy the template into the
customfolder, which will override anything that shipped with Plone. This works for other templates as well as other resources (try putting an image called “logo.jpg” in that folder).
The way we alter
main_template is the same whether we do the override TTW or override it in our theme product, so I’m going to cover getting
main_template into the theme product first.
In this case, we’ll just copy the template from it’s home in
I found the template by doing a
find, and then copied the CMFPlone one.
This will only work after you run bin/buildout :)
$ cd ~/my.theme $ find . -name main_template* ./eggs/plone.app.kss-1.4.3-py2.4.egg/plone/app/kss/browser/main_template_standalone.pt ./parts/plone/CMFPlone/skins/plone_templates/main_template.pt ./parts/plone/CMFDefault/skins/zpt_generic/main_template.pt ./parts/plone/NuPlone/skins/nuplone_templates/main_template.pt $ cp ./parts/plone/CMFPlone/skins/plone_templates/main_template.pt ./src/my.theme/my/theme/skins/my_theme_custom_templates/
In either case, getting the viewlet manager to render is accomplished the same way. We simply need to add this line:
<div tal:replace="structure provider:my.header" />
To the copy of
main_template. In my case, I put it just after the opening <body> tag, so my header would be above all the others.
This is a
<div> tag that is using the
replace TAL attribute to call the viewlet manager that I registered earlier as a content provider. A content provider is an object that produces a chunk of content. This is a TALES expression. I’m specifying the value (
my.header) of the
name attribute that I used when I registered my viewlet manager.
structure keyword tells TAL that the output of the expression shouldn’t be escaped; it should be treated as literal HTML.
For more information about TALES syntax, see the page templates reference
Now if you reload the main Plone page, you’ll see the header template output at the top of the screen, similar to this:
Note:I am not happy about this situation. It’s fairly simple to customize
main_template, but there is a lot of Plone-specific code in that template right now. I know, it could be worse, but it’s still pretty bad. I worry that keeping up with this template as Plone evolves may prove to be a real PITA. :)
Let There Be Viewlets!
Now that the viewlet manager is up and running, and I can see it in the plone site, there are two steps left before styling the whole shebang. First, I have to associate all of the viewlets we want with the new viewlet manager. Second, I have to call the viewlets inside of the vielwet manager’s template,
myheader.pt, which I created earlier and registered in the
Before we get down to business, I need to identify all of the viewlets that we’ll need for my header.
I won’t go into the details here, but taking a glance at the
@@manage-viewlets view (by say, opening http://localhost:8080/plone/@@manage-viewlets) you can figure out the names of the various viewlets that come with plone. That view also lets you show or hide various viewlets. Handy!
Another useful tool for figuring out what viewlets you want (or want to override, for that matter) is GloWorm. It gives you an introspection interface, similar to FireBug, for inspecting and overriding viewlets.
So here’s what I’ll need, viewlet-wise:
plone.logo. I want to move the logo over to the right hand column of my header.
plone.personal_bar. This viewlet handles the "log in" link.
plone.global_sections. This viewlet controls the main navigation tabs across the top of the site. I want to merge them into the same line as the personal bar
plone.path_bar. This is the "crumb trail" that lets you know where you are in the site.
plone.site_actions. These are the links to the control panel, accessibility information, etc that are usually on the top right hand side of the site. I want to move them to the left hand side.
To connect viewlets to a viewlet manager, you use the
<browser:viewlet> ZCML tag. I’ll need a tag for each of the viewlets I want access to in my viewlet manager.
Aside from the name of the viewlets I want, I also need to know what the name of their class is. Luckily, all of the stock Plone viewlets are located in one place. Poke around in
eggs/plone.app.layout-x.x.x-py2.4.egg/plone/app/layout/viewlets (the x’s are the version number, which may vary) to see them all, how they’re coded, and how they’re wired up.
... <!-- Viewlets registration --> <browser:viewletManager name="my.header" provides=".interfaces.IMyTop" class=".viewletmanager.MyHeaderViewletManager" layer=".interfaces.IThemeSpecific" permission="zope2.View" template="myheader.pt" /> <browser:viewlet name="plone.logo" manager=".interfaces.IMyTop" class="plone.app.layout.viewlets.common.LogoViewlet" permission="zope2.View" /> <browser:viewlet name="plone.global_sections" manager=".interfaces.IMyTop" class="plone.app.layout.viewlets.common.GlobalSectionsViewlet" permission="zope2.View" /> <browser:viewlet name="plone.personal_bar" manager=".interfaces.IMyTop" class="plone.app.layout.viewlets.common.PersonalBarViewlet" permission="zope2.View" /> <browser:viewlet name="plone.path_bar" manager=".interfaces.IMyTop" class="plone.app.layout.viewlets.common.PathBarViewlet" permission="zope2.View" /> <browser:viewlet name="plone.site_actions" manager=".interfaces.IMyTop" class="plone.app.layout.viewlets.common.SiteActionsViewlet" permission="zope2.View" /> ...
In this ZCML, I’m defining new viewlets that utilize the class of an existing plone viewlet. To do this, I specify the manager via the marker interface (
IMyTop), and attach a permission to restrict access. I haven’t tried this but according to the docs, you can also specify a
template attribute like I did for the viewlet manager to customize the way the viewlet looks without touching the python code. Nice!
Now for rendering the viewlets in my template.
Viewlet managers implement the
__getitem__() method of the dictionary api, so in the viewlet manager template you can access a viewlet by the simple TALES expression (for the plone logo)
At first, I thought I could use the
provider prefix like I did to get the viewlet manager to display in
main_template. This doesn’t work. You have to access the viewlet as an object, and call it’s
But again, things aren’t always as they seem. Calling
render() directly causes problems because it’s expected that another method is called first,
update(). It took me a while to figure this out, so hopefully this will save some hassle. It doesn’t seem quite right, however. My understanding of the
provider concept is that the
update() method would be called before the
render() method and all would be well. Yet, I get a ComponentLookupError whenever I try to render a viewlet as a provider. Weird.
To get around this problem, I went back to my viewlet manager class and overrode the
__getitem__() method, so that when it’s called, the
update() method is called on the viewlet before the viewlet object is returned.
from Products.Five.viewlet.manager import ViewletManagerBase class MyHeaderViewletManager(ViewletManagerBase): """ A custom viewlet manager to re-organize some stock plone viewlets. """ def __getitem__(self, name): """ Overriding getitem to call update() on access """ viewlet = super(MyHeaderViewletManager, self).__getitem__(name) viewlet.update() return viewlet
There’s something fishy about having to explicitly call
render(). I believe that if I could have gotten to viewlets to work as proper content providers, this wouldn’t be a problem (the. In spite of that, it’s pretty simple and it works, so I’m not complaining (much).
Now that the viewlets are available in a proper manner (and by proper I mean working), I can add them to my header template.
<div id="my_header"> <div id="main"> <div id="left_side"> <tal:site-actions replace="structure view/plone.site_actions/render" /> </div> <div id="right_side"> <div tal:replace="structure view/plone.logo/render" /> </div> </div> <br /> <div> <div id="personal_bar"> <tal:global-tabs replace="structure view/plone.personal_bar/render" /> </div> <div id="sections"> <tal:global-tabs replace="structure view/plone.global_sections/render" /> </div> </div> <br /> <div id="crumbtrail"> <tal:global-tabs replace="structure view/plone.path_bar/render" /> </div> </div>
After restarting Zope, here’s what the main page looks like:
Things aren’t terribly different. This is, obviously, because there isn’t any CSS contoling how things are layed out. But because of the way the divs break things up, I’ll be able to put things in their place quite easily.
Once everything is styled, I’ll use GenericSetup to hide the viewlets/managers that are redundant now.