[ZPT] dynamic navigation menus
Mark McEahern
marklists@mceahern.com
Fri, 2 Aug 2002 14:09:57 -0500
[Thom Wysong]
> A python script should be able to determine the current section and
> subsection. Then a tal:condition could be used to determine whether to
> output 'selected' or 'unselected' css/html tags.
Here's what I ended up with:
###
# current_section
#
# Parameters
#
# level
# Should be an integer greater than or equal to 1.
#
# Return the current section for the specified level.
current = context
path_to_root = []
while 1:
path_to_root.insert(0, current)
if current == container:
break
current = current.getParentNode()
# path_to_root might look like this:
# [container, content, section1, subsection1_1, ...]
try:
# We have to add 1 because of the content folder.
section = path_to_root[level + 1]
return section
except IndexError:
return container.default_section(level)
###
# default_section
#
# Parameters
#
# level
# Should be an integer greater than or equal to 1.
#
# Return the default section for the specified level.
section = container.content.getFirstChild()
for i in range(level - 1):
section = section.getFirstChild()
return section
###
Here's the snippet of the ZPT where I use these to create a two-level tabbed
navigation. The HTML is more fragile than I would like (it doesn't look
good with any reasonable number of sections and requires hand-tweaking
attributes that should either be in the CSS or perhaps I should abandon the
table-based approach altogether?), but that's a different problem.
<!-- Navigation -->
<table metal:define-macro="navigation"
tal:define="global section_count
python:len(container.content.objectValues(['Ordered Folder']));
global current_section
python:here.current_section(level=1);
global current_subsection
python:here.current_section(level=2)"
tal:attributes="cols python:section_count + 1"
width="744" cellpadding="0" cellspacing="0" border="0">
<tr>
<span tal:omit-tag=""
tal:repeat="section
python:container.content.objectValues(['Ordered Folder'])">
<td tal:attributes="class python:test(section == current_section,
'tab_select', 'tab_unselect')"
width="148" height="24">
<a tal:condition="python:section <> current_section"
tal:content="section/title_or_id"
tal:attributes="href section/absolute_url"
class="tab">Section title</a>
<span tal:omit-tag=""
tal:condition="python:section == current_section"
tal:replace="section/title_or_id">Section title</span>
</td>
</span>
<td width="24"> </td>
</tr>
<tr>
<td tal:attributes="colspan python:section_count + 1"
height="24" class="menu_bar">
<span tal:repeat="subsection
python:current_section.objectValues(['Ordered Folder'])"
tal:attributes="class python:test(subsection ==
current_subsection, 'menu_item_select', 'menu_item_unselect')">
<a tal:condition="python:subsection != current_subsection"
tal:attributes="href subsection/absolute_url"
tal:content="subsection/title_or_id"
class="item">Subsection title</a>
<span tal:omit-tag=""
tal:condition="python:subsection == current_subsection"
tal:replace="subsection/title_or_id">Subsection title</span>
</span>
</td>
</tr>
</table>
> However, trying to use a tabbed interface might be tricky. My guess is you
> want the values of the top-level tabs to remain static as you move through
> the site - with the only thing changing on them being which one(s) are
> highlighted/selected. However, using here/objectValues and
> container/objectValues probably will output values for the tabs that are
> relative to where you are in the site - changing the titles/links
> that show up in the tabs.
Yes, it certainly is tricky, but the above code albeit more messy than I'd
like has the virtue that It Works (tm) albeit in a fragile manner!
> The easiest way would probably be to set a "default_secton_id" or
> "default_section_path" property on the root content folder. The property
> would then be accessible by any object in that root folder via
> acquisition.
As the above code shows, I prefer the approach of using the getFirstChild()
function to get the default section. I should look into it, but I wonder
whether getFirstChild() allows you to specify a type filter like
objectValues() does. That is:
objectValues(['Ordered Folder'])
Of course, I should resort to good old trial and error if not RTFM.
> The site could be viewed as being three levels - with the 'root content
> folder' being level 1, 'sections' being level 2, and 'subsections' being
> level 3. At each level you'll want the tabs to display the
> objectValues for
> the root content folder, and for whichever 'section' the user is in (if
> any). However, depending on which level in the site the user is
> at, the way
> you get those values will change (if you try to use
> container/objectValues,
> etc in your tal). Sometimes it will be objectValues for the current
> container and each of its children (level 1). Sometimes it will be
> objectValues for the parent of the current container and the current
> container itself (level 2). Sometimes it will be objectValues for the
> grandparent and parent of the current container (level 3).
This is a helpful explanation. Thank you.
> Another way of handling this would be to have a python script in the root
> content folder return a data structure of the entire site, that
> the ZPT then
> uses to build tabs off of. That data structure could be built
> dynamically by
> spidering the site (tricky) or by hard-coding the
> section/subsection values
> into the python script (not dynamic, but easier to begin with). The script
> should be accessible to the ZPTs anywhere on the site via acquisition.
I prefer the approach of using Folders (or OrderedFolders) to define the
navigation structure because that seems to fit the security model--each
Folder can have a distinct set of authors/editors/etc stored in its
UserFolder. Does that make sense?
> In the ZMI, when creating multiple instances of the same type of object,
> it's probably faster to create only one instance of it, then copy
> & paste a
> bunch of copies of it where you want. Then do a mass rename (click the
> checkboxes for all the copy_of_ instances, click the 'Rename' button, then
> rename all the copies at once). This is faster than filling in a numerous
> 'Add' forms.
Thanks, this is a very helpful suggestion!
> If you are able to use OrderedFolders, then you could simply filter your
> objectValues - listing out only OrderedFolders.
>
> If not, then you could add a Boolean property to your root content folder
> called in_nav_menu. Set it to 'true' (check the box). Then every
> folder that
> is intended to be in the navigation menu will automatically acquire this
> property. Every folder that is not a intended to be in the navigation menu
> (ie images, styles) will need their own in_nav_menu property, set to
> 'false'. Then use a <tal:condition="seq_item.in_nav_menu"> inside the
> tal:repeat loop to only output items that have in_nav_menu set to 'true'.
I may end up needing to do this, but I hope I can simply filter on
OrderedFolders and be done with it.
> As mentioned in the previous email on this topic, put a <base />
> tag at the
> top of your ZPT template. That should prevent Zope from mucking up any
> relative URI addressing you use.
I continue to punt on this--my current strategy is to use
item.absolute_url() when creating the link to menu items. I'm not sure this
is a bad solution, but again: It Works.
> As a rule of thumb, when placing an expression after "python:"
> inside a ZPT tag, you need to use python syntax. Use ZPT syntax
> elsewhere inside ZPT tags.
>
> Python syntax --> object.method() .. object.property
> ZPT syntax -----> object/method .... object/property
That's clear now, thank you. I even ran across something in the Zope Book's
chapters on ZPT that addressed this--but it was only AFTER I ran into the
problem and discovered the solution on my own. There's probably no way
around the fact that people who jump right in and read a little, try a
little, will get bit by this.
> Determining current position .. this can be tricky .. will
> probably need to
> be done in a python script .. might want to investigate pulling the URL
> parameter out of the REQUEST variable .. then parsing it to help determine
> where the user is in the site.
I like the approach I ended up with. I don't use REQUEST. I build a list
of the context's parent and return the item at a specified "level."
So, for a path like this:
/Root/
navtest/
content/
section1/
subsection1_1/
This gives a path_to_root like this:
[ navtest, content, section1, subsection1_1 ]
If I navigate to:
http://localhost:8080/navtest/content/section1/subsection1_1/
To get the current main menu item, I use current_section(level=1). For the
current submenu item, I use current_section(level=2).
current_section = path_to_root[level + 1]
I have to add 1 because of the fact that all the navigation menu folders
don't start from navtest (the "root") but from a subfolder called content.
Thanks!
// mark
-