How to make a Plone theme available (for activation) after installation as an add-on

— 10 minute read

Adding a theme to your Plone site makes it available for activation imediately, for all Plone sites on the same Plone instance. This is not always the desired behavior. To require the theme to be installed as an add-on before we can activate it we have to change the way the registration is done.

In Plone we can distinguish between three types of themes: there are filesystem themes (or Python package themes), themes coming from a resource directory in the filesystem (resource directory themes) and ZODB themes (stored in the database). A ZODB theme can be stored in the database by either uploading a new theme as a zip file or by copying an existing (filesystem or ZODB) theme for customizations. ZODB and resource directory themes are not in our interest today, we will only focus on the filesystem themes, so when I say “theme” later in this article I am talking about a filesystem theme.

Note: ZODB themes are only available to one Plone site within a Plone installation, whereas filesystem themes and resource directory themes are available to all Plone sites in the same installation.

To make a (filesystem) theme available in Plone the first thing which needs to be done is installing it to the Plone server. This is done in the buildout configuration, and after the buildout command was run and the instance restarted, the theme is available within Plone.

[instance]
recipe = plone.recipe.zope2instance
http-address = 127.0.0.1:8080
eggs =
Plone
my.theme [test]

The problem permalink

By default themes can be activated as soon as they are available in the Plone site. This works well for most simple themes where all the assets and customizations are handled in the “theme” folder. Those themes are also save to install as a zip file, which removes the hassle to update the buildout configuration and restart the Plone server.

But with more advanced themes, which require customizations on the Python level, add new views, viewlets or portlets, activate add-on dependencies or register resources using the Plone resource registry, simply activating the theme in the theme control panel is not enough. Those themes need to be installed in the add-on control panel so that all features are available.

Note: There is also the collective.themesitesetup add-on which lets you run GenericSetup installations when the theme is activated, but it is not compatible with the latest Plone 5.2 running on Python 3.

So let’s assume we have such an advanced theme. We add it to the Plone instance in the buildout configuration, restart the server, activate the theme... and the site looks far from what we expect. Why? Because we forgot to install the corresponding theme add-on package. We as developer and maintainer of the theme know what to do, but other folks who want to install the theme may be stuck.

So why does the theme show up in the theming control panel when it needs to be installed to work correctly? The reason is the following ZCML declaration, which is added by default if you create your theme using bobtemplates.plone or the Plone CLI, or by following the official documentation.

<plone:static
directory="theme"
type="theme"
name="my-theme"
/>

This declaration registers the theme so that it can be activated in the theming control panel. But it does this on a global level (as a global utility in the global site manager). It is available as soon as the Plone server is started, for all Plone sites.

But we want the theme to show up in the theming control panel after the theme has been installed as an add-on. To achieve this, the first thing we have to do is to remove the declaration shown above from our configuration file.

Note: If you restart your Plone server now the theme will not be available anymore in the theming control panel.

Going local permalink

To add the theme to the list of available themes when we install the add-on (and remove it when we uninstall it again), we can use some helpers from GenericSetup: the pre_handler and post_handler hooks. Those are run right before the GenericSetup profile import is started (pre_handler) or when all import steps have been applied (post_handler). This can be used for the install and uninstall profile to execute custom code.

In those hooks we will do exactly what is done with the ZCML declaration from above. Except that we register the utility in the local site manager. This way the theme is only available to the site where it is installed in.

As a first step we have to add some required Plone and Zope imports:

from plone.resource.directory import FilesystemResourceDirectory
from plone.resource.interfaces import IResourceDirectory
from zope.component import getSiteManager

import os

To register the theme in the Plone site we need to add the pre_install handler function, which is called before the import steps of the profile are run. This function does the following:

  1. Get the local site manager of the current Plone site. During the installation, context is the current Plone site object.
  2. Get the path of the theme folder (which contains all the diazo resources) in our add-on. This is the directory attribute from the ZCML declaration above.
  3. Create a FilesystemResourceDirectory instance with the name of the theme.
  4. Register the utility in the local site manager with the theme identifier. This theme identifier needs to be in the format ++theme++ + the name of the theme, so in the following example ++theme++my-theme.
def pre_install(context):
"""Pre install script"""
# Do something at the beginning of the installation of this package.
sm = getSiteManager(context=context)
path = os.path.join(os.path.dirname(__file__), "theme")
directory = FilesystemResourceDirectory(path, "my-theme")
sm.registerUtility(directory, provided=IResourceDirectory, name="++theme++my-theme")

We have to use the pre_handler hook instead of the post_handler hook because we need to register the theme and make it available within Plone before other parts of the install process might want to access the theme resource (e.g. to activate the theme after the add-on is installed).

By default (when you create your add-on using the Plone CLI or bobtemplates.plone), the install profile for the theme already contains a declaration for the post_handler, but not for the pre_handler function:

<genericsetup:registerProfile
name="default"
title="my.theme"
directory="profiles/default"
description="Installs the my.theme add-on."
provides="Products.GenericSetup.interfaces.EXTENSION"
post_handler=".setuphandlers.post_install"
/>

Without the pre_handler attribute our code to register the theme will not be executed. To do so we have to add the line pre_handler=".setuphandlers.pre_install". When this is done the install profile declaration should look like this (yours might look slightly different):

<genericsetup:registerProfile
name="default"
title="my.theme"
directory="profiles/default"
description="Installs the my.theme add-on."
provides="Products.GenericSetup.interfaces.EXTENSION"
post_handler=".setuphandlers.post_install"
pre_handler=".setuphandlers.pre_install"
/>

When we now restart the Plone server we can check the theming control panel first. The theme should not be in the list of available themes (if it does, double check if the ZCML declaration for the theme has been removed). When we now install the theme in the add-on control panel and check the theming control panel again, the theme should show up in the list of available themes.

Note: Your theme might be automatically activated after installation. That is because of the theme.xml setting.

Cleanup time permalink

We did it! Our theme can now only be activated after it has been installed as an add-on. What is left to do now is to remove the theme registration when the theme add-on is uninstalled. To do so, we use the post_handler hook for the uninstall profile. The handler is executed after all other uninstall parts have been processed. So this is a save place to add our “un-registration”.

The code shown below does the following:

  1. Get the local site manager of the current Plone site. Here context again is the current Plone site object.
  2. Un-register the utility in the local site manager with the theme identifier (not the name).
def uninstall(context):
"""Uninstall script"""
# Do something at the end of the uninstallation of this package.
sm = getSiteManager(context=context)
sm.unregisterUtility(provided=IResourceDirectory, name="++theme++my-theme")

The corresponding profile registration in ZCML looks like this:

<genericsetup:registerProfile
name="uninstall"
title="my.theme (uninstall)"
directory="profiles/uninstall"
description="Uninstalls the my.theme add-on."
provides="Products.GenericSetup.interfaces.EXTENSION"
post_handler=".setuphandlers.uninstall"
/>

We don’t need to provide the FilesystemResourceDirectory instance when removing the utility registration from the local site manager. The name of the utility is enough information we have to provide.

When we now restart Plone and uninstall the theme in the add-on control panel, the theme registration is removed and the theme does not show up anymore in the theming control panel.

Wrapping up permalink

With those three changes (removing ZCML declaration, adding pre- & post install handler) we are now able to make our theme available for activation only if it has been installed as a package.

But there is one problem we didn’t cover (and where I don’t have a solution for right now): when the theme is copied and stored as a ZODB theme (for some customizations), we are able to uninstall the theme add-on, but the new customized copy of our theme is still available and can be activated. In my opinion this is a rare use case, and people who do it (should) know what they are doing.