Navigation structures
Note: Return to reference manual view.
1. Navigation root
As of Plone 2.5, the root property in portal_properties/navtree_properties enables the site admin to set a folder as the root of the site's navigation. This will affect not only the navigation tree (which was all it did in Plone 2.1.3 and later versions in the 2.1 series), but also affect where the breadcrumbs, the tabs and the sitemap are rooted.
The canonical navigation root is returned as a path, relative to the portal root, in CMFPlone.browser.navtree.getNavigationRoot(). This will use the property in navtree_properties, unless the context or a parent object is marked with the CMFPlone.browser.interfaces.INavigationRoot interface. If such an object is found, that is used as the navigation root. By default, the portal root is marked with this interface, but it's possible to mark other objects with it as well.
The root path is used by the navtree and sitemap query builders and the navigation tabs and breadcrumb algorithms (see the following pages) to root the catalog query used to construct these.
2. Constructing the navigation tree
The navigation tree portlet uses a view, found in CMFPlone.browser.interfaces.INavigationPortlet with a factory in CMFPlone.browser.portlets.navigation. This view in turn uses a more general view, CMFPlone.browser.interfaces.INavigationTree that is implemented in CMFPlone.browser.navigation.CatalogNavigationTree.
Previously, the navigation tree used to be constructed in CMFPlone.PloneTool, and there is still a deprecated createNavTree() method there. A utility method that invokes the view is found in CMFPlone.utils.createNavTree().
The actual construction of the navtree is delegated to a generic function found in CMFPlone.browser.navtree called buildFolderTree(). This function has a single purpose: Execute a catalog query and turn the results into a navigation tree-like structure. It is thus generic, and can be used to construct other navigational structures (such as the TOC for this reference manual).
Because the navigation tree needs to take several properties into account, the navtree builder can take a "strategy" object, which implements CMFPlone.browser.interfaces.INavtreeStrategy. This can provide methods used to decide whether a given node should be included and/or whether a whole subtree should be pruned. It can also decorate the node dicts with additional keys, used to hold domain-specific information.
The standard navigation tree strategy is found in CMFPlone.browser.navtree.DefaultNavtreeStrategy. In fact this extends the sitemap navtree strategy, because a navtree is essentially a more restricted form of a sitemap.
The navtree strategy is an adapter, looked up on the context (the content object being viewed - registered for * by default) and the interface of the view that is constructing it (to distinguish the sitemap from the navtree):
<adapter for="*
.interfaces.INavigationTree"
factory=".navtree.DefaultNavtreeStrategy"
provides=".interfaces.INavtreeStrategy" />
The CatalogNavigationTree class that implements INavigationTree thus does this:
def navigationTree(self):
context = utils.context(self)
queryBuilder = NavtreeQueryBuilder(context)
query = queryBuilder()
strategy = getMultiAdapter((context, self), INavtreeStrategy)
return buildFolderTree(context, obj=context, query=query, strategy=strategy)
The NavtreeQueryBuilder, is found in CMFPlone.browser.navtree, with an interface in CMFPlone.browser.interfaces.INavigationQueryBuilder. This simply wraps up the task of constructing the catalog query dict that is used to build the navtree, so that it may be re-used.
To make it easier to create custom navtrees and similar structures for site integrators, the contents of CMFPlone.browser.navtree may be imported from protected code, by virtue of the following statement in 'CMFPlone/__init__.py':
allow_module('Products.CMFPlone.browser.navtree')
To ensure that the methods and attributes of the query builders and navtree strategies are accessible as well, CMFPlone/browser/configure.zcml contains:
<content class="Products.CMFPlone.Portal.PloneSite">
<implements interface=".interfaces.INavigationRoot" />
</content>
<content class=".navtree.NavtreeStrategyBase">
<allow interface=".interfaces.INavtreeStrategy" />
</content>
<content class=".navtree.NavtreeQueryBuilder">
<allow interface=".interfaces.INavigationQueryBuilder" />
</content>
3. Constructing the sitemap
The sitemap is constructed using the same mechanisms as the navigation tree - it simply uses a different query builder and navtree strategy.
The view that is used by the sitemap.pt page template is found in CMFPlone.browser.sitemap.SitemapView, and implements CMFPlone.browser.interfaces.ISitemapView. It in turn delegates to the more general view CMFPlone.browser.navigation.CatalogSiteMap which implements CMFPlone.browser.interfaces.ISitemap. This does:
def siteMap(self):
context = utils.context(self)
queryBuilder = SitemapQueryBuilder(context)
query = queryBuilder()
strategy = getMultiAdapter((context, self), INavtreeStrategy)
return buildFolderTree(context, obj=context, query=query, strategy=strategy)
Note that the sitemap and navtree query builders and strategies share much code and are derived from one another.
There is a utilty method in CMFPlone.utils called createSiteMap() that invokes the ISitemap view. The deprecated way of constructing the sitemap is through a call to PloneTool.createNavTree setting sitemap=True.
4. Navigation tabs
Since Plone 2.1, the navigation tabs at the top of the site have been constructed from two sources:
- Any actions in the
portal_tabscategory - Any folders in the root of the site
In Plone 2.1, these were created using PloneTool.createTopLevelTabs(). This is now deprecated. A utility method in PloneTool.utils with the same name now wraps the call to the view that implements this functionality. The view can be found in CMFPlone.browser.navigation.CatalogNavigationTabs, with an interface in CMFPlone.browser.INavigationTabs.
The view implementation performs an action lookup, and constructs a query that that finds folderish items in the navigation root. Note that the "navigation root" is not necessarily the portal root - see the section on the navigation root.
5. Breadcrumbs
Plone 2.5 actually ships with two implementations of the breadcrumbs. Both can be used as the view that implements CMFPlone.browser.interfaces.INavigationBreadcrumbs. The older, currently disabled method is found in CMFPlone.browser.navigation.CatalogNavigationBreadcrumbs, and, as it name implies, uses a catalog query to find the breadcrumbs.
The newer method is found in CMFPlone.browser.navigation.PhysicalNavigationBreadcrumbs. This walks up the acquisition parent hierarchy to find each parent. The astute reader will remember that waking objects like this is generally bad. However, some benchmarking revealed that since the direct parents of the context are quite likely to be in the ZODB cache, and since there are not likely to be very many of them, the cost of traversing to these objects is smaller than the cost of constructing and executing a catalog query.
By being based on real objects, the breadcrumbs can make more extensive use of views. The breadcrumbs are constructed in reverse order, starting at the depeest node. The parents are prepended to the breadcrumbs trail of this node by recursively doing a view lookup on the parent, like so:
def breadcrumbs(self):
context = utils.context(self)
request = self.request
container = utils.parent(context)
...
view = getMultiAdapter((container, request), name='breadcrumbs_view')
base = tuple(view.breadcrumbs())
...
rootPath = getNavigationRoot(context)
itemPath = '/'.join(context.getPhysicalPath())
...
if not utils.isDefaultPage(context, request) and not rootPath.startswith(itemPath):
base += ({'absolute_url': item_url,
'Title': utils.pretty_title_or_id(context, context),
},)
return base
The traversal is stopped at the navigation root by the following degenerate view:
class RootPhysicalNavigationBreadcrumbs(utils.BrowserView):
implements(INavigationBreadcrumbs)
def breadcrumbs(self):
# XXX Root never gets included, it's hardcoded as 'Home' in
# the template. We will fix and remove the hardcoding and fix
# the tests.
context = utils.context(self)
return ()
Then, the two views are registered in CMFPlone/browser/configure.zcml as follows:
<browser:page
for="*"
name="breadcrumbs_view"
class=".navigation.PhysicalNavigationBreadcrumbs"
permission="zope.Public"
allowed_attributes="breadcrumbs"
/>
<browser:page
for=".interfaces.INavigationRoot"
name="breadcrumbs_view"
class=".navigation.RootPhysicalNavigationBreadcrumbs"
permission="zope.Public"
allowed_attributes="breadcrumbs"
/>
This pattern is quite elegant, and allows for more specific implementations to be plugged based on the interface of the object, rather than having to substitute it globally.