2 Level Navigation In Plone 3

by Abdullah Zainul Abidin last modified Aug 06, 2009 06:14 PM
This How To shows how to modify a Plone 3 theme product so that the tabs would have 2 levels of navigation.

Purpose

This implementation is based on the Two Level Navigation In Plone. In the original how-to, the author customized the global_sections template which is deprecated for Plone 3 and onwards. This how-to shows how to do it by modifying the viewlet for the navigation and also uses subfolders as submenu. For more details about modifying the viewlet, please refer to the Overriding Template View how-to.

Prerequisites

This how-to makes the assumption that you already have a working Plone 3 theme product which you could modify to apply the menu layout.

Step by step

  1. First you have to have your theme product ready. If you do not know how to do this then please refer to How To Create a Plone 3 Theme Product on the Filesystem. For this example let's assume the theme product is called inigo.theme.

  2. Then inside your theme product you should have a directory called viewlet. I used this method with the idea that any modifications for the viewlets which you would like to do for your theme should be put in here.

  3. In the viewlet folder you should have 4 files:

    • __init__.py
    • common.py
    • configure.zcml
    • sections.pt
  4. Contents of those files should be:

    • __init__.py : empty file. Should be there just so that you could package the folder as part of your egg

    • common.py

      from zope.interface import implements, alsoProvides
      from zope.component import getMultiAdapter
      from zope.viewlet.interfaces import IViewlet
      from zope.deprecation.deprecation import deprecate

      from plone.app.layout.globals.interfaces import IViewView

      from AccessControl import getSecurityManager
      from Acquisition import aq_base, aq_inner
      from Products.CMFPlone.utils import safe_unicode
      from Products.Five.browser import BrowserView
      from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
      from Products.CMFCore.utils import getToolByName
      from Products.CMFPlone import utils
      from Products.CMFPlone.browser.navigation import get_url,get_id,get_view_url
      from cgi import escape
      from urllib import quote_plus
      from plone.app.layout.navigation.root import getNavigationRoot

      class ViewletBase(BrowserView):
      """ Base class with common functions for link viewlets.
      """
      implements(IViewlet)

      def __init__(self, context, request, view, manager):
      super(ViewletBase, self).__init__(context, request)
      self.__parent__ = view
      self.context = context
      self.request = request
      self.view = view
      self.manager = manager

      @property
      @deprecate("Use site_url instead. ViewletBase.portal_url will be removed in Plone 4")
      def portal_url(self):
      return self.site_url


      def update(self):
      self.portal_state = getMultiAdapter((self.context, self.request),
      name=u'plone_portal_state')
      self.site_url = self.portal_state.portal_url()

      def render(self):
      # defer to index method, because that's what gets overridden by the template ZCML attribute
      return self.index()

      def index(self):
      raise NotImplementedError(
      '`index` method must be implemented by subclass.')

      class GlobalSectionsViewlet(ViewletBase):
      index = ViewPageTemplateFile('sections.pt')

      def update(self):

      context_state = getMultiAdapter((self.context, self.request),
      name=u'plone_context_state')
      actions = context_state.actions()
      portal_tabs_view = getMultiAdapter((self.context, self.request),
      name='portal_tabs_view')
      self.portal_tabs = portal_tabs_view.topLevelTabs(actions=actions)

      selectedTabs = self.context.restrictedTraverse('selectedTabs')
      self.selected_tabs = selectedTabs('index_html',
      self.context,
      self.portal_tabs)
      self.selected_portal_tab = self.selected_tabs['portal']

      for tab in self.portal_tabs:
      if tab['id']!='Members':
      tab['subtab']=self.getsubtab(self.context,tab)
      else:
      tab['subtab']=[]

      def getsubtab(self,context,tab):
      query={}
      result=[]
      portal_properties = getToolByName(context, 'portal_properties')
      portal_catalog = getToolByName(context, 'portal_catalog')
      navtree_properties = getattr(portal_properties, 'navtree_properties')
      site_properties = getattr(portal_properties, 'site_properties')

      rootPath = getNavigationRoot(context)
      dpath='/'.join([rootPath,tab['id']])
      query['path'] = {'query' : dpath, 'depth' : 1}

      query['portal_type'] = utils.typesToList(context)

      sortAttribute = navtree_properties.getProperty('sortAttribute', None)
      if sortAttribute is not None:
      query['sort_on'] = sortAttribute

      sortOrder = navtree_properties.getProperty('sortOrder', None)
      if sortOrder is not None:
      query['sort_order'] = sortOrder

      if navtree_properties.getProperty('enable_wf_state_filtering', False):
      query['review_state'] = navtree_properties.getProperty('wf_states_to_show', [])

      query['is_default_page'] = False

      if site_properties.getProperty('disable_nonfolderish_sections', False):
      query['is_folderish'] = True

      # Get ids not to list and make a dict to make the search fast
      idsNotToList = navtree_properties.getProperty('idsNotToList', ())
      excludedIds = {}
      for id in idsNotToList:
      excludedIds[id]=1

      rawresult = portal_catalog.searchResults(**query)

      # now add the content to results
      for item in rawresult:
      if not (excludedIds.has_key(item.getId) or item.exclude_from_nav):
      id, item_url = get_view_url(item)
      data = {'name' : utils.pretty_title_or_id(context, item),
      'id' : item.getId,
      'url' : item_url,
      'description': item.Description}
      result.append(data)
      return result
    • configure.zcml (Make sure you change the inigo.theme part to the name of your theme product)

      <configure
      xmlns="http://namespaces.zope.org/zope"
      xmlns:five="http://namespaces.zope.org/five"
      xmlns:browser="http://namespaces.zope.org/browser">

      <browser:viewlet
      name="plone.global_sections"
      manager="plone.app.layout.viewlets.interfaces.IPortalHeader"
      class=".common.GlobalSectionsViewlet"
      permission="zope2.View"
      layer="inigo.theme.browser.interfaces.IThemeSpecific"
      />

      </configure>
    • sections.pt

      <tal:tabs tal:condition="view/portal_tabs"
      i18n:domain="plone">
      <h5 class="hiddenStructure" i18n:translate="heading_sections">Sections</h5>

      <ul id="portal-globalnav">
      <tal:tabs tal:repeat="tab view/portal_tabs"><li tal:attributes="id string:portaltab-${tab/id};
      class python:view.selected_portal_tab==tab['id'] and 'selected' or 'plain'"
      ><a href=""
      tal:content="tab/name"
      tal:attributes="href tab/url;
      title tab/description|nothing">
      Tab Name
      </a>
      <tal:block omit-tag=""
      tal:define="subnav tab/subtab;">
      <tal:block omit-tag="" tal:condition="subnav">
      <ul class="nn-twolevel-subnav">
      <tal:tabs tal:repeat="subtab subnav">
      <li tal:attributes="id string:portaltab-${subtab/id};">
      <a href="" class="" tal:attributes="href subtab/url;" accesskey="accesskeys-tabs" i18n:attributes="accesskey">
      <tal:block omit-tag="" i18n:translate="" >
      <span tal:replace="subtab/name">Tab Name</span>
      </tal:block>
      </a>
      </li>
      </tal:tabs>
      </ul>
      </tal:block>
      </tal:block>
      </li></tal:tabs>
      </ul>
      </tal:tabs>
  5. Once the viewlet folder is complete, you need to add it to your theme product configure.zcml by adding:

    <include package=".viewlet" />
  6. Then you should modify your theme main css file (usually mytheme.css.dtml or public.css.dtml) so that it would hide the submenu and reveal it only on hover of the main tab:

    #portal-globalnav {
    position:relative;
    display: inline;
    }

    #portal-globalnav li ul {
    display: none;
    visibility: hidden;
    position: absolute;
    z-index: 200;
    }

    #portal-globalnav li ul li {
    display: block;
    margin: 0;
    padding: 0;
    width: 100%;
    clear: left;
    }

    #portal-globalnav li ul li a {
    display: block;
    }

    #portal-globalnav li:hover ul {
    visibility: visible;
    display: block;
    }

    #portal-globalnav li {
    display: inline;
    position: relative;
    float: left;
    }

Further information

The code for the getsubtab function was mostly copied from the CMFPlone/browser/navigation.py topLevelTabs function. This is so that the same behaviour can be expected for our sub menu as our normal tabs (eg. hide from navigation etc). The CSS given above is mostly just to hide and show the sub-menu. It would require additional CSS mark-up to make it completely compatible and completely blend with your theme.