Creating a custom navigation section

When creating a custom skin, you may want something more concrete than the navigation tree, but a little more flexible than the portal tabs. This HowTo shows how to create a navigation section which is automatically generated from the contents of a folder.

If you are creating a custom skin, you may wish to have a custom navigation section, perhaps in a portlet, or simply as part of a customised main_template or other standard template (header, perhaps). This document shows how to do just that, with the following assumptions and caveats:

  • All items to appear in the navigation will be under a single folder. For this example, that folder will be /sections, but you can use what you like. (If you want to use the portal root, pass an empty string as the folder name in the call to getSections below. If you want to list items under the current folder no matter where this may be, pass the string . (a single full stop) instead.)
  • The order of navigation is significant. A portal administrator can use the standard folder re-ordering buttons in Plone to change the order of the contents of /sections
  • You may want to ignore some items. The standard Plone exclude_from_nav attribute, which is exposed from the Properties tab of all standard content types will work as it does in the standard navigation tree.
  • You may want sub-menus in your navigation - this method will allow you to specify a fixed depth of sub-menus to consider. For example, if you have a folder /sections/info, and you set the depth to 2, you will be able to create links to both info itself, and its children. Alternatively, you can get all sub-sections under a given root.
  • You probably want to highlight the currently selected item. getSections works out which item you are currently viewing and sets its selected property to True. This happens recursively, meaning that if you have a two level navigation, and the user is in /sections/contact/email-form, both contact and email-form would be marked as selected.

Simple usage

This approach consists of two parts: a script called getSections.py which you should put in your skin folder (or portal_skins/custom, in which case drop the .py suffix) and a snippet of TAL code which you can use as a base to develop your custom navigation. I tend to put this straight into main_template or a template inclded by from main_template as it is the principal way of navigating a site when I use it, but a portlet may be another good place to put it.

The TAL code to display the navigation must call the getSections script and use its returned data-structure in one or more tal:repeat loops. For a two-level navigation showing all folders and documents under /sections in your portal root, you'd use something like:

  <ul class="topLevelNavigation"
      tal:define="sections python:here.getSections('/sections', levels=2, types=['Folder', 'Document'])">

      <li tal:repeat="section sections">
        <a tal:attributes="href section/item/getURL;
                class python:test (section['active'], 'selected', '');" 
           tal:content="section/item/Title">
          Top level section
        </a>

        <ul class="secondLevelNavigation">
          <li tal:repeat="subsection section/children">
             <a tal:attributes="href subsection/item/getURL;
                  class python:test (subsection['active'], 'selected', '');"
                         tal:content="subsection/item/Title">
               Second level section
             </a>
          </li>
        </ul>
      </li>

  </ul>

Notice the use of CSS classes - use these (or similar classes) to appropriately style your navigation. In particular, notice how the class of the <a> tag is set to selected for selected items. Use .selected in your style sheet to highlight the currently selected item in the navigation.

For accessability, navigation items like these should always be in a <ul> styled look the way you want. To check whether your navigation works, try accessing your site in a text-mode browser such as lynx, or turn off the Plone stylesheet in your browser (use Opera or Firefox). If you see a nice, usable nested list, your site is much more likely to be accessible people with disabilities, and via less capable browsers.

Recursive macros and nested lists

To take this a step further, you can use recursive macros to get arbitrarily nested lists, however deep the tree. This requires two templates and the use of macros.

First, create a template called 'navigation_macros.pt':

  <ul metal:define-macro="navigation_menu">

      <li tal:repeat="section sections">
        <a tal:attributes="href section/item/getURL;
                class python:test (section['active'], 'selected', '');" 
           tal:content="section/item/Title">
          Top level section
        </a>

        <tal:recurse define="sections section/children"
                     condition="nocall:sections">
          <metal:call use-macro="here/navigation_macros/macros/navigation_menu" />
        </tal:recurse>

      </li>

  </ul>

Notice the tal:recurse block. This calls the same macro again for children of the current item, hence producing nested lists. It works by expecting the template calling the macro to define a variable sections holding the top level sections, and then re-defines these to point to the sub-sections, recursively.

Now we just need to start the recursion from the template where the navigation menu is to be displayed:

  <tal:block define="sections python:here.getSections('/info')">
    <metal:call use-macro="here/navigation_macros/macros/navigation_menu" />
  </tal:block>

By not passing a levels parameter, you will get all sections under /info.

Attached files

Problem with comments?

Posted by Cyrille Bonnet at Mar 30, 2006 02:08 AM
Thanks Martin, this is very useful. I've had a small problem though, when I started adding comments to my pages.

After a comment was added, I would get an error:

2006-03-17T21:33:32 INFO(0) Plone Debug Error exceptions.AttributeError on 'NoneType' object has no attribute 'getURL' while rendering portlet here/portlet_subnav/macros/portlet

Essentially, it is failing on the call to: section/item/getURL.

Not sure why this is happening, but the solution I used was to test that section/item existed.

Otherwise, this is a great piece of code. Thanks for sharing that!

Strange

Posted by Martin Aspeli at Mar 30, 2006 09:00 AM
Very strange indeed. My guess is that the catalog search is returning Discussion Items as well. If you can, try to send a list of portal_types to the query, e.g. portal_type = ('Folder', 'Document',) to show only documents and folders. If you want to take the list of types currently chosen as shown in the navtree, you can use context.plone_utils.typesToList(), I believe.

P.S.
When 2.1.3 is out this may get yet another rewrite, since 2.1.3 and 2.5 contain some helper methods that make writing these kinds of navigation structures very easy :)

Plone 2.5 replacement for getSections()

Posted by Ian F. Hood at Feb 24, 2007 01:19 AM
Martin recently advised me that instead of the getSections() script included in this 'how to' we can reuse:

    Products.CMFPlone.browser.navtree (from Plone 2.5)

It's well documented.
Thanks Martin

Plone 2.5 replacement for getSections()

Posted by Rob Porter at Apr 10, 2007 10:32 PM
I am not sure but when I use the code in the tutorial it works great until I stick in a folder inside of a folder. Once I do that the folders that have folders in them filter to the top. I am not sure how to get Products.CMFPlone.browser.navtree to work instead. I figured I would throw this out there.

Plone 2.5 replacement for getSections()

Posted by Ian F. Hood at Apr 10, 2007 10:58 PM
IIRC I had problems with the order that the nested folders came back... sometimes they were wrong. I suppose that's why it's called getSections() instead of getTree().

I tinkered with Products.CMFPlone.browser.navtree until I ran up against my deadline and had to hack out a non-catalog workaround for my particular use case.

I didn't get either variant working but I suspect I will need to take another look at it in the future.

A Debugged Version

Posted by Erik Rose at Apr 11, 2007 08:15 PM

nice, but...

Posted by Nelson Minica at Apr 02, 2006 07:52 AM
I noticed the item ids are missing first character with a basePath of "/" because of the extra slash at end, I added this after basePath was set: if basePath[-1]=='/': basePath=basePath[:-1]

If I passed filter=python:{'review_state':'published'} to the script and had published children under a private parent it would return NoneType errors. Would be nice if the script respected the filter, but adding this to template helps some: tal:condition="python:section['item'] is not None"

Items are not ordered correctly. How could this be done?

Re: nice, but...

Posted by Gavin at May 10, 2007 02:34 PM
Hi there. Has anyone managed to figure out how to get the 1st level items to order correctly? It seems as if the sort_on filter only sorts the 2nd level items.

I've been puzzled on this for some time now. Any help would be greatly appreciated :)

See ErikRose's patch above

Posted by Jon Stahl at Jul 10, 2007 08:40 PM
@Gavin

I think ErikRose's patch above (https://weblion.psu.edu/[…]/getSections.py) solves this problem.

Two questions from a newbie...

Posted by Thierry CERETTO at Jun 21, 2006 03:28 PM
Hi Martin,

First your "How to" is very useful, thank's.

I'm very newbie :( and I need some explanations, please.

1) You write -"This approach consists of two parts: a script called getSections.py which you should put in your skin folder..."

Well, I have put the script in my Zope-Instance/CMFPlone/skins but I get an error something this : "KeyError: 'sections' "
I think that the script (getSections.py) is at the wromg place. Where should it be put exactly?

(I have put the script in Page Template in plone-skins/customs and my navigation menu works... but I would like to know to make in the two ways.)
  
2) You write -"...This happens recursively, meaning that if you have a two level navigation, and the user is in /sections/contact/email-form, both contact and email-form would be marked as selected..."

Well, that's work but I would like only highlighted the active item (not the parent too)
How make that? (I sought in script but I do not see how make that)

Thank you,

Thierry
   

  

Two answers

Posted by Martin Aspeli at Jun 21, 2006 03:46 PM
1) You put it in the wrong place :) First of all don't put anything (or change anything) in the standard Plone directories. Create your own product (e.g. look at DIYPloneStyle, and the "custom style" tutorial on plone.org/documentation/tutorial) and put your skin customisations there; secondly, the convention is that these go in Products/MyProduct/skins/my_product (note last directory).

2) I think if you check 'current' instead of 'active' it ought to highlight only the current folder and no any parents.

... I really ought to update this for Plone 2.1.3 and 2.5 and the new navtree-generating reusable code in there, though. Time, time, time... :)

Martin

Two answers...feedback (maybe useful for someone else)

Posted by Thierry CERETTO at Jun 29, 2006 09:30 AM
Thanks, to have taken time to answer me...

About your answer at my second second question, no luck, that doesn't work :(( but I found the answer myself (I begin in Python too :) ) , I have changed your script like this :

In step "# Check if this is the current item or a parent thereof" instead :

if currentPath.startswith(itemPath):

I put :

if currentPath == itemPath :

and now only the effective current seleted item is highlighted

Voilà !

Thierry

Questions from another newbie...

Posted by Carla Delgado at Sep 23, 2006 04:35 PM
It looks like this how-two is exactly what I need, thanks for making it availabe. But still I need some help to get it working.
I created a product with DIYPloneStyle, and placed there the script.
My problem is that I don't know where I should put the TAL code to make my menu appear. You mention the main_template, but WHERE in the main template?
What I would like to have in the end is a menu to substitute the navigation menu on my skin.
Any help would be VERY welcome.
Thanks!

Placing template code

Posted by John Habermann at Oct 09, 2006 11:35 PM
I just put the template code into my main_template.pt file. You could either just customise the main_template.pt file through th e ZMI in which case it will appear in the plone_skins/custom folder or as in my case I copied it to my theme_templates folder which is in the Product I had created using the DIYStyle product.
Everything works fine in my 2.5 version of Plone except for the fact that I can't figure out how to control the ordering of the navigation elements and it appears to be sorting in a somewhat weird way as my elements are numbered in the title and it looks like it sorts based on the first letter it encounters in the title. I will post her again if I figure out how to do it.

cheers
John

Problems with AT types

Posted by Patrick Charrault at Oct 27, 2006 11:16 AM
Very intersting stuff indeed !

My problem is when I put AT Content Types I made in the "types=" criteria. I get this as error message : "'NoneType' object has no attribute 'getURL'". What should I do ?

Where does getSections.py go?

Posted by Rob Hills at Nov 06, 2006 03:16 PM
Hi,

I've created a skin for my app as a product and it's basically working fine. I've followed this excellent howto but can't get it to work - AFAICT, it can't find the getSections - the stacktrace ends with "AttributeError: getSections"

If my product is called My_Product and I have my templates in Products/My_Product/skins/my_product_templates, exactly which directory should I put the getSections.py script?

For the record, I've tried the following without success:
  Products/My_Product/
  Products/My_Product/Extensions/
  Products/My_Product/skins/
  Products/My_Product/skins/my_product_scripts/

Many thanks for sharing the code/howto.

getSections.py goes in the templates directory

Posted by Rob Hills at Nov 06, 2006 04:12 PM
I worked it out shortly after posting the question - ain't that always the way?

I realised that one place I'd not tried was in with my product skin layer's templates, ie:

Products/My_Product/skins/my_product_templates

I put getSections.py there and it all worked as advertised.

Thought I'd post this to hopefully save some other noob the time it took me to work it out!

Fix for parent ordering issue

Posted by Gavin at May 10, 2007 03:05 PM

If anyone was wondering, here is a fix I found that fixes the ordering for the parent level items:

https://weblion.psu.edu/[…]/getSections.py?rev=1270

Fix for parent ordering issue

Posted by christoph boehner at Jul 31, 2007 11:13 AM
We recently implemented a fairly complex 2 level horizontal navigation with an aditional 3rd level dropdown navigation using the suckerfish approach. Since we also ran into the 'weird sorting' problem that resulted in long discussions with our customer i have to admit: you are my hero of the day! I couldn't figure it out myself. Thank you for posting this fix.