Creating a custom navigation section
This How-to applies to:
Plone 2.1.x
This How-to is intended for:
Integrators, Customizers
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 togetSectionsbelow. 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_navattribute, which is exposed from thePropertiestab 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 bothinfoitself, and its children. Alternatively, you can get all sub-sections under a given root. - You probably want to highlight the currently selected item.
getSectionsworks out which item you are currently viewing and sets itsselectedproperty to True. This happens recursively, meaning that if you have a two level navigation, and the user is in/sections/contact/email-form, bothcontactandemail-formwould be marked asselected.
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.
Strange
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()
Products.CMFPlone.browser.navtree (from Plone 2.5)
It's well documented.
Thanks Martin
Plone 2.5 replacement for getSections()
Plone 2.5 replacement for getSections()
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
Here's the diff: https://weblion.psu.edu/trac/weblion/changeset/1270/collegeOfEducation/products/EducationStyles2006/trunk/skins/educationstyles2006_scripts/getSections.py
Cheers!
nice, but...
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...
I've been puzzled on this for some time now. Any help would be greatly appreciated :)
See ErikRose's patch above
I think ErikRose's patch above (https://weblion.psu.edu/trac/weblion/changeset/1270/collegeOfEducation/products/EducationStyles2006/trunk/skins/educationstyles2006_scripts/getSections.py) solves this problem.
Two questions from a newbie...
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
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)
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...
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
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
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?
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
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
If anyone was wondering, here is a fix I found that fixes the ordering for the parent level items:
https://weblion.psu.edu/trac/weblion/browser/huckInstitutes/huckstyles1/skins/huckstyles1_scripts/getSections.py?rev=1270
Problem with comments?
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
NoneTypeobject has no attributegetURLwhile rendering portlet here/portlet_subnav/macros/portletEssentially, 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!