Text File
menu.txt

Browser Menus

Browser menus are used to categorize browser actions, such as the views of a content component or the addable components of a container. In essence they provide the same functionality as menu bars in desktop application.

>>> from zope.app.publisher.browser import menu, menumeta

Menus are simple components that have an id, title and description. They also must provide a method called getMenuItems(object, request) that returns a TAL-friendly list of information dictionaries. We will see this in detail later. The default menu implementation, however, makes the menu be very transparent by identifying the menu through an interface. So let's define and register a simple edit menu:

>>> import zope.interface
>>> class EditMenu(zope.interface.Interface):
...     """This is an edit menu."""
>>> from zope.app.publisher.interfaces.browser import IMenuItemType
>>> zope.interface.directlyProvides(EditMenu, IMenuItemType)
>>> from zope.app.testing import ztapi
>>> ztapi.provideUtility(IMenuItemType, EditMenu, 'edit')

Now we have to create and register the menu itself:

>>> from zope.app.publisher.interfaces.browser import IBrowserMenu
>>> ztapi.provideUtility(
...     IBrowserMenu, menu.BrowserMenu('edit', u'Edit', u'Edit Menu'), 'edit')

Note that these steps seem like a lot of boilerplate, but all this work is commonly done for you via ZCML. An item in a menu is simply an adapter that provides. In the following section we will have a closer look at the browser menu item:

BrowserMenuItem class

The browser menu item represents an entry in the menu. Essentially, the menu item is a browser view of a content component. Thus we have to create a content component first:

>>> class IContent(zope.interface.Interface):
...     pass
>>> from zope.publisher.interfaces.browser import IBrowserPublisher
>>> from zope.security.interfaces import Unauthorized, Forbidden
>>> class Content(object):
...     zope.interface.implements(IContent, IBrowserPublisher)
...
...     def foo(self):
...         pass
...
...     def browserDefault(self, r):
...         return self, ()
...
...     def publishTraverse(self, request, name):
...         if name.startswith('fb'):
...             raise Forbidden, name
...         if name.startswith('ua'):
...             raise Unauthorized, name
...         if name.startswith('le'):
...             raise LookupError, name
...         return self.foo

We also implemented the IBrowserPublisher interface, because we want to make the object traversable, so that we can make availability checks later.

Since the BrowserMenuItem is just a view, we can initiate it with an object and a request.

>>> from zope.publisher.browser import TestRequest
>>> item = menu.BrowserMenuItem(Content(), TestRequest())

Note that the menu item knows nothing about the menu itself. It purely depends on the adapter registration to determine in which menu it will appear. The advantage is that a menu item can be reused in several menus.

Now we add a title, description, order and icon and see whether we can then access the value. Note that these assignments are always automatically done by the framework.

>>> item.title = u'Item 1'
>>> item.title
u'Item 1'
>>> item.description = u'This is Item 1.'
>>> item.description
u'This is Item 1.'
>>> item.order
0
>>> item.order = 1
>>> item.order
1
>>> item.icon is None
True
>>> item.icon = u'/@@/icon.png'
>>> item.icon
u'/@@/icon.png'

Since there is no permission or view specified yet, the menu item should be available and not selected.

>>> item.available()
True
>>> item.selected()
False

There are two ways to deny availability of a menu item: (1) the current user does not have the correct permission to access the action or the menu item itself, or (2) the filter returns False, in which case the menu item should also not be shown.

>>> from zope.security.interfaces import IPermission
>>> from zope.security.permission import Permission
>>> from zope.app.testing import ztapi
>>> perm = Permission('perm', 'Permission')
>>> ztapi.provideUtility(IPermission, perm, 'perm')
>>> class ParticipationStub(object):
...     principal = 'principal'
...     interaction = None

In the first case, the permission of the menu item was explicitely specified. Make sure that the user needs this permission to make the menu item available.

>>> item.permission = perm

Now, we are not setting any user. This means that the menu item should be available.

>>> from zope.security.management import newInteraction, endInteraction
>>> endInteraction()
>>> newInteraction()
>>> item.available()
True

Now we specify a principal that does not have the specified permission.

>>> endInteraction()
>>> newInteraction(ParticipationStub())
>>> item.available()
False

In the second case, the permission is not explicitely defined and the availability is determined by the permission required to access the action.

>>> item.permission = None

All views starting with 'fb' are forbidden, the ones with 'ua' are unauthorized and all others are allowed.

>>> item.action = u'fb'
>>> item.available()
False
>>> item.action = u'ua'
>>> item.available()
False
>>> item.action = u'a'
>>> item.available()
True

Also, sometimes a menu item might be registered for a view that does not exist. In those cases the traversal mechanism raises a TraversalError, which is a special type of LookupError. All actions starting with le should raise this error:

>>> item.action = u'le'
>>> item.available()
False

Now let's test filtering. If the filter is specified, it is assumed to be a TALES obejct.

>>> from zope.app.pagetemplate.engine import Engine
>>> item.action = u'a'
>>> item.filter = Engine.compile('not:context')
>>> item.available()
False
>>> item.filter = Engine.compile('context')
>>> item.available()
True

Finally, make sure that the menu item can be selected.

>>> item.request = TestRequest(SERVER_URL='http://127.0.0.1/@@view.html',
...                            PATH_INFO='/@@view.html')
>>> item.selected()
False
>>> item.action = u'view.html'
>>> item.selected()
True
>>> item.action = u'@@view.html'
>>> item.selected()
True
>>> item.request = TestRequest(
...     SERVER_URL='http://127.0.0.1/++view++view.html',
...     PATH_INFO='/++view++view.html')
>>> item.selected()
True
>>> item.action = u'otherview.html'
>>> item.selected()
False

BrowserSubMenuItem class

The menu framework also allows for submenus. Submenus can be inserted by creating a special menu item that simply points to another menu to be inserted:

>>> item = menu.BrowserSubMenuItem(Content(), TestRequest())

The framework will always set the sub-menu automatically (we do it manually here):

>>> class SaveOptions(zope.interface.Interface):
...     "A sub-menu that describes available save options for the content."
>>> zope.interface.directlyProvides(SaveOptions, IMenuItemType)
>>> ztapi.provideUtility(IMenuItemType, SaveOptions, 'save')
>>> ztapi.provideUtility(IBrowserMenu,
...                      menu.BrowserMenu('save', u'Save', u'Save Menu'),
...                      'save')

Now we can assign the sub-menu id to the menu item:

>>> item.submenuId = 'save'

Also, the action attribute for the browser sub-menu item is optional, because you often do not want the item itself to represent something. The rest of the class is identical to the BrowserMenuItem class.

Getting a Menu

Now that we know how the single menu item works, let's have a look at how menu items get put together to a menu. But let's first create some menu items and register them as adapters with the component architecture.

Register the edit menu entries first. We use the menu item factory to create the items:

>>> from zope.app.testing import ztapi
>>> from zope.publisher.interfaces.browser import IBrowserRequest
>>> undo = menumeta.MenuItemFactory(menu.BrowserMenuItem, title="Undo",
...                                 action="undo.html")
>>> ztapi.provideAdapter((IContent, IBrowserRequest), EditMenu, undo, 'undo')
>>> redo = menumeta.MenuItemFactory(menu.BrowserMenuItem, title="Redo",
...                                 action="redo.html", icon="/@@/redo.png")
>>> ztapi.provideAdapter((IContent, IBrowserRequest), EditMenu, redo, 'redo')
>>> save = menumeta.MenuItemFactory(menu.BrowserSubMenuItem, title="Save",
...                                 submenuId='save', order=2)
>>> ztapi.provideAdapter((IContent, IBrowserRequest), EditMenu, save, 'save')

And now the save options:

>>> saveas = menumeta.MenuItemFactory(menu.BrowserMenuItem, title="Save as",
...                                   action="saveas.html")
>>> ztapi.provideAdapter((IContent, IBrowserRequest),
...                      SaveOptions, saveas, 'saveas')
>>> saveall = menumeta.MenuItemFactory(menu.BrowserMenuItem, title="Save all",
...                                    action="saveall.html")
>>> ztapi.provideAdapter((IContent, IBrowserRequest),
...                      SaveOptions, saveall, 'saveall')

Note that we can also register menu items for classes:

>>> new = menumeta.MenuItemFactory(menu.BrowserMenuItem, title="New",
...                                 action="new.html", _for=Content)
>>> ztapi.provideAdapter((Content, IBrowserRequest), EditMenu, new, 'new')

The utility that is used to generate the menu into a TAL-friendly data-structure is getMenu():

getMenu(menuId, object, request)

where menuId is the id originally specified for the menu. Let's look up the menu now:

>>> pprint(menu.getMenu('edit', Content(), TestRequest()))
[{'action': 'new.html',
  'description': u'',
  'extra': None,
  'icon': None,
  'selected': u'',
  'submenu': None,
  'title': 'New'},
{'action': 'redo.html',
  'description': u'',
  'extra': None,
  'icon': '/@@/redo.png',
  'selected': u'',
  'submenu': None,
  'title': 'Redo'},
 {'action': 'undo.html',
  'description': u'',
  'extra': None,
  'icon': None,
  'selected': u'',
  'submenu': None,
  'title': 'Undo'},
 {'action': u'',
  'description': u'',
  'extra': None,
  'icon': None,
  'selected': u'',
  'submenu': [{'action': 'saveall.html',
               'description': u'',
               'extra': None,
               'icon': None,
               'selected': u'',
               'submenu': None,
               'title': 'Save all'},
              {'action': 'saveas.html',
               'description': u'',
               'extra': None,
               'icon': None,
               'selected': u'',
               'submenu': None,
               'title': 'Save as'}],
  'title': 'Save'}]

Custom IBrowserMenu Implementations

Until now we have only seen how to use the default menu implementation. Much of the above boilerplate was necessary just to support custom menus. But what could custom menus do? Sometimes menu items are dynamically generated based on a certain state of the object the menu is for. For example, you might want to show all items in a folder-like component. So first let's create this folder-like component:

>>> class Folderish(Content):
...     names = ['README.txt', 'logo.png', 'script.py']

Now we create a menu using the names to create a menu:

>>> from zope.app.publisher.interfaces.browser import IBrowserMenu
>>> class Items(object):
...     zope.interface.implements(IBrowserMenu)
...
...     def __init__(self, id, title=u'', description=u''):
...         self.id = id
...         self.title = title
...         self.description = description
...
...     def getMenuItems(self, object, request):
...         return [{'title': name,
...                  'description': None,
...                  'action': name + '/manage',
...                  'selected': u'',
...                  'icon': None,
...                  'extra': {},
...                  'submenu': None}
...                 for name in object.names]

and register it:

>>> ztapi.provideUtility(IBrowserMenu,
...                      Items('items', u'Items', u'Items Menu'),
...                      'items')

We can now get the menu items using the previously introduced API:

>>> pprint(menu.getMenu('items', Folderish(), TestRequest()))
[{'action': 'README.txt/manage',
  'description': None,
  'extra': {},
  'icon': None,
  'selected': u'',
  'submenu': None,
  'title': 'README.txt'},
 {'action': 'logo.png/manage',
  'description': None,
  'extra': {},
  'icon': None,
  'selected': u'',
  'submenu': None,
  'title': 'logo.png'},
 {'action': 'script.py/manage',
  'description': None,
  'extra': {},
  'icon': None,
  'selected': u'',
  'submenu': None,
  'title': 'script.py'}]

MenuItemFactory class

As you have seen above already, we have used the menu item factory to generate adapter factories for menu items. The factory needs a particular IBrowserMenuItem class to instantiate. Here is an example using a dummy menu item class:

>>> class DummyBrowserMenuItem(object):
...     "a dummy factory for menu items"
...     def __init__(self, context, request):
...         self.context = context
...         self.request = request

To instantiate this class, pass the factory and the other arguments as keyword arguments (every key in the arguments should map to an attribute of the menu item class). We use dummy values for this example.

>>> factory = menumeta.MenuItemFactory(
...     DummyBrowserMenuItem, title='Title', description='Description',
...     icon='Icon', action='Action', filter='Filter',
...     permission='zope.Public', extra='Extra', order='Order', _for='For')
>>> factory.factory is DummyBrowserMenuItem
True

The "zope.Public" permission needs to be translated to CheckerPublic.

>>> from zope.security.checker import CheckerPublic
>>> factory.kwargs['permission'] is CheckerPublic
True

Call the factory with context and request to return the instance. We continue to use dummy values.

>>> item = factory('Context', 'Request')

The returned value should be an instance of the DummyBrowserMenuItem, and have all of the values we initially set on the factory.

>>> isinstance(item, DummyBrowserMenuItem)
True
>>> item.context
'Context'
>>> item.request
'Request'
>>> item.title
'Title'
>>> item.description
'Description'
>>> item.icon
'Icon'
>>> item.action
'Action'
>>> item.filter
'Filter'
>>> item.permission is CheckerPublic
True
>>> item.extra
'Extra'
>>> item.order
'Order'
>>> item._for
'For'

If you pass a permission other than zope.Public to the MenuItemFactory, it should pass through unmodified.

>>> factory = menumeta.MenuItemFactory(
...     DummyBrowserMenuItem, title='Title', description='Description',
...     icon='Icon', action='Action', filter='Filter',
...     permission='another.Permission', extra='Extra', order='Order',
...     _for='For_')
>>> factory.kwargs['permission']
'another.Permission'

Directive Handlers

menu Directive Handler

Provides a new menu (item type).

>>> class Context(object):
...     info = u'doc'
...     def __init__(self):
...         self.actions = []
...
...     def action(self, **kw):
...         self.actions.append(kw)
Possibility 1: The Old Way
>>> context = Context()
>>> menumeta.menuDirective(context, u'menu1', title=u'Menu 1')
>>> iface = context.actions[0]['args'][1]
>>> iface.getName()
u'menu1'
>>> import sys
>>> hasattr(sys.modules['zope.app.menus'], 'menu1')
True
>>> del sys.modules['zope.app.menus'].menu1
Possibility 2: Just specify an interface
>>> class menu1(zope.interface.Interface):
...     pass
>>> context = Context()
>>> menumeta.menuDirective(context, interface=menu1)
>>> context.actions[0]['args'][1] is menu1
True
Possibility 3: Specify an interface and an id
>>> context = Context()
>>> menumeta.menuDirective(context, id='menu1', interface=menu1)
>>> pprint([action['discriminator'] for action in context.actions])
[('browser', 'MenuItemType', '__builtin__.menu1'),
 ('interface', '__builtin__.menu1'),
 ('browser', 'MenuItemType', 'menu1'),
 ('utility',
  <InterfaceClass zope.app.publisher.interfaces.browser.IBrowserMenu>,
  'menu1'),
 None]

Here are some disallowed configurations.

>>> context = Context()
>>> menumeta.menuDirective(context)
Traceback (most recent call last):
...
ConfigurationError: You must specify the 'id' or 'interface' attribute.
>>> menumeta.menuDirective(context, title='Menu 1')
Traceback (most recent call last):
...
ConfigurationError: You must specify the 'id' or 'interface' attribute.

menuItems Directive Handler

Register several menu items for a particular menu.

>>> class TestMenuItemType(zope.interface.Interface):
...     pass
>>> class ITest(zope.interface.Interface):
...     pass
>>> context = Context()
>>> items = menumeta.menuItemsDirective(context, TestMenuItemType, ITest)
>>> context.actions
[]
>>> items.menuItem(context, u'view.html', 'View')
>>> items.subMenuItem(context, SaveOptions, 'Save')
>>> disc = [action['discriminator'] for action in context.actions]
>>> disc.sort()
>>> pprint(disc[-2:])
[('adapter',
  (<InterfaceClass __builtin__.ITest>,
   <InterfaceClass zope.publisher.interfaces.browser.IDefaultBrowserLayer>),
  <InterfaceClass __builtin__.TestMenuItemType>,
  'Save'),
 ('adapter',
  (<InterfaceClass __builtin__.ITest>,
   <InterfaceClass zope.publisher.interfaces.browser.IDefaultBrowserLayer>),
  <InterfaceClass __builtin__.TestMenuItemType>,
  'View')]

ManagementViewSelector

Test the action (redirect url) of the first menu item and ensure that we don't redirect to the parent before we reach the item if we use actions like: '../' 'javascript:popup()' or '++namespace++'.

First setup the 'zmi_views' menu.

>>> class ZMIViews(zope.interface.Interface):
...     """This is the zmi_views menu."""
>>> zope.interface.directlyProvides(ZMIViews, IMenuItemType)
>>> ztapi.provideUtility(IMenuItemType, ZMIViews, 'zmi_views')
>>> from zope.app.publisher.interfaces.browser import IBrowserMenu
>>> ztapi.provideUtility(
...     IBrowserMenu, menu.BrowserMenu('zmi_views', u'ZMIViews',
...     u'ZMI Views'), 'zmi_views')

Register some 'zmi_views' menu items.

>>> first = menumeta.MenuItemFactory(menu.BrowserMenuItem, title="First",
...     action="../")
>>> ztapi.provideAdapter((IContent, IBrowserRequest), ZMIViews, first,
...    'first')
>>> second = menumeta.MenuItemFactory(menu.BrowserMenuItem, title="Second",
...     action="second.html")
>>> ztapi.provideAdapter((IContent, IBrowserRequest), ZMIViews, second,
...    'second')

Now create a new content object.

>>> content = Content()
>>> request = TestRequest(SERVER_URL='http://127.0.0.1/',
...     PATH_INFO='/')

Test the actions. Remember that this action get redirected before we traverse to the content. This means actions like ../ will make it impossible to access the view because the ../ will redirect us before we traverse.

>>> menus = menu.getMenu('zmi_views', content, request)
>>> [menu['action'] for menu in menus]
['../', 'second.html']

Now call the ManagementViewSelector view and we get a empty string as result.

>>> from zope.app.publisher.browser.managementviewselector import \
...     ManagementViewSelector
>>> view = ManagementViewSelector(content, request)
>>> view()
u''

Now check the more interesting redirect in the request. The redirect location is not like excpected '../'. We get '.' as the redirect location. This is important otherwise we get redirected to the parent if we call the first menu item with actions like '../parentview.html' instead of traverse to the item.

>>> request.response.getHeader('Location')
'.'