
Introduction
------------

The navigation package provides a flexible way to render menus and
toolbars in HTML.  It's very similar to the menu concept existent
in Zope 3 itself, and also to the implementation existent in Launchpad.

The most noticeable feature that this package supports in addition to
the aforementioned ones, and the reason behind the creation of it, is
dynamic creation of items.  For instance, to create a sidebar which
shows a submenu for each account a given user is registered for, these
account items can't be created in advance.

Doing this with either the Zope 3 system or the Launchpad one means
reimplementing most of the item registration and processing algorithm,
which is pretty much what this package is doing.


Basics
------

Each item in a toolbar or a menu is a navigation item.  Let's create a
simple navigation item.

    >>> from canonical.navigation.interfaces import INavigationItem
    >>> from canonical.navigation import NavigationItem
    >>> from zope.interface.verify import verifyObject
    >>> from zope.publisher.browser import TestRequest

    >>> class DashboardItem(NavigationItem):
    ...
    ...     title = "Dashboard"
    ...     action = "/dashboard"

    >>> context = object()
    >>> request = TestRequest()

    >>> dashboard_item = DashboardItem(context, request)

    >>> verifyObject(INavigationItem, dashboard_item)
    True


A navigation item also behaves as a navigation, since it may include
additional items under it.

    >>> from canonical.navigation.interfaces import INavigation

    >>> verifyObject(INavigation, dashboard_item)
    True


Navigation items may be sorted.  The default sort order is based on
their 'order' and 'title' attributes.  We'll create another item to
check it out.

   >>> class SettingsItem(NavigationItem):
   ...
   ...     title = "Settings"
   ...     action = "/settings"
   ...     order = -1


   >>> settings_item = SettingsItem(context, request)

   >>> sorted([dashboard_item, settings_item])
   [<...SettingsItem object at ...>, <...DashboardItem object at ...>]

   >>> SettingsItem.order = 1
   >>> sorted([dashboard_item, settings_item])
   [<...DashboardItem object at ...>, <...SettingsItem object at ...>]

   >>> DashboardItem.order = SettingsItem.order = 0
   >>> SettingsItem.title = "A Settings Area"
   >>> sorted([dashboard_item, settings_item])
   [<...SettingsItem object at ...>, <...DashboardItem object at ...>]

   >>> SettingsItem.title = "Settings"


Two interesting methods in the navigation item are 'is_selected' and
'is_available'.  They inform if the given navigation item is currently
being seen by the user, and if this item should be rendered or not.

Given our very vague context so far, they should currently be returning
good defaults.

    >>> dashboard_item.is_selected()
    False
    >>> dashboard_item.is_available()
    True

As aforementioned, items are also navigation parents, so they may have
subitems.  Since we've done nothing so far, it should also be a
resonable default, which is an empty list in this case.

    >>> dashboard_item.get_items()
    []


So, let's create a navigation for a fictitious sidebar menu.

    >>> from canonical.navigation import Navigation

    >>> class SidebarNavigation(Navigation):
    ...     pass

    >>> sidebar = SidebarNavigation(context, request)

    >>> verifyObject(INavigation, sidebar)
    True


At this point there's nothing attached to the sidebar either.

    >>> sidebar.get_items()
    []


Let's attach the dashboard item to the sidebar.

    >>> from zope.component import getSiteManager
    >>> sm = getSiteManager()

    >>> sm.registerAdapter(DashboardItem, (None, None, SidebarNavigation),
    ...                    INavigation, name="dashboard")


That's all!  Let's try it again.

    >>> sidebar.get_items()
    [<...DashboardItem object at ...>]


Let's investigate the returned item.

    >>> item = sidebar.get_items()[0]
    >>> item.context is context
    True
    >>> item.request is request
    True
    >>> item.parent is sidebar
    True


Navigation Item
---------------

If the current URL starts with the action of the navigation item, then
'is_selected' should return True.

    >>> request = TestRequest(environ={"SCRIPT_NAME": "/dashboard",})
    >>> DashboardItem(None, request).is_selected()
    True

    >>> request = TestRequest(environ={"SCRIPT_NAME": "/foo-bar"})
    >>> DashboardItem(None, request).is_selected()
    False

A navigation item is considered an external link by default if the url
is not relative:

   >>> class InternalItem(NavigationItem):
   ...
   ...     action = "/internal"


   >>> class ExternalItem(NavigationItem):
   ...
   ...     action = "http://google.com"


   >>> InternalItem(None, request).is_external()
   False

   >>> ExternalItem(None, request).is_external()
   True


Dynamic Items
-------------

To introduce an item dynamically in a navigation, we register another
navigation as a child of the parent navigation.

For instance, let's say we want to have an arbitrary number of items
pointing to accounts in our sidebar.  First, let's create an account
content class, and a couple of objects.

    >>> from zope.traversing.adapters import DefaultTraversable
    >>> class Account(DefaultTraversable):
    ...
    ...     def __init__(self, name):
    ...         super(Account, self).__init__(self)
    ...         self.name = name
    ...         self.selected = False

    >>> accounts = [Account("unifi"), Account("google")]


Now, let's create the navigation item that will display these accounts
in the sidebar menu.

    >>> class AccountItem(NavigationItem):
    ...
    ...     order = 2
    ...
    ...     @property
    ...     def title(self):
    ...         return self.context.name
    ...
    ...     @property
    ...     def action(self):
    ...         return "/account/" + self.context.name
    ...
    ...     def is_selected(self):
    ...         return self.context.selected
    ...
    ...     def __cmp__(self, other):
    ...         return cmp((self.order, self.title),
    ...                    (other.order, other.title))
    ...
    ...     def __repr__(self):
    ...         return "<...AccountItem %s>" % repr(self.title)


Finally, let's create the navigation that will insert these items
in the sidebar navigation.

    >>> class AccountsNavigation(Navigation):
    ...
    ...     def get_items(self):
    ...         return [AccountItem(account, self.request)
    ...                 for account in accounts]


Done.  The last step is to register this navigation as a child of the
sidebar one.  We do this the same way we register a normal item.

    >>> sm.registerAdapter(AccountsNavigation, (None, None, SidebarNavigation),
    ...                    INavigation, name="accounts")


Now, let's query the sidebar.

    >>> sorted([item for item in sidebar.get_items()])
    [<...DashboardItem ...>, <...AccountItem 'google'>, <...AccountItem 'unifi'>]


Iterating over the navigation should yield the same results, except
that items should already be ordered.

    >>> [item for item in sidebar]
    [<...DashboardItem ...>, <...AccountItem 'google'>, <...AccountItem 'unifi'>]


TALES Navigation Item
---------------------

The TALES navigation item provides a few additional features which
are helpful when defining navigation items.

One of its features are the definition of attributes via TALES
expressions.

Let's redefine the account navigation item using it.

    >>> from canonical.navigation import TALESNavigationItem

    >>> class FancyAccountItem(TALESNavigationItem):
    ...
    ...     title_expr = "context/name"
    ...     description_expr = "string:Access ${context/name} account"
    ...     action_expr = "string:/account/${context/name}"
    ...     icon_expr = "python:'/account/'+path('context/name')+'/icon.png'"
    ...     available_expr = "context/available"
    ...     selected_expr = "context/selected"


    >>> unifi = accounts[0]
    >>> unifi.selected = False
    >>> unifi.available = True

    >>> unifi_item = FancyAccountItem(unifi, request)
    >>> unifi_item.title
    'unifi'
    >>> unifi_item.description
    'Access unifi account'
    >>> unifi_item.action
    '/account/unifi'
    >>> unifi_item.icon
    '/account/unifi/icon.png'


With the available attribute we defined, we can make an account
available/unavailable and selected/unselected by setting the
'available' and 'selected' attributes in accounts.

    >>> unifi_item.is_selected()
    False
    >>> unifi_item.is_available()
    True

    >>> unifi.selected = True
    >>> unifi.available = False
    >>> unifi_item = FancyAccountItem(unifi, request)
    >>> unifi_item.is_selected()
    True
    >>> unifi_item.is_available()
    False


This class may be subclassed, and specific attributes overloaded.

    >>> class SubFancyAccountItem(FancyAccountItem):
    ...     icon = "/account.png"

    >>> sub_account_item = SubFancyAccountItem(accounts[0], request)
    >>> sub_account_item.title
    'unifi'
    >>> sub_account_item.icon
    '/account.png'


With TALESNavigationItem, it's also possible to define a permission
to be checked for defining the item's availability.

    >>> from zope.security.management import (
    ...    newInteraction, endInteraction, getSecurityPolicy, setSecurityPolicy)

    >>> from canonical.security.role import RoleRegistry
    >>> from canonical.security.policy import SecurityPolicy
    >>> sm.registerUtility(RoleRegistry())
    >>> orig_policy = getSecurityPolicy()
    >>> _ = setSecurityPolicy(SecurityPolicy)
    >>> newInteraction()

    >>> class PermissionItem(TALESNavigationItem):
    ...
    ...     permission = "NotAllowed"

    >>> perm_item = PermissionItem(context, request)
    >>> perm_item.is_available()
    False

    >>> from zope.security.checker import CheckerPublic
    >>> perm_item.permission = CheckerPublic
    >>> perm_item.is_available()
    True

    >>> endInteraction()
    >>> _ = setSecurityPolicy(orig_policy)


Navigation via ZCML
-------------------

Navigation definitions may also be done via ZCML.

    >>> import sys
    >>> class FakeModule(object):
    ...     def __getattr__(self, name):
    ...         return globals()[name]
    >>> sys.modules["fake_module"] = FakeModule()
    >>> from canonical.testing.resources import zcml
    >>> import canonical.navigation

    >>> zcml("""
    ... <configure xmlns="http://namespaces.zope.org/browser">
    ...    <navigation
    ...        name="zcml-sidebar"
    ...        class="fake_module.SidebarNavigation" />
    ... </configure>
    ... """, packages=(canonical.navigation,))
    <...ConfigurationMachine object at ...>

    >>> from zope.component import getMultiAdapter
    >>> getMultiAdapter((context, request), INavigation, "zcml-sidebar")
    <...SidebarNavigation object at ...>


A parent interface/class may also be defined.

    >>> zcml("""
    ... <configure xmlns="http://namespaces.zope.org/browser">
    ...    <navigation
    ...        name="zcml-accounts"
    ...        class="fake_module.AccountsNavigation"
    ...        parent="fake_module.SidebarNavigation" />
    ... </configure>
    ... """, packages=(canonical.navigation,))
    <...ConfigurationMachine object at ...>

    >>> getMultiAdapter((context, request, sidebar),
    ...                 INavigation, "zcml-accounts")
    <...AccountsNavigation object at ...>


Registering items via ZCML is also possible, and just as easy.

    >>> zcml("""
    ... <configure xmlns="http://namespaces.zope.org/browser">
    ...    <navigationItem
    ...        name="zcml-settings"
    ...        class="fake_module.SettingsItem"
    ...        parent="fake_module.SidebarNavigation" />
    ... </configure>
    ... """, packages=(canonical.navigation,))
    <...ConfigurationMachine object at ...>

    >>> item = getMultiAdapter((context, request, sidebar),
    ...                        INavigationItem, "zcml-settings")
    >>> item
    <...ZCMLNavigationItem object at ...>

    >>> item.name
    'zcml-settings'

    >>> item.title
    'Settings'


In the case above, we've registered an existent class, but this
is not required for items.  We can use pure ZCML items as well.

    >>> zcml("""
    ... <configure xmlns="http://namespaces.zope.org/browser">
    ...    <navigationItem
    ...        name="zcml-pure"
    ...        title="Feedback"
    ...        action="/feedback"
    ...        parent="fake_module.SidebarNavigation" />
    ... </configure>
    ... """, packages=(canonical.navigation,))
    <...ConfigurationMachine object at ...>

    >>> item = getMultiAdapter((context, request, sidebar),
    ...                        INavigationItem, "zcml-pure")
    >>> item.title
    'Feedback'
    >>> item.action
    '/feedback'


We can get fancier items by providing details via TALES expressions.


    >>> zcml("""
    ... <configure xmlns="http://namespaces.zope.org/browser">
    ...    <navigationItem
    ...        name="zcml-pure-tales"
    ...        title_expr="string:TALES title!"
    ...        action_expr="string:/tales/action"
    ...        parent="fake_module.SidebarNavigation" />
    ... </configure>
    ... """, packages=(canonical.navigation,))
    <...ConfigurationMachine object at ...>

    >>> item = getMultiAdapter((context, request, sidebar),
    ...                        INavigationItem, "zcml-pure-tales")
    >>> item.title
    'TALES title!'
    >>> item.action
    '/tales/action'


Traversing to navigations
-------------------------

The navigation package also provides the needed means to extract
navigations when traversing paths. For instance:

  >>> from canonical.navigation.traversing import NavigationTraverser

  >>> sm.registerAdapter(NavigationTraverser, name="navigation")


Let's prepare a page template to be able to play with the registered
traverser.

  >>> from tempfile import NamedTemporaryFile
  >>> from zope.browserpage import ViewPageTemplateFile

  >>> class DummyView(object):
  ...     context = None
  ...     request = request

  >>> def render_page(content):
  ...     file = NamedTemporaryFile()
  ...     try:
  ...         file.write(content.strip())
  ...         file.flush()
  ...         print(ViewPageTemplateFile(file.name, _prefix="")(DummyView()))
  ...     finally:
  ...         file.close()

  >>> render_page(b"""
  ... <tal:loop repeat="item context/++navigation++zcml-sidebar">
  ...     <tal:span replace="item/title" />
  ... </tal:loop>
  ... """) # doctest: +NORMALIZE_WHITESPACE
  Dashboard
  ...
  Settings
  ...



Cleanup
-------

    >>> del sys.modules["fake_module"]


vim:ts=4:sw=4:et:ft=doctest

