Creating a Custom theme for Plone

From ThemesWiki

Jump to: navigation, search
Plone
Official Page
Project Documentation
Download
Source Book
Professional Plone Development
Professional Plone Development
ISBN 978-1-847191-98-4
Publisher Packt Publishing
Author(s) Martin Aspeli

Plone's default look and feel is best described as functional. It has evolved over several years to facilitate a wide range of features and user interface metaphors. If Plone is being deployed as an internal content management solution, it is probably a good choice. However, custom branding is normally required for public-facing systems such as the Optilux website.

In this tutorial, we will demonstrate how to create a new theme for Plone. We will add a new style sheet to change the visual appearance of various parts of the Plone site, change the default page layout, and customize a few templates to provide alternative markup where necessary.

Contents

[edit] Background

As we know, the portal_skins tool manages a list of skin layers, which contain templates, style sheets, images, and other resources. Skin layers are collected and ordered in themes (skins). The order is important, since items in skin layers nearer to the top of the list can override more general items from skin layers further down.

The process of building a custom look and feel for a site typically begins by defining a new theme with one or more skin layers near the top of the list providing theme-specific graphics and style sheets, as well as overrides for standard Plone templates. As we will see later in this tutorial, it is also possible to tie specific versions of Zope 3-style views and viewlets to a theme. Viewlets are fragments that provide different elements of the page, such as the logo or the row of tabs across the top of the site. Viewlets can be hidden or re-ordered on a theme-by-theme basis as well.

By keeping all our registrations and overrides collated into a single theme, we can easily switch back to the default look and feel. The theme's package is also the natural home for all customized templates, making it easier to keep track of which parts of Plone have been overridden for a particular application.

[edit] The Theme Package

As with the optilux.policy product, we will use paster to create the package containing the theme. This time, we will use the plone3_theme template. Go to the src/ directory of our buildout, and run:

 $ paster create -t plone3_theme optilux.theme

Enter optilux as the namespace of the package, and theme as the package name. The skin name should be Optilux Theme, the skin base should be left as Plone Default, and the zip safe flag should be set to False. The answers to the other questions are less important unless you plan to release the theme to the Python Cheese Shop, in which case you should enter a contact name and email address, as well as the package's license.

There should now be a new egg in src/optilux.theme. We need to add this to our buildout, so edit buildout.cfg, and add the following:

 [buildout]
 ...
 develop =
  src/optilux.policy

src/optilux.theme
 ...
 eggs =
  optilux.policy

optilux.theme
 ...
 [instance]
 zcml = optilux.policy

Then re-run buildout with:

 $ ./bin/buildout -o

This should make the package available in the build environment. For the package to work, we also need to tell Zope to read its configure.zcml file at startup. We could install a ZCML slug as we did for optilux.policy, but it is easier to let the policy product orchestrate all the other custom products we use. Therefore, we will include the optilux.theme package as a dependency of optilux.policy. In src/optilux.policy/optilux/policy/configure.zcml, we have the following:


 <configure
  xmlns="http://namespaces.zope.org/zope"
  xmlns:five="http://namespaces.zope.org/five"
  xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
  i18n_domain="optilux.policy">
 
  <!-- Include direct package dependencies -->

<include package="optilux.theme" />
  ...
 
 </configure>

If we now start Zope, the optilux.theme package should show up in the Product Management screen, under the Control_Panel in the ZMI root.

We must also ensure that the theme product is installed automatically when the policy product is installed. We will add the following near the top of src/optilux.policy/optilux/policy/Extensions/Install.py:

 PRODUCT_DEPENDENCIES = ('RichDocument',
  'optilux.theme',)

[edit] Adding a Test to the Policy Product

There should be a test in optilux.policy to ensure that the theme is installed properly when the policy product is installed. In optilux.policy, we must first add the following line to tests/base.py:

 from Products.Five import zcml
 from Products.Five import fiveconfigure
 
 from Testing import ZopeTestCase as ztc
 
 from Products.PloneTestCase import PloneTestCase as ptc
 from Products.PloneTestCase.layer import onsetup
 
 ztc.installProduct('SimpleAttachment')
 ztc.installProduct('RichDocument')
 
 @onsetup
 def setup_optilux_policy():
  fiveconfigure.debug_mode = True
  import optilux.policy
  zcml.load_config('configure.zcml', optilux.policy)
  fiveconfigure.debug_mode = False
 
  ztc.installProduct('optilux.policy', package=True)

ztc.installProduct('optilux.theme', package=True)
 
 setup_optilux_policy()
 ptc.setupPloneSite(products=['optilux.policy'])
 
 ...

This ensures that after the optilux.policy ZCML file which includes optilux.theme has been loaded, we install optilux.theme as a product. Without this, the installation of optilux.theme from the optilux.policy Install.py file would fail.

The test itself, found in optilux.policy.tests.test_setup.py, is simple:

  def test_theme_installed(self):
  skins = getToolByName(self.portal, 'portal_skins')
  layer = skins.getSkinPath('Optilux Theme')
  self.failUnless('optilux_theme_custom_templates' in layer)
  self.assertEquals('Optilux Theme', skins.getDefaultSkin())

[edit] Theme Product Contents

Let us take a look at the contents of our new theme product. As with all egg-based packages, there is a setup.py file in the root directory, used to pull in dependencies and provide release metadata. The real code lives in src/optilux.theme/optilux/theme, and includes the following files and directories.

  • The configure.zcml file includes another file called profile.zcml which registers the GenericSetup profile used to install the product and the browser sub-package, where Zope 3-style browser views, viewlets, and portlets will be registered.
  • The GenericSetup extension profile is found in profiles/default as usual. It installs the new theme and registers its style sheet. There are also example files for registering new Java Script resources and re-ordering or hiding viewlets. We will cover the latter in more detail below.
  • The file setuphandlers.py contains a blank setupVarious() method, which can be used to perform additional setup operations using Python code when the theme product is installed. We will not need this.
  • In browser/interfaces.py, there is an interface called IThemeSpecific, which is associated with the Optilux Theme theme in browser/configure.zcml. We can reference this interface from the layer attribute of various ZCML directives to register visual components as specific to this theme. More on that later in this tutorial.
  • The browser/ directory also includes two sub-directories that are registered as resource directories in browser/configure.zcml: stylesheets and images. We will see how to use these shortly.
  • The skins directory houses various custom skin layers, which are installed in the portal_skins tool and associated with the Optilux Theme theme.

By default, there are three theme-specific skin layers: one for style sheets (optilux_theme_styles), one for images (optilux_theme_custom_images), and one for templates (optilux_theme_custom_templates). The separation into several skin layers is arbitrary, but encourages good practice.


It is a good idea to keep customized (overridden) resources separate from bespoke ones. This makes it easier to keep track of what you have changed from an out-of-the-box Plone installation, which is especially useful if you upgrade Plone in future. When building a new theme, you should generally prefer to create images, style sheets, and templates as Zope 3-style browser resources in the browser sub-package, using the theme-specific skin layers to override existing skin layer-based resources only.


If you install the Optilux theme using the Add-on products control panel in Plone, you should see the three new layers installed in portal_skins in the ZMI. Take a look at profiles/default/skins.xml to better understand how they are configured.

[edit] Tools and Techniques

When working on a theme product, make sure that Zope is running in debug mode. This is set in zope.conf. You must have added the appropriate configuration to buildout.cfg to ensure debug mode is enabled in our development environment. When Zope is in debug mode, you can modify resources in skin layers without having to re-start Zope. The same goes for Zope 3-style templates (but not view classes: as a rule of thumb, anything in a skin layer can be modified at run time while in debug mode, as can any page template file (.pt) on the file system).

When working with CSS, you should also go into the portal_css tool in the ZMI and enable Debug/development mode. When this is off as it should be in a production scenario CSS files may be merged and cached to improve performance. During development, this can mean that changes do not get properly propagated to the browser.


The portal_css tool has two siblings: portal_javascripts, and portal_kss. Both of these support the same debug mode feature.

Finally, you should get Mozilla Firefox and the Firebug extension (http://getfirebug.com). Firebug is an incredible productivity tool for web developers, allowing you to inspect any element in the page, determine which CSS styles are being applied to it, experiment with changes to styles and structure live, watch HTTP traffic, profile your pages for performance problems, and much more.

[edit] Building the Theme

Most themes begin life as a design mockup. Developers then break visual elements of the design up into small image files used as backgrounds and fillers, or represent them using stylized HTML. This process gets easier with experience, as you learn to visualize how a page can most easily be represented with HTML and CSS. It often helps to draw guidelines on the mockup to identify how it can be represented as floating boxes, tables, and lists.

Let us take a look at the mockup. We have annotated this to pick out some standard design features:

Here is a picture of Plone's default look and feel, with the same features highlighted:

As far as possible, we will aim to re-use these elements, styling them as necessary.

Plone's standard markup is almost entirely structural. Colors, borders, spacing, and positioning are all controlled by CSS. Furthermore, each of the main page elements is managed by a viewlet. As we will see, it is possible to re-order, hide, and re-assign viewlets.

Thus, building a theme is usually an iterative process:

  • If your theme will deviate radically from Plone's look and feel, consider disabling one or more of Plone's public-facing style sheets. It is usually a good idea to keep the authoring style sheets intact, since reproducing the CSS for forms and other editing UI is a lot of work.
  • Add your own style sheet, images, and other resources, and apply them to the HTML markup that comes out of Plone to get as close to the desired look and feel as possible without changing any HTML.
  • Re-order, hide, or re-assign any viewlets for page elements that are not where you need them to be.
  • Consider creating new viewlets in the existing viewlet managers if you want to plug in some generic user interface elements.
  • If the viewlets infrastructure does not give you enough control over the page layout, consider customizing Plone's main_template. This page template is used by most views to invoke the page layout. Making radical changes to main_template can sometimes make it harder to upgrade in the future, so be careful, and document your changes if you go down this route.
  • If specific templates, views, or viewlets cannot be made to look the way you want with CSS, you can customize (override) them individually.

The remainder of this tutorial will follow the process above to achieve a final theme for the Optilux website. It can be seen in the image below, showing an almost fresh Plone instance with the policy product installed:

The author will not pretend to be a great web designer. We could do a lot more with the theme's CSS to make it look more professional, but we have opted to keep things reasonably simple for the purposes of this book.

[edit] Custom CSS Style Sheets

Plone's style sheets are managed by a tool in the ZMI called portal_css. It looks like this:

As mentioned, you should enable Debug/development mode during development. The Automatic grouping mode option can improve performance if you use a lot of custom style sheets, but introduces a small risk of style sheets being included in a different order to what the designer intended. Turn this on if it does not cause any problems for your theme.

The various style sheets listed will be collated (and possibly concatenated, if development mode is off) and rendered in the page sent to the web browser. The installer for the optilux.theme package will have added a new item called ++resource++optilux.theme.styles/main.css near the bottom of this list.


The name ++resource++optilux.theme.styles/main.css refers to the file main.css in the directory browser/stylesheets. This is registered as a resource directory with the name optilux.theme.styles, in browser/configure.zcml. You can add other style sheets here and refer to them in the same way if you need to.

Style sheets are rendered in order. This means that styles in later style sheets may override those in earlier style sheets, according to the usual CSS rules.

It may be educational to turn off one or more of the style sheets here and observe the effects on Plone in your browser. The first two style sheets in particular base.css and public.css contain styles for basic tags and public-facing page elements. Most theme authors prefer to re-use Plone's authoring.css and most of the other style sheets. Plone is a complex system, and styling it all from scratch can take a lot of time.

By default, a package generated using the plone3_theme template will have blank overrides for base.css and public.css in its styles skin layer, which is only installed for the new theme. These overrides apply only when the theme is selected.

This is useful if your theme deviates a lot from Plone's standard look and feel, since you will not need to counteract any of Plone's public-facing styles. For the purposes of the Optilux example, it was easier to let Plone's public styles apply, and override only when necessary. Therefore, we have removed these files from skins/optilux_theme_styles, making the skin fall back on Plone's defaults.

If Zope is running in debug mode (e.g. it was started with the command ./bin/instance fg), you should be able to make changes to browser/stylesheets/main.css, and see the changes immediately. You may need to perform a hard refresh each time, if your browser caches aggressively. Here is a short extract from the file:

 h1, h2 {
  border: none;
  border-bottom: solid transparent 1px;
 }

Consult a CSS reference to learn more about the rules of CSS. There are plenty of good references available online for free.

If you are inexperienced with CSS-based design, you will undoubtedly hit problems where things look great in one browser, but wrong in another. Unfortunately, web browsers have subtle bugs and inconsistencies that can cause a lot of pain for web developers. Worse still, the most popular browser Internet Explorer not only causes the most headaches, but also has the fewest free tools to help you debug problems.

Cross-browser web design is outside the scope of this book, but a few tips are in order:

  • The author prefers to work with Mozilla Firefox (and the Firebug extension) in the first instance, cross-checking with Safari and Internet Explorer regularly.
  • A lot of time has gone into making Plone's CSS as cross-browser friendly as possible. It usually makes sense to reuse styles from Plone where possible.
  • Plone has a special style sheet called IEFixes.css, which is conditionally included in main_template.pt. You may want to re-use or customize this for your own themes, if you need to make Internet Explorer-specific overrides.
  • If you ever find that floating elements disappear temporarily or permanently in Internet Explorer for no good reason, you may have been stung by the infamous peekaboo bug. Try to use the visualIEFloatFix class from Plone's CSS on the disappearing element, or its parent.

For more general tips and examples of how to deal with browser inconsistencies in your own templates, consult a good, modern web design guide. Again, there is a multitude of resources on the Internet, as well as a few documents at http://plone.org/documentation.

[edit] Using "base_properties" and DTML Variables in Style Sheets

You may have seen style sheet files in skin layers that have an extension of .dtml instead of .css. This extension is not carried forward to the resource name in Zope, but it signifies that the file should be processed as a DTML document. DTML is a legacy template language for Zope, largely superseded by Zope Page Templates (ZPT). It is still quite useful for non-XML files such as style sheets, however.

The only feature of DTML commonly used in style sheets is variable interpolation. For example, CMFPlone/skins/plone_styles/base.css contains:

 /* <dtml-with base_properties> (do not remove this :) */
 /* <dtml-call "REQUEST.set('portal_url', portal_url())"> */
 
 body {
  font: &dtml-fontBaseSize; <dtml-var fontFamily>;
  background-color: &dtml-backgroundColor;;
  color: &dtml-fontColor;;
  margin: 0;
  padding: 0;
 }
 
 ...
 
 /* </dtml-with> */

The dtml-with and dtml-call tags are ignored by CSS parsers, but will pull in various variables from a property sheet called base_properties.props. This contains definitions of standard colors, border styles, and fonts. There is a theme-specific customization of this file in the optilux_theme_styles layer, which looks like this:

 title:string=Optilux Theme's color, font, logo and border defaults
 
 plone_skin:string=Optilux Theme
 
 logoName:string=logo.jpg
 
 fontFamily:string="Lucida Grande", Verdana, Lucida, Helvetica, Arial, sans-serif
 fontBaseSize:string=69%
 fontColor:string=Black
 fontSmallSize:string=85%
 
 ...
 
 globalBorderColor:string=#8cacbb
 globalBackgroundColor:string=#f0f0f0
 globalFontColor:string=#436976
 
 ...
 
 contentViewBorderColor:string=transparent
 contentViewBackgroundColor:string=#f0f0f0
 contentViewFontColor:string=#fe9900
 
 ...
 
 portalMinWidth:string=70em
 columnOneWidth:string=181px
 columnTwoWidth:string=16em

We can bring the Plone color scheme in line with our site branding simply by editing this file.


Note that the main.css style sheet that we use for theme-specific styling is not a DTML document and thus does not support such variable interpolation.

[edit] Image Resources

The browser/images directory is analogous to the brower/stylesheets directory, intended to store theme-specific images. Both are registered as resource directories in browser/configure.zcml like this:

 <!-- Resource directory for stylesheets -->
 <browser:resourceDirectory
  name="optilux.theme.stylesheets"
  directory="stylesheets"
  layer=".interfaces.IThemeSpecific"
  />
 
 <!-- Resource directory for images -->
 <browser:resourceDirectory
  name="optilux.theme.images"
  directory="images"
  layer=".interfaces.IThemeSpecific"
  />

To render an image myimage.jpg placed in the images directory using a standard HTML image tag, we could use a construct such as:

 <img tal:attributes="src portal/++resource++optilux.theme.images/myimage.jpg" />

Here, portal is a globally available variable, which refers to the portal root object. If for some reason this is not available, we could have used the context variable instead. However, it is generally preferable to render image resources using a fixed path relative to the portal root, as this makes it easier for browsers and proxies to cache the image.

In main.css, we use an image in the resource directory as the background of the left-hand side column:

 #portal-column-one {
  background-image: url(++resource++optilux.theme.images/ column-one-bg.jpg);
  background-position: top-left;
  background-repeat: no-repeat;
  background-color: #f0f0f0;
  padding-top: 60px;
 }

Since the style sheet is always rendered with the same path via the portal_css tool, we do not need to explicitly reference the portal root here.

[edit] Managing Viewlets

Plone 3 uses viewlets to manage various standard page elements, such the logo, the navigation breadcrumbs, and the personal bar. Viewlets are registered to a viewlet manager, which controls which viewlets are displayed, and in which order they are rendered.

To see which viewlet managers and viewlets are used to compose a page, append /@@manage-viewlets to a URL.


Note that this only works with viewlet managers based on the plone.app.viewletmanager package. All of Plone's viewlet managers use this, but third-party products may come with simpler managers that sort viewlets alphabetically or according to some other means.

Here is how it looks:

You can see the name of each viewlet manager and its viewlets. Use the arrows and the hide link to re-order and hide viewlets. Any changes will be persisted in the ZODB immediately.

Of course, we should make changes in a repeatable, controllable way, and not rely on settings stored only in the ZODB. Therefore, we use a GenericSetup import step, found in profiles/default/viewlets.xml:

 <object>
 
  ...
 
  <!-- Header - contained within plone.portaltop -->
  <order manager="plone.portalheader" skinname="Optilux Theme"
  based-on="Plone Default">
  <viewlet name="plone.skip_links" />
  <viewlet name="plone.site_actions" />
  <viewlet name="plone.logo" />
  <viewlet name="plone.global_sections" />
  </order>
 
  <hidden manager="plone.portalheader" skinname="Optilux Theme">
  <viewlet name="plone.searchbox" />
  </hidden>
 
  ...
 
 </object>

The <order /> directive can be used to define the order of viewlets in a manager for a particular theme (specified with skinname). You can get the name of the viewlet manager, set in the manager attribute, from the @@manage-viewlets screen. The based-on attribute can be used to copy the default settings from another skin usually "Plone Default".

It is often easiest to list all the eligible viewlets in order, as we have done above. However, if you want to specify the order of a viewlet relative to an existing one, you can use a line such as:

 <viewlet name="my.viewlet" insert-before="another.viewlet" />

You can write insert-before="*" to place a viewlet first in the list, or use insert-after to place a viewlet after another one.

The <hidden /> directive is used to hide (disable) viewlets in a particular viewlet manager. Again, this is tied to a specific skinname, and you can use a based-on attribute to copy settings from an existing theme.

[edit] Defining Viewlet Managers

Viewlet managers are declared in ZCML. All of Plone's standard viewlet managers are in the plone.app.layout.viewlets package, where you will see lines such as this in configure.zcml:

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

The interface is defined in plone.app.layout.viewlets' interfaces.py file:

 from zope.viewlet.interfaces import IViewletManager
 
 class IPortalTop(IViewletManager):
  """A viewlet manager that sits at the very top of the rendered page  """

We will see in a moment how this is used to tie viewlet registrations to specific managers.

Plone's main_template, found in CMFPlone/skins/plone_templates, asks the viewlet manager to render itself, with a line such as:

 <div tal:replace="structure provider:plone.portaltop" />

You can use similar constructs in your own themes to define new viewlet managers. Of course, a viewlet manager does not need to be rendered in main_template it could be invoked from any page template, such as the view of a particular content object.

[edit] Reassigning Viewlets

For the Optilux theme, we need to move the breadcrumbs and personal bar from plone.portaltop manager down to the plone.contentviews manager, which is inside the main content area.

We cannot reassign viewlets between portlet managers using viewlets.xml, but we can re-register a viewlet for a new manager. In browser/configure.zcml in the optilux.theme package, we have:

 <browser:viewlet
  name="optilux.personal_bar"
  manager="plone.app.layout.viewlets.interfaces.IContentViews"
  layer=".interfaces.IThemeSpecific"
  class="plone.app.layout.viewlets.common.PersonalBarViewlet"
  permission="zope2.View"
  />

This is taken from the corresponding statement in the plone.app.layout.viewlets configure.zcml file. We have given it a new name, and registered it for a different viewlet manager interface, but we still reference the same implementation, using the class attribute with an absolute package name.

To ensure that this registration only affects the Optilux theme, we use the layer attribute to reference the IThemeSpecific interface. This interface is registered earlier in browser/configure.zcml, with a block of code generated by the plone3_theme template:

 <interface
  interface=".interfaces.IThemeSpecific"
  type="zope.publisher.interfaces.browser.IBrowserSkinType"
  name="Optilux Theme"
  />

The name here should match our theme name.

Finally, we hide the viewlet in its original viewlet manager otherwise, we would get the personal bar twice. This is done in viewlets.xml:

  <hidden manager="plone.portaltop" skinname="Optilux Theme">
  <viewlet name="plone.app.i18n.locales.languageselector" />
  <viewlet name="plone.path_bar" />

<viewlet name="plone.personal_bar" />
  </hidden>

[edit] Creating New Viewlets

Let us now add a brand-new viewlet. We will do so using a page template only. The new viewlet must be registered in browser/configure.zcml:

 <browser:viewlet
  name="optilux.footer"
  manager="plone.app.layout.viewlets.interfaces.IPortalFooter"
  layer=".interfaces.IThemeSpecific"
  template="templates/footer.pt"
  permission="zope2.View"
  />

We have to specify a unique name, the interface for the viewlet manager we want it to be displayed in, and a required permission. As before, the layer attribute ensures that this viewlet only shows up for this particular theme. We then reference a page template file called footer.pt in a newly created templates directory, which contains:

 <div class="discreet">
  All content copyright Optilux Corporation.
 </div>

To learn more about TAL and Zope Page Templates, see http://plone.org/documentation/tutorial/zpt.

If we needed to, we could have added an <order /> directive to viewlets.xml to control the appearance of the new viewlet more precisely.

[edit] Overriding Visual Elements

The relative ease with which Plone's visual elements can be customized without having to modify Plone's own source code is one of the greatest advantages in using Plone as a development platform. Broadly speaking, visual elements can come from one of these four different sources:

  • A page template, style sheet, script, or other file in a skin layer. This is the most common type of resource (although Zope 3-style browser views and viewlets are becoming more and more prevalent). These can be found in portal_skins in the ZMI. Plone's standard skins are mostly in CMFPlone/skins, though other packages provide their own skin layers too.
  • A Zope 3 browser view. These are registered in a configure.zcml file, using a <browser:page /> directive. When invoked, they are normally prefixed with @@ (e.g. the manage-viewlets view is normally seen at the end of a URL as /@manage-viewlets). Views consist of a Python class and/or a page template, and live on the file system only. They are typically defined in the same package as the functionality they relate to. The portal_view_customizations tool provides a listing of most registered browser views (and allows them to be customized through the Web).
  • A Zope 3 viewlet is similar to a browser view in that it may consist of a class and/or a template. Viewlets are registered for a particular viewlet manager. Most of Plone's standard viewlets are defined in the plone.app.layout package, in the viewlets module.
  • Portlets have renderers, which are similar to viewlets in that they render a part of the page. However, portlet renderers are aware of additional components such as their associated portlet data provider holding configuration data (settings made from the Manage portlets screen). Portlets are rendered inside portlet managers, which are similar to viewlet managers. All of Plone's standard portlets are defined in the plone.app.portlets package, under the portlets module.

Apart from resources in skin layers, overrides for visual components involve a registration in ZCML, and either a template or a class containing the actual customizations. We will focus on template customizations in this tutorial.

As we know, blanket overrides can be made using an overrides.zcml file. However, it is usually preferable to provide overrides for a particular theme (using the layer attribute as we have seen already), or perhaps for a particular type of context content object (using the for attribute).

Note that for Zope to process an overrides.zcml in the root of your package, you will need a ZCML slug. In buildout.cfg, this would mean a line like zcml = my.package-overrides under the [instance] section.

[edit] Templates and Other Resources in Skin Layers

We have already seen how to override skin layer resources by using skin layers higher up. Again, it is a good idea to keep track of any customized elements separately from new page templates, style sheets, and images.

The login, logout, and signup templates all look a little strange under the Optilux theme, because they clear the left-hand side column. Therefore, we will copy the relevant templates from CMFPlone/skins/plone_login into the optilux_theme_custom_templates skin layer folder.

Each of these templates has an associated .metadata file, which we must also copy. For example, join_form.cpt.metadata is associated with join_form.cpt. These files contain page flow information, though they can also contain page titles as well as cache and security settings.

Page templates, images, and style sheets in skin layers may have associated metadata files. These have the same name as the file, with a suffix of .metadata. Zope will only be able to find these if they are in the same directory as the file they refer to.

The actual template customizations we require are quite simple. For example, in login_form.cpt, we have commented out this line:

Before it was commented out, this statement would fill the column_one_slot in main_template with an empty tag, essentially removing the table cell that houses the left-hand side column. Of course, if it were necessary, we could have customized the template much more extensively.

[edit] Zope 3-Style Browser Views

Zope 3 browser views are typically rendered with page templates as well. However, such templates live in a folder on the file system conventionally in a sub-package called browser in the package the view belongs to not in a skin layer in portal_skins. We will use a ZCML declaration that ties the skin to our particular theme (using the layer attribute as seen earlier) to customize these.

We would like staff members to be able to use Plone's Dashboard to manage personal portlets. However, like the login-related templates alrealdy mentioned, these clear the left and right columns to utilize the full width of the screen. We will customize them to clear the right column only.

The two views we want to customize are @@dashboard (the main dashboard view), and @@manage-dashboard (the edit tab for the dashboard). One way to discover which packages these templates are defined in is to look in the portal_view_customizations tool in the ZMI. Hover your mouse cursor over the name of a view, and you should see the package and file name.

Alternatively, you can search Plone's source code for files with extension .zcml containing the view names.

In this case, the views are defined in two different packages: plone.app.layout.dashboard and plone.app.portlets.browser. First, plone.app.layout.dashboard's configure.zcml file contains this:

 <browser:page
  for="Products.CMFCore.interfaces.ISiteRoot"
  name="dashboard"
  permission="plone.app.portlets.ManageOwnPortlets"
  class=".dashboard.DashboardView"
  template="dashboard.pt"
  />

Second, plone.app.portlet.browser's configure.zcml file has:

 <browser:page
  for="Products.CMFCore.interfaces.ISiteRoot"
  class=".manage.ManageDashboardPortlets"
  name="manage-dashboard"
  template="templates/manage-dashboard.pt"
  permission="plone.app.portlets.ManageOwnPortlets"
  />

These reference both a template and a class. Some views do not need a companion class to manage their display logic, and so only use a template. Others either do not use page templates for rendering, or explicitly invoke a template from their view class. To customize the two views, we will add the following to optilux.theme.browser's configure.zcml file:

 <browser:page
  for="Products.CMFCore.interfaces.ISiteRoot"
  name="dashboard"
  permission="plone.app.portlets.ManageOwnPortlets"
  class="plone.app.layout.dashboard.dashboard.DashboardView"
  template="templates/dashboard.pt"
  layer=".interfaces.IThemeSpecific"
  />
 
 <browser:page
  for="Products.CMFCore.interfaces.ISiteRoot"
  name="manage-dashboard"
  permission="plone.app.portlets.ManageOwnPortlets"
  class="plone.app.portlets.browser.manage.ManageDashboardPortlets"
  template="templates/manage-dashboard.pt"
  layer=".interfaces.IThemeSpecific"
  />

In both cases, we use layer=".interfaces.IThemeSpecific" to provide an override for this particular theme only, and keep the name, for, and permission attributes the same.

In the class attribute, we use an absolute dotted name to reference the original class containing the display logic used by the template. We could of course have used a custom class (perhaps a subclass of the original one) or no class at all. In this case, we want to use a template that is very similar to the original, so it makes sense to tie this to the default view class.

The two templates dashboard.pt and manage-dashboard.pt have been copied into the browser/templates directory in the optilux.theme package, and modified in a way analogous to the customizations for the login templates, by commenting out this line:

 <!-- <metal:left fill-slot="column_one_slot" /> --> 

Again, we could have modified the templates much more extensively if it were necessary.

[edit] Viewlets

The mechanism for overriding viewlets is similar to that for overriding views. To change the rendering of the breadcrumbs, we have the following registration in browser/configure.zcml:

 <browser:viewlet
  name="optilux.path_bar"
  manager="plone.app.layout.viewlets.interfaces.IContentViews"
  layer=".interfaces.IThemeSpecific"
  class=".viewlets.PathBarViewlet"
  permission="zope2.View"
  />

Since we hide the default plone.path_bar viewlet in viewlets.xml, we can register this one with a unique name to make it clear that it is different. To ensure that the viewlet only shows up for the Optilux theme, we use the same layer specification as seen before. We then reference a class that is responsible for updating and rendering the viewlet, found in browser/viewlets.py:

 from zope.component import getMultiAdapter
 from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
 from plone.app.layout.viewlets import common
 
 class PathBarViewlet(common.PathBarViewlet):
  """A custom version of the path bar (breadcrumbs) viewlet, which
  uses slightly different markup.
  """
 
  render = ViewPageTemplateFile('templates/path_bar.pt')
 
  # The update() method, inherited from the base class, takes care
  # of initializing various variables used in the template

This is simply an extension of the equivalent class from plone.app.layout.viewlets, referencing a different template as its render() method. The template path_bar.pt lives in the templates directory, and is referenced with a relative path. It was originally copied from plone.app.layout.viewlets.

 <div id="portal-breadcrumbs"
  i18n:domain="plone">
 
  <span id="breadcrumbs-you-are-here" i18n:translate="you_are_here">You are here:</span>
  <a id="breadcrumbs-home" i18n:translate="tabs_home"
  tal:attributes="href view/navigation_root_url">Home</a>
  <span tal:condition="view/breadcrumbs" class="breadcrumbSeparator">
  /
  </span>
  <span class="breadcrumbItem"
  tal:repeat="crumb view/breadcrumbs"
  tal:attributes="dir python:view.is_rtl and 'rtl' or 'ltr'">
  <tal:last tal:define="is_last repeat/crumb/end">
  <a href="#"
  tal:omit-tag="not: crumb/absolute_url"
  tal:condition="python:not is_last"
  tal:attributes="href crumb/absolute_url"
  tal:content="crumb/Title">
  crumb
  </a>
  <span class="breadcrumbSeparator" tal:condition="not: is_last">
  /
  </span>
  <span tal:condition="is_last"
  tal:content="crumb/Title">crumb</span>
  </tal:last>
  </span>
 
 </div>

Here, we have added a few CSS IDs and classes, which are used in the main.css style sheet. We have also changed the separator from an arrow to a forward slash.

[edit] Portlets

Finally, we will customize the view of the search portlet, which we will use in the left-hand side column to display a search box. In configure.zcml, we have:

 <configure
  xmlns="http://namespaces.zope.org/zope"
  xmlns:browser="http://namespaces.zope.org/browser"
  xmlns:plone="http://namespaces.plone.org/plone"
  i18n_domain="optilux.theme">
 
  <include package="plone.app.portlets" />
 
  ...
 
  <plone:portletRenderer
  portlet="plone.app.portlets.portlets.search.ISearchPortlet"
  layer=".interfaces.IThemeSpecific"
  template="templates/search_portlet.pt"
  />
 
 </configure>

We have to make sure that we include the plone XML namespace as well as the more common zope and browser ones. We also include the plone.app.portlets package, to make sure that its ZCML directives are processed before the new renderer is defined. This is because the <plone:portletRenderer /> directive makes use of the initial portlet registration to provide some defaults for the new renderer. This saves a lot of typing, and reduces the chance of subtle errors.

The <plone:portletRenderer /> directive references a portlet by its interface. All of Plone's standard portlets are found plone.app.portlets.portlets. As before, we specify a layer, and then reference a template.

Unlike the <browser:page /> and <browser:viewlet /> directives, we will actually get the original portlet renderer class if we do not specify a class directly. This is available to the template using the implicit variable view. The template, search_portlet.pt, is based on search.pt in plone.app.portlets.portlets, and looks like this:

 <dl class="portlet portletSearch"
  i18n:domain="plone">
 
  <dt class="portletHeader">
  <span class="portletTopLeft"></span>
  <a class="tile"
  tal:attributes="href view/search_form"
  i18n:translate="box_search">Search</a>
  <span class="portletTopRight"></span>
  </dt>
 
  <dd class="portletItem odd">
  <form name="searchform" action="search"
  tal:define="livesearch view/enable_livesearch;"
  tal:attributes="action view/search_action">
  <div class="LSBox">
  <input class="portlet-search-gadget"
  name="SearchableText"
  type="text"
  size="15"
  title="Enter search query"
  i18n:attributes="title title_search_title;"
  tal:attributes="value request/SearchableTest|nothing;
  class python:livesearch and 'inputLabel portlet-search-gadget' or 'inputLabel portlet-search-gadget-nols'"
  class="inputLabel" />
  <div class="LSResult" style="" tal:condition="livesearch">
  <div class="LSShadow"></div>
  </div>
  </div>
  </form>
 
  <dd class="portletFooter">
  </dd>
 </dl>

This is mostly a simplification of the original template.

[edit] Summary

In this tutorial, we have learned:

  • How to create a new package for a custom theme and integrate it into our build environment
  • How to put Zope and the portal_css tool into development mode
  • That Firebug is the greatest thing since sliced bread
  • How to apply new CSS in a theme
  • How to re-order, hide, and register new viewlets
  • How to override templates from CMF skin layers
  • How to override Zope 3-style views
  • How to override the rendering of a viewlet
  • How to customize the rendering of a portlet

These constitute the main techniques for adding a custom look and feel to Plone. If you have not done so already, it is probably instructive to look at the code in the optilux.theme package in detail to understand how the various pieces fit together.

We have not spent a lot of time discussing fundamental web technologies such as HTML and CSS. Demonstrating how to create usable, accessible, maintainable, standards-compliant, and cross-browser compatible designs with HTML and CSS deserves a book in its own right. There are plenty of good resources online as well.

If you are the type of web developer who uses a <font /> tag every now and then, manages all layout with <table />s and is not quite sure how to float a box to the right of a block of text, you owe it to yourself to spend some time with a quality CSS reference.

We have also not spent much time on the syntax of Zope Page Templates (ZPTs), their Template Attribute Language (TAL), and their Macro Extensions (METAL). If you are not familiar with these, please see the ZPT tutorial at http://plone.org/documentation/tutorial/zpt. The documentation section on plone.org also features tutorials, and useful "how-to" documents describing approaches to theming, and visual customizations.

[edit] Additional References

For instructions on installing Plone, click here

[edit] Source

The source of this content is Chapter 8:Creating a Custom Theme of Professional Plone Development by Martin Aspeli Packt Publishing, 2008.