[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