Adventures In Theming: Rolling My Own Viewlet Manager

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 browser directory.

The Class

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.

src/my.theme/my/theme/browser/viewletmanager.py

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.

The Interface

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.

src/my.theme/my/theme/browser/interfaces.py

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 ZCML

The real heavy lifting in setting up a viewlet manager happens via ZCML, in the <browser:viewletManager> tag.

src/my.theme/my/theme/browser/configure.zcml

...
<!-- Viewlets registration -->
  <browser:viewletManager
     name="my.header"
     provides=".interfaces.IMyTop"
     class=".viewletmanager.MyHeaderViewletManager"
     layer=".interfaces.IThemeSpecific"
     permission="zope2.View"
     template="myheader.pt"  
     />
... 

The 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).

The 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 my.theme.browser.interfaces.IThemeSpecific.

Finally, the template directive lets you specify a zope page template that will be used to render the viewlet manager.

The Template

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.

src/my.theme/my/theme/browser/myheader.pt

<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.

First Test

At this point, I’ve got a working custom viewlet manager. Assuming you’ve already run bootstrap.py and bin/buildout, running 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.

Altering main_template

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:

  1. Fist make sure that your Plone instance is up and running.

  2. 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).

  3. 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

  4. 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.

  5. Click the "Customize" button. This will copy the template into the custom folder, 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 parts/plone/CMFPlone/skins.

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.

The 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:

The viewlet manager is ALIVE!

The viewlet manager is ALIVE!

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 <browser:viewletManager> tag.

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.

src/my.theme/my/theme/browser/configure.zcml

...
<!-- 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) view/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 render() method.

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.

src/my.theme/my/theme/browser/viewletmanager.py

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 update() and 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.

src/my.theme/my/theme/browser/myheader.pt

<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:

Is that double vision? Nope!

Is that double vision? Nope!

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.

Advertisements
This entry was posted in Uncategorized and tagged . Bookmark the permalink.

3 Responses to Adventures In Theming: Rolling My Own Viewlet Manager

  1. Kevin says:

    I really wanted to just thank you for this great tutorial. You documented the whole process, and even went over things most people would have skipped over — but they proved extremely helpful to me. Thanks so much for your effort on this.

  2. alex says:

    Thank you for this wonderfull and specific tutorial. I added it to my blog, as a reference.

  3. Alex says:

    Thanks a lot man! I found a little mistake in interfaces.py. You have to change “class MyTop(IViewletManager):” to “class IMyTop(IViewletManager):”.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s