=============
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

The concept of a menu is more of a pattern than concrete
implementation. Interfaces are used to denote a menu. So let's define a simple
edit menu:

  >>> import zope.interface
  >>> class EditMenu(zope.interface.Interface):
  ...     """This is an edit menu."""

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
  ...         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.app.testing import ztapi
  >>> from zope.app.security.interfaces import IPermission
  >>> from zope.app.security.permission import Permission
  >>> 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 'f' are forbidden, the ones with 'u' 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

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.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 type automatically (we do it
manually here):

  >>> class SaveOptions(zope.interface.Interface):
  ...     "A sub-menu that describes available save options for the content."

  >>> item.submenuType = SaveOptions

  >>> item.submenuType
  <InterfaceClass __builtin__.SaveOptions>

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 = menu.MenuItemFactory(menu.BrowserMenuItem, title="Undo", 
  ...                             action="undo.html")
  >>> ztapi.provideAdapter((IContent, IBrowserRequest), EditMenu, undo, 'undo')

  >>> redo = menu.MenuItemFactory(menu.BrowserMenuItem, title="Redo",
  ...                             action="redo.html", icon="/@@/redo.png")
  >>> ztapi.provideAdapter((IContent, IBrowserRequest), EditMenu, redo, 'redo')

  >>> save = menu.MenuItemFactory(menu.BrowserSubMenuItem, title="Save", 
  ...                             submenuType=SaveOptions, order=2)
  >>> ztapi.provideAdapter((IContent, IBrowserRequest), EditMenu, save, 'save')

And now the save options:

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

  >>> saveall = menu.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(menuItemType, object, request)

where `menuItemType` is the menu interface. Let's look up the menu now:

  >>> pprint(menu.getMenu(EditMenu, 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'}]


`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 = menu.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 = menu.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()
  >>> menu.menuDirective(context, u'menu1', title=u'Menu 1')
  >>> iface = context.actions[0]['args'][1]
  >>> iface.getName()
  u'menu1'
  >>> iface.getTaggedValue('title')
  u'Menu 1'
  >>> iface.getTaggedValue('description')
  u''

  >>> 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()
  >>> menu.menuDirective(context, interface=menu1)
  >>> context.actions[0]['args'][1] is menu1
  True

Possibility 3: Specify an interface and an id
+++++++++++++++++++++++++++++++++++++++++++++

  >>> context = Context()
  >>> menu.menuDirective(context, id='menu1', interface=menu1)

  >>> pprint([action['discriminator'] for action in context.actions])
  [('browser', 'MenuItemType', '__builtin__.menu1'),
   ('interface', '__builtin__.menu1'),
   ('browser', 'MenuItemType', 'menu1')]
   
Here are some disallowed configurations.

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

  >>> menu.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 = menu.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.IBrowserRequest>),
    <InterfaceClass __builtin__.TestMenuItemType>,
    'Save'),
   ('adapter',
    (<InterfaceClass __builtin__.ITest>,
     <InterfaceClass zope.publisher.interfaces.browser.IBrowserRequest>),
    <InterfaceClass __builtin__.TestMenuItemType>,
    'View')]
