[Zope3-checkins] SVN: Zope3/trunk/ Implemented browser sub-menus. Boy, this was easy. I don't understand why

Stephan Richter srichter at cosmos.phy.tufts.edu
Sun Feb 27 16:54:18 EST 2005


Log message for revision 29324:
  Implemented browser sub-menus. Boy, this was easy. I don't understand why 
  I never grasped that before.
  
  

Changed:
  U   Zope3/trunk/doc/CHANGES.txt
  U   Zope3/trunk/src/zope/app/publisher/browser/configure.zcml
  U   Zope3/trunk/src/zope/app/publisher/browser/menu.py
  A   Zope3/trunk/src/zope/app/publisher/browser/menu.txt
  U   Zope3/trunk/src/zope/app/publisher/browser/meta.zcml
  U   Zope3/trunk/src/zope/app/publisher/browser/metadirectives.py
  U   Zope3/trunk/src/zope/app/publisher/browser/tests/menus.zcml
  U   Zope3/trunk/src/zope/app/publisher/browser/tests/test_menu.py
  U   Zope3/trunk/src/zope/app/publisher/browser/tests/test_menudirectives.py
  U   Zope3/trunk/src/zope/app/publisher/interfaces/browser.py

-=-
Modified: Zope3/trunk/doc/CHANGES.txt
===================================================================
--- Zope3/trunk/doc/CHANGES.txt	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/doc/CHANGES.txt	2005-02-27 21:54:18 UTC (rev 29324)
@@ -402,6 +402,8 @@
         adapters. Menu Item Types (in other words, menus) are now utilities
         that provide `IMenuItemType`.
 
+        + Implemented sub-menus.
+
         + Completes http://dev.zope.org/Zope3/AdaptersForMenuItems. New
           features such as sub-menus, icons, disabled entries and so on were
           not well defined and should be reconsidered in a different proposal.

Modified: Zope3/trunk/src/zope/app/publisher/browser/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/configure.zcml	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/configure.zcml	2005-02-27 21:54:18 UTC (rev 29324)
@@ -8,6 +8,9 @@
 <interface
   interface="zope.publisher.interfaces.browser.ISkin" />
 
+<interface
+  interface="zope.app.publisher.interfaces.browser.IMenuItemType" />
+
 <defaultLayer
   type="zope.publisher.interfaces.browser.IBrowserRequest"
   layer="zope.publisher.interfaces.browser.IDefaultBrowserLayer" />

Modified: Zope3/trunk/src/zope/app/publisher/browser/menu.py
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/menu.py	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/menu.py	2005-02-27 21:54:18 UTC (rev 29324)
@@ -18,11 +18,12 @@
 __docformat__ = "reStructuredText"
 from zope.component.interfaces import IFactory
 from zope.configuration.exceptions import ConfigurationError
+
 from zope.interface import Interface, implements, classImplements
 from zope.interface import directlyProvides, providedBy
 from zope.interface.interface import InterfaceClass
 from zope.publisher.interfaces.browser import IBrowserRequest
-from zope.security import checkPermission
+from zope.security import checkPermission, canAccess
 from zope.security.checker import InterfaceChecker, CheckerPublic
 from zope.security.interfaces import Unauthorized, Forbidden
 from zope.security.proxy import ProxyFactory, removeSecurityProxy
@@ -35,6 +36,7 @@
 from zope.app.publisher.browser import BrowserView
 from zope.app.publisher.interfaces.browser import IMenuAccessView
 from zope.app.publisher.interfaces.browser import IBrowserMenuItem
+from zope.app.publisher.interfaces.browser import IBrowserSubMenuItem
 from zope.app.publisher.interfaces.browser import IMenuItemType
 
 # Create special modules that contain all menu item types
@@ -47,157 +49,7 @@
 _order_counter = {}
 
 class BrowserMenuItem(BrowserView):
-    """Browser Menu Item Base Class
-
-    >>> from zope.publisher.browser import TestRequest
-
-    >>> class ITestInterface(Interface):
-    ...     pass
-
-    >>> from zope.publisher.interfaces.browser import IBrowserPublisher
-    >>> class TestObject(object):
-    ...     implements(IBrowserPublisher, ITestInterface)
-    ... 
-    ...     def foo(self):
-    ...         pass
-    ... 
-    ...     def browserDefault(self, r):
-    ...         return self, ()
-    ... 
-    ...     def publishTraverse(self, request, name):
-    ...         if name.startswith('f'):
-    ...             raise Forbidden, name
-    ...         if name.startswith('u'):
-    ...             raise Unauthorized, name
-    ...         return self.foo
-
-
-    Since the `BrowserMenuItem` is just a view, we can initiate it with an
-    object and a request.
-
-    >>> item = BrowserMenuItem(TestObject(), TestRequest())
-
-    Now we add a title and description 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
-
-    >>> from zope.security.management import newInteraction, endInteraction
-
-    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.
-    
-    >>> 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'f'
-    >>> item.available()
-    False
-    >>> item.action = u'u'
-    >>> 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.
-
-    >>> 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
-    """
+    """Browser Menu Item Class"""
     implements(IBrowserMenuItem)
 
     # See zope.app.publisher.interfaces.browser.IBrowserMenuItem
@@ -231,13 +83,13 @@
             try:
                 view = traverser.traverseRelativeURL(
                     self.request, self.context, path)
-                # TODO:
-                # tickle the security proxy's checker
-                # we're assuming that view pages are callable
-                # this is a pretty sound assumption
-                view.__call__
             except (Unauthorized, Forbidden):
                 return False
+            else:
+                # we're assuming that view pages are callable
+                # this is a pretty sound assumption
+                if not canAccess(view, '__call__'):
+                    return False
 
         # Make sure that we really want to see this menu item
         if self.filter is not None:
@@ -276,53 +128,26 @@
         return False
 
 
-def getMenu(menuItemType, object, request, max=999999):
-    """Return menu item entries in a TAL-friendly form.
+class BrowserSubMenuItem(BrowserMenuItem):
+    """Browser Menu Item Base Class"""
+    implements(IBrowserSubMenuItem)
 
-    >>> from zope.publisher.browser import TestRequest
+    # See zope.app.publisher.interfaces.browser.IBrowserSubMenuItem
+    submenuType = None
 
-    >>> from zope.app.testing import ztapi
-    >>> def defineMenuItem(menuItemType, for_, title, action=u'', order=0):
-    ...     newclass = type(title, (BrowserMenuItem,),
-    ...                     {'title':title, 'action':action, 'order':order})
-    ...     classImplements(newclass, menuItemType)
-    ...     ztapi.provideAdapter((for_, IBrowserRequest), menuItemType,
-    ...                          newclass, title)
+    def selected(self):
+        """See zope.app.publisher.interfaces.browser.IBrowserMenuItem"""
+        if self.action is u'':
+            return False
+        return super(BrowserSubMenuItem, self).selected()
 
-    >>> class IFoo(Interface): pass
-    >>> class IFooBar(IFoo): pass
-    >>> class IBlah(Interface): pass
 
-    >>> class FooBar(object):
-    ...     implements(IFooBar)
-
-    >>> class Menu1(Interface): pass
-    >>> class Menu2(Interface): pass
-
-    >>> defineMenuItem(Menu1, IFoo,    'i1')
-    >>> defineMenuItem(Menu1, IFooBar, 'i2')
-    >>> defineMenuItem(Menu1, IBlah,   'i3')
-    >>> defineMenuItem(Menu2, IFoo,    'i4')
-    >>> defineMenuItem(Menu2, IFooBar, 'i5')
-    >>> defineMenuItem(Menu2, IBlah,   'i6')
-    >>> defineMenuItem(Menu1, IFoo,    'i7', order=-1)
-
-    >>> items = getMenu(Menu1, FooBar(), TestRequest())
-    >>> [item['title'] for item in items]
-    ['i7', 'i1', 'i2']
-    >>> items = getMenu(Menu2, FooBar(), TestRequest())
-    >>> [item['title'] for item in items]
-    ['i4', 'i5']
-    >>> items = getMenu(Menu2, FooBar(), TestRequest())
-    >>> [item['title'] for item in items]
-    ['i4', 'i5']
-    """
+def getMenu(menuItemType, object, request):
+    """Return menu item entries in a TAL-friendly form."""
     result = []
     for name, item in zapi.getAdapters((object, request), menuItemType):
         if item.available():
             result.append(item)
-            if len(result) >= max:
-                break
         
     # Now order the result. This is not as easy as it seems.
     #
@@ -335,13 +160,16 @@
         for item in result]
     result.sort()
     
-    result = [{'title': item.title,
-               'description': item.description,
-               'action': item.action,
-               'selected': (item.selected() and u'selected') or u'',
-               'icon': item.icon,
-               'extra': item.extra}
-              for index, order, title, item in result]
+    result = [
+        {'title': item.title,
+         'description': item.description,
+         'action': item.action,
+         'selected': (item.selected() and u'selected') or u'',
+         'icon': item.icon,
+         'extra': item.extra,
+         'submenu': (IBrowserSubMenuItem.providedBy(item) and
+                     getMenu(item.submenuType, object, request)) or None}
+        for index, order, title, item in result]
     return result
 
 
@@ -352,6 +180,7 @@
         return items[0]
     return None
 
+
 class MenuAccessView(BrowserView):
     """A view allowing easy access to menus."""
     implements(IMenuAccessView)
@@ -364,68 +193,7 @@
 
 def menuDirective(_context, id=None, interface=None,
                   title=u'', description=u''):
-    """Provides a new menu (item type).
-
-    >>> import pprint
-    >>> 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()
-    >>> 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''
-
-    >>> hasattr(sys.modules['zope.app.menus'], 'menu1')
-    True
-
-    >>> del sys.modules['zope.app.menus'].menu1
-
-    Possibility 2: Just specify an interface
-    ----------------------------------------
-
-    >>> class menu1(Interface):
-    ...     pass
-
-    >>> context = Context()
-    >>> menuDirective(context, interface=menu1)
-    >>> context.actions[0]['args'][1] is menu1
-    True
-
-    Possibility 3: Specify an interface and an id
-    ---------------------------------------------
-
-    >>> context = Context()
-    >>> menuDirective(context, id='menu1', interface=menu1)
-    >>> context.actions[0]['args'][1] is menu1
-    True
-    >>> import pprint
-    >>> pprint.pprint([action['discriminator'] for action in context.actions])
-    [('browser', 'MenuItemType', 'zope.app.publisher.browser.menu.menu1'),
-     ('interface', 'zope.app.publisher.browser.menu.menu1'),
-     ('browser', 'MenuItemType', 'menu1')]
-     
-    Here are some disallowed configurations.
-
-    >>> context = Context()
-    >>> menuDirective(context)
-    Traceback (most recent call last):
-    ...
-    ConfigurationError: You must specify the 'id' or 'interface' attribute.
-    >>> menuDirective(context, title='Menu 1')
-    Traceback (most recent call last):
-    ...
-    ConfigurationError: You must specify the 'id' or 'interface' attribute.
-    """
+    """Provides a new menu (item type)."""
     if id is None and interface is None: 
         raise ConfigurationError(
             "You must specify the 'id' or 'interface' attribute.")
@@ -481,143 +249,46 @@
 def menuItemDirective(_context, menu, for_,
                       action, title, description=u'', icon=None, filter=None,
                       permission=None, extra=None, order=0):
-    """Register a single menu item.
-
-    See the `menuItemsDirective` class for tests.
-    """
+    """Register a single menu item."""
     return menuItemsDirective(_context, menu, for_).menuItem(
         _context, action, title, description, icon, filter,
         permission, extra, order)
 
+
+def subMenuItemDirective(_context, menu, for_, title, submenu,
+                         action=u'', description=u'', icon=None, filter=None,
+                         permission=None, extra=None, order=0):
+    """Register a single sub-menu menu item."""
+    return menuItemsDirective(_context, menu, for_).subMenuItem(
+        _context, submenu, title, description, action, icon, filter,
+        permission, extra, order)
+
+
 class MenuItemFactory(object):
-    # XXX this used to be a function created inline within menuItemsDirective,
-    # with the necessary values bound in context.  That approach may be
-    # faster than this one, but it does not encourage approachable doc tests.
-    # Please revise as desired, or remove this triple-X comment if this 
-    # solution is acceptable for now.
-    """generic factory for menu items.
-    
-    The factory needs a class to instantiate.  This will generally implement
-    IBrowserMenuItem.  Here is a dummy example.
-    
-    >>> 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 
-    described by the signature (and mapped to the IBrowserMenuItem interface).
-    We use dummy values for this example.
-    
-    >>> factory = MenuItemFactory(
-    ...     DummyBrowserMenuItem, 'Title', 'Description', 'Icon', 'Action',
-    ...     'Filter', 'zope.Public', 'Extra', 'Order', 'For_')
-    >>> factory.factory is DummyBrowserMenuItem
-    True
-    
-    The 'zope.Public' permission needs to be translated to CheckerPublic.
-    
-    >>> factory.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 = MenuItemFactory(
-    ...     DummyBrowserMenuItem, 'Title', 'Description', 'Icon', 'Action',
-    ...     'Filter', 'another.Permission', 'Extra', 'Order', 'For_')
-    >>> factory.permission
-    'another.Permission'
-    """
-    def __init__(self, factory, title, description, icon, action, filter, 
-                 permission, extra, order, for_):
+    """generic factory for menu items."""
+
+    def __init__(self, factory, **kwargs):
         self.factory = factory
-        self.title = title
-        self.description = description
-        self.icon = icon
-        self.action = action
-        self.filter = filter
-        if permission == 'zope.Public':
-            permission = CheckerPublic
-        self.permission = permission
-        self.extra = extra
-        self.order = order
-        self.for_ = for_
+        if 'permission' in kwargs and kwargs['permission'] == 'zope.Public':
+            kwargs['permission'] = CheckerPublic
+        self.kwargs = kwargs
     
     def __call__(self, context, request):
         item = self.factory(context, request)
-        item.title = self.title
-        item.description = self.description
-        item.icon = self.icon
-        item.action = self.action
-        item.filter = self.filter
-        # we could not set the permission if self.permission is CheckerPublic.
-        # choosing to be explicit for now.
-        item.permission = self.permission
-        item.extra = self.extra
-        item.order = self.order
-        item._for = self.for_
-        if self.permission is not None:
-            checker = InterfaceChecker(IBrowserMenuItem, self.permission)
+
+        for key, value in self.kwargs.items():
+            setattr(item, key, value)
+
+        if item.permission is not None:
+            checker = InterfaceChecker(IBrowserMenuItem, item.permission)
             item = proxify(item, checker)
+
         return item
 
+
 class menuItemsDirective(object):
-    """Register several menu items for a particular menu.
+    """Register several menu items for a particular menu."""
 
-    >>> class Context(object):
-    ...     info = u'doc'
-    ...     def __init__(self): self.actions = []
-    ...     def action(self, **kw): self.actions.append(kw)
-
-    >>> class TestMenuItemType(Interface): pass
-    >>> class ITest(Interface): pass
-
-    >>> context = Context()
-    >>> items = menuItemsDirective(context, TestMenuItemType, ITest)
-    >>> context.actions
-    []
-    >>> items.menuItem(context, u'view.html', 'View')
-    >>> context.actions[0]['args'][0]
-    'provideAdapter'
-    >>> len(context.actions)
-    4
-    """
     def __init__(self, _context, menu, for_):
         self.for_ = for_
         self.menuItemType = menu
@@ -633,10 +304,31 @@
             _order_counter[self.for_] = order + 1
 
         factory = MenuItemFactory(
-            BrowserMenuItem, title, description, icon, action, filter, 
-            permission, extra, order, self.for_)
+            BrowserMenuItem,
+            title=title, description=description, icon=icon, action=action,
+            filter=filter, permission=permission, extra=extra, order=order,
+            _for=self.for_)
         adapter(_context, (factory,), self.menuItemType,
                 (self.for_, IBrowserRequest), name=title)
+
+    def subMenuItem(self, _context, submenu, title, description=u'',
+                    action=u'', icon=None, filter=None, permission=None,
+                    extra=None, order=0):
+
+        if filter is not None:
+            filter = Engine.compile(filter)
+
+        if order == 0:
+            order = _order_counter.get(self.for_, 1)
+            _order_counter[self.for_] = order + 1
+
+        factory = MenuItemFactory(
+            BrowserSubMenuItem,
+            title=title, description=description, icon=icon, action=action,
+            filter=filter, permission=permission, extra=extra, order=order,
+            _for=self.for_, submenuType=submenu)
+        adapter(_context, (factory,), self.menuItemType,
+                (self.for_, IBrowserRequest), name=title)
         
     def __call__(self, _context):
         # Nothing to do.

Added: Zope3/trunk/src/zope/app/publisher/browser/menu.txt
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/menu.txt	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/menu.txt	2005-02-27 21:54:18 UTC (rev 29324)
@@ -0,0 +1,471 @@
+=============
+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')]


Property changes on: Zope3/trunk/src/zope/app/publisher/browser/menu.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/trunk/src/zope/app/publisher/browser/meta.zcml
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/meta.zcml	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/meta.zcml	2005-02-27 21:54:18 UTC (rev 29324)
@@ -118,6 +118,11 @@
           schema=".metadirectives.IMenuItemSubdirective"
           />
 
+      <meta:subdirective
+          name="subMenuItem"
+          schema=".metadirectives.ISubMenuItemSubdirective"
+          />
+
     </meta:complexDirective>
 
     <meta:directive
@@ -126,6 +131,12 @@
         handler=".menu.menuItemDirective"
         />
 
+    <meta:directive
+        name="subMenuItem"
+        schema=".metadirectives.ISubMenuItemDirective"
+        handler=".menu.subMenuItemDirective"
+        />
+
     <!-- misc. directives -->
 
     <meta:directive

Modified: Zope3/trunk/src/zope/app/publisher/browser/metadirectives.py
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/metadirectives.py	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/metadirectives.py	2005-02-27 21:54:18 UTC (rev 29324)
@@ -450,11 +450,8 @@
         default=0
         )
 
-
 class IMenuItemSubdirective(IMenuItem):
-    """
-    Define a menu item within a group of menu items
-    """
+    """Define a menu item within a group of menu items"""
 
     action = TextLine(
         title=u"The relative url to use if the item is selected",
@@ -465,13 +462,34 @@
         )
 
 class IMenuItemDirective(IMenuItemsDirective, IMenuItemSubdirective):
+    """Define one menu item"""
+
+class ISubMenuItemSubdirective(IMenuItem):
+    """Define a menu item that represents a a sub menu.
+
+    For a sub-menu menu item, the action is optional, this the item itself
+    might not represent a destination, but just an entry point to the sub menu. 
     """
-    Define one menu item
-    """
 
+    action = TextLine(
+        title=u"The relative url to use if the item is selected",
+        description=u"""
+        The url is relative to the object the menu is being displayed
+        for.""",
+        required=False
+        )
+
+    submenu = MenuField(
+        title=u"Sub-Menu name",
+        description=u"The menu that will be used to provide the sub-entries.",
+        required=True,
+        )
+    
+class ISubMenuItemDirective(IMenuItemsDirective, ISubMenuItemSubdirective):
+    """Define one menu item"""
+
 class IAddMenuItemDirective(IMenuItem):
-    """Define an add-menu item
-    """
+    """Define an add-menu item"""
 
     class_ = GlobalObject(
         title=u"Class",

Modified: Zope3/trunk/src/zope/app/publisher/browser/tests/menus.zcml
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/tests/menus.zcml	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/tests/menus.zcml	2005-02-27 21:54:18 UTC (rev 29324)
@@ -7,10 +7,15 @@
       id="test_id" 
       title="test menu" />
 
+  <browser:menu 
+      id="test_sub_id" 
+      title="test sub menu" />
+
   <browser:menuItems 
       menu="test_id" 
       for="zope.interface.Interface">
     <browser:menuItem action="a1" title="t1" />
+    <browser:subMenuItem submenu="test_sub_id" title="s1" />
   </browser:menuItems>
 
   <browser:menuItems 
@@ -41,4 +46,10 @@
     <browser:menuItem action="a9" title="t9" />
   </browser:menuItems>
 
+  <browser:menuItems 
+      menu="test_sub_id"
+      for=".tests.test_menudirectives.I111">
+    <browser:menuItem action="a10" title="t10" />
+  </browser:menuItems>
+
 </configure>

Modified: Zope3/trunk/src/zope/app/publisher/browser/tests/test_menu.py
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/tests/test_menu.py	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/tests/test_menu.py	2005-02-27 21:54:18 UTC (rev 29324)
@@ -16,15 +16,19 @@
 $Id$
 """
 import unittest
-from zope.testing.doctestunit import DocTestSuite
+from zope.testing import doctest, doctestunit
 
 from zope.app.testing import placelesssetup
 
 
 def test_suite():
-    return DocTestSuite('zope.app.publisher.browser.menu',
-                        setUp=placelesssetup.setUp,
-                        tearDown=placelesssetup.tearDown)
-    
-if __name__=='__main__':
-    unittest.main(defaultTest='test_suite')
+    return unittest.TestSuite((
+        doctest.DocFileSuite('../menu.txt',
+                             setUp=placelesssetup.setUp,
+                             tearDown=placelesssetup.tearDown,
+                             globs={'pprint': doctestunit.pprint},
+                             optionflags=doctest.NORMALIZE_WHITESPACE),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(default='test_suite')

Modified: Zope3/trunk/src/zope/app/publisher/browser/tests/test_menudirectives.py
===================================================================
--- Zope3/trunk/src/zope/app/publisher/browser/tests/test_menudirectives.py	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/browser/tests/test_menudirectives.py	2005-02-27 21:54:18 UTC (rev 29324)
@@ -76,12 +76,28 @@
         def d(n):
             return {'action': "a%s" % n,
                     'title':  "t%s" % n,
-                    'description':  "",
+                    'description': u'',
                     'selected': '',
+                    'submenu': None,
                     'icon': None,
                     'extra': None}
 
-        self.assertEqual(list(menu), [d(5), d(6), d(3), d(2), d(1)])
+        self.assertEqual(menu[:-1], [d(5), d(6), d(3), d(2), d(1)])
+        self.assertEqual(
+            menu[-1],
+            {'submenu': [{'submenu': None,
+                          'description': u'',
+                          'extra': None,
+                          'selected': u'',
+                          'action': u'a10',
+                          'title': u't10',
+                          'icon': None}],
+             'description': u'',
+             'extra': None,
+             'selected': u'',
+             'action': u'',
+             'title': u's1',
+             'icon': None})
 
         first = zope.app.publisher.browser.menu.getFirstMenuItem(
             test_id, TestObject(), TestRequest())

Modified: Zope3/trunk/src/zope/app/publisher/interfaces/browser.py
===================================================================
--- Zope3/trunk/src/zope/app/publisher/interfaces/browser.py	2005-02-27 18:25:58 UTC (rev 29323)
+++ Zope3/trunk/src/zope/app/publisher/interfaces/browser.py	2005-02-27 21:54:18 UTC (rev 29324)
@@ -19,7 +19,7 @@
 from zope.app.i18n import ZopeMessageIDFactory as _
 from zope.interface import Interface, directlyProvides
 from zope.interface.interfaces import IInterface
-from zope.schema import TextLine, Text, Choice, URI, Int
+from zope.schema import TextLine, Text, Choice, URI, Int, InterfaceField
 
 
 class IBrowserView(IView):
@@ -101,6 +101,25 @@
         due to security limitations or constraints.
         """
 
+class IBrowserSubMenuItem(IBrowserMenuItem):
+    """A menu item that points to a sub-menu."""
+
+    submenuType = InterfaceField(
+        title=_("Sub-Menu Type"),
+        description=_("The menu interface of the menu that describes the "
+                      "sub-menu below this item."),
+        required=True)
+        
+    action = TextLine(
+        title=_("The URL to display if the item is selected"),
+        description=_("When a user selects a browser menu item, the URL"
+                      "given in the action is displayed. The action is "
+                      "usually given as a relative URL, relative to the "
+                      "object the menu item is for."),
+       required=False
+       )
+
+
 class IMenuAccessView(Interface):
     """View that provides access to menus"""
 



More information about the Zope3-Checkins mailing list