Changing Plone theme/skin based on portal_type

By Roel Bruggink | On Aug 04, 2009
Changing your Plone theme/skin based on the object's portal_type using a skin switcher.

Update

I have updated this post to fix some issues. There where problems with VHM style virtual hosting (because of the /virtual_hosting added to TraversalRequestNameStack) and I fixed my catalog query (shame on me).

As Deichi pointed out, you need Plone >= 3.1 because of IThemeSpecific. We have tested this on Plone 3.2.2 and 3.2.3, but it should work on Plone >= 3.1.

Objective

I have a folder structure like (section)[/random]/(department)[/random]/(project)[/random], and the folders project, department and district need to have a different theme. This sounds like a fairly easy task, but that couldn't be futher away from the truth.

Problems all over

According to Maurits' blog on Skin switching and Weblions' "Theme switching by URL, folder, page, etc.", IBeforeTraverseEvent should be used. I thought it would give me every object in the url, but you guessed it: it didn't. It just gave me the Plone Site object. I then implemented __bobo_traverse__ in the contenttypes. Either way, things like browser resources didn't work because changeSkin just changes the theme for the request it's in. And because everything inside the themes is IThemeSpecific and the currently active theme is a different theme, I got error 404 galore.

Solution

Using IBeforeTraverseEvent is the way to go to hijack every request. We need to do that, so portal_css and portal_js also works like expected. We need to take three steps:

In configure.zcml:
1. Configure a event handler

<subscriber
for="plone.app.layout.navigation.interfaces.INavigationRoot
zope.app.publication.interfaces.IBeforeTraverseEvent"
handler=".skinswitcher.setskin" />

In skinswitcher.py:
2. Define the event handler that sets the theme.

def setskin(site, event):
"""Eventhandler to set the skin"""
skin_name = findMeMySkin(site, event.request)
portal = getToolByName(site, 'portal_url').getPortalObject()
portal.changeSkin(skin_name, event.request)

3. And we need some code to find out which theme should be used.

def findMeMySkin(site, request):
# define possible skins
PROJECT_THEME = 'Project Plone Theme'
AFDELING_THEME = 'Afdeling Plone Theme'
DISTRICT_THEME = 'District Plone Theme'
DEFAULT = 'Plone Tableless'
skins=[PROJECT_THEME, AFDELING_THEME, DISTRICT_THEME, DEFAULT]

# map portal_type to theme
mapping={
'Project': PROJECT_THEME,
'Afdeling': AFDELING_THEME,
'District': DISTRICT_THEME,
}

if not request.TraversalRequestNameStack:
return DEFAULT

# reverse to look from the root up
stack=[]
stack.extend(request.TraversalRequestNameStack)
stack.extend(site.getPhysicalPath())
stack=[x for x in stack if x and x!='/' and x!='virtual_hosting']
stack.append('')
stack=stack[::-1]

# support portal_css and portal_js
for item in stack:
if item in skins:
return item

# check objects
portal_catalog = getToolByName(site, 'portal_catalog')
while stack:
item=stack.pop()
path = '/'.join(stack)

query={}
query['id']=str(item)
query['path'] = {'query' : path}

brains = portal_catalog.unrestrictedSearchResults(query)
if brains:
brain=brains[0]
portal_type=brain.portal_type
try:
return mapping[portal_type]
except:
pass

return DEFAULT