[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">&nbsp;</td>
  </tr>
  <tr>
    <td tal:attributes="colspan python:section_count + 1"
        height="24" class="menu_bar">
      &nbsp;
      <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

-