============= 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') 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': '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', , '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', (, ), , 'Save'), ('adapter', (, ), , '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') '.'