diff --git a/browser/fields.py b/browser/fields.py new file mode 100644 index 0000000..c745656 --- /dev/null +++ b/browser/fields.py @@ -0,0 +1,110 @@ +############################################################################# +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser-Presentation related Fields. + +$Id$ +""" +__docformat__ = 'restructuredtext' + +import zope.schema +from zope.component.exceptions import ComponentLookupError +from zope.configuration.exceptions import ConfigurationError +from zope.configuration.fields import GlobalObject +from zope.interface.interfaces import IInterface +from zope.app.publisher.interfaces.browser import IMenuItemType + +from zope.app import zapi + + +class MenuField(GlobalObject): + r"""This fields represents a menu (item type). + + Besides being able to look up the menu by importing it, we also try + to look up the name in the utility service. + + >>> from zope.interface import directlyProvides + >>> from zope.interface.interface import InterfaceClass + + >>> menu1 = InterfaceClass('menu1', (), + ... __doc__='Menu Item Type: menu1', + ... __module__='zope.app.menus') + >>> directlyProvides(menu1, IMenuItemType) + + >>> menus = None + >>> class Resolver(object): + ... def resolve(self, path): + ... if path.startswith('zope.app.menus') and \ + ... hasattr(menus, 'menu1') or \ + ... path == 'zope.app.component.menus.menu1': + ... return menu1 + ... raise ConfigurationError, 'menu1' + + >>> field = MenuField() + >>> field = field.bind(Resolver()) + + Test 1: Import the menu + ----------------------- + + >>> field.fromUnicode('zope.app.component.menus.menu1') is menu1 + True + + Test 2: We have a shortcut name. Import the menu from `zope.app.menus1`. + ------------------------------------------------------------------------ + + >>> from types import ModuleType as module + >>> import sys + >>> menus = module('menus') + >>> old = sys.modules.get('zope.app.menus', None) + >>> sys.modules['zope.app.menus'] = menus + >>> setattr(menus, 'menu1', menu1) + + >>> field.fromUnicode('menu1') is menu1 + True + + >>> if old is not None: + ... sys.modules['zope.app.menus'] = old + + Test 3: Get the menu from the utility service + --------------------------------------------- + + >>> from zope.app.testing import ztapi + >>> ztapi.provideUtility(IMenuItemType, menu1, 'menu1') + + >>> field.fromUnicode('menu1') is menu1 + True + """ + + def fromUnicode(self, u): + name = str(u.strip()) + + try: + value = zapi.queryUtility(IMenuItemType, name) + except ComponentLookupError: + # The component architecture is not up and running. + pass + else: + if value is not None: + self.validate(value) + return value + + try: + value = self.context.resolve('zope.app.menus.'+name) + except ConfigurationError, v: + try: + value = self.context.resolve(name) + except ConfigurationError, v: + raise zope.schema.ValidationError(v) + + self.validate(value) + return value diff --git a/browser/i18nresourcemeta.py b/browser/i18nresourcemeta.py new file mode 100644 index 0000000..4fe981a --- /dev/null +++ b/browser/i18nresourcemeta.py @@ -0,0 +1,124 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser configuration code + +$Id$ +""" +from zope.configuration.exceptions import ConfigurationError +from zope.interface import Interface +from zope.publisher.interfaces.browser import IBrowserRequest +from zope.publisher.interfaces.browser import IDefaultBrowserLayer +from zope.security.proxy import Proxy +from zope.security.checker import CheckerPublic, Checker + +from zope.app import zapi +from zope.app.component.metaconfigure import handler +from zope.app.publisher.fileresource import File, Image + +from i18nfileresource import I18nFileResourceFactory + + +class I18nResource(object): + + type = IBrowserRequest + default_allowed_attributes = '__call__' + + def __init__(self, _context, name=None, defaultLanguage='en', + layer=IDefaultBrowserLayer, permission=None): + self._context = _context + self.name = name + self.defaultLanguage = defaultLanguage + self.layer = layer + self.permission = permission + self.__data = {} + self.__format = None + + def translation(self, _context, language, file=None, image=None): + + if file is not None and image is not None: + raise ConfigurationError( + "Can't use more than one of file, and image " + "attributes for resource directives" + ) + elif file is not None: + if self.__format is not None and self.__format != File: + raise ConfigurationError( + "Can't use both files and images in the same " + "i18n-resource directive" + ) + self.__data[language] = File(_context.path(file), self.name) + self.__format = File + elif image is not None: + if self.__format is not None and self.__format != Image: + raise ConfigurationError( + "Can't use both files and images in the same " + "i18n-resource directive" + ) + self.__data[language] = Image(_context.path(image), self.name) + self.__format = Image + else: + raise ConfigurationError( + "At least one of the file, and image " + "attributes for resource directives must be specified" + ) + + return () + + + def __call__(self, require = None): + if self.name is None: + return () + + if not self.__data.has_key(self.defaultLanguage): + raise ConfigurationError( + "A translation for the default language (%s) " + "must be specified" % self.defaultLanguage + ) + + permission = self.permission + factory = I18nFileResourceFactory(self.__data, self.defaultLanguage) + + if permission: + if require is None: + require = {} + + if permission == 'zope.Public': + permission = CheckerPublic + + if require: + checker = Checker(require) + + factory = self._proxyFactory(factory, checker) + + self._context.action( + discriminator = ('i18n-resource', self.name, self.type, self.layer), + callable = handler, + args = ('provideAdapter', + (self.layer,), Interface, self.name, factory, + self._context.info) + ) + + + def _proxyFactory(self, factory, checker): + def proxyView(request, + factory=factory, checker=checker): + resource = factory(request) + + # We need this in case the resource gets unwrapped and + # needs to be rewrapped + resource.__Security_checker__ = checker + + return Proxy(resource, checker) + + return proxyView diff --git a/browser/icon.py b/browser/icon.py new file mode 100644 index 0000000..2525656 --- /dev/null +++ b/browser/icon.py @@ -0,0 +1,109 @@ +############################################################################## +# +# Copyright (c) 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Icon support + +$Id$ +""" +import os +import re + +from zope.interface import Interface +from zope.publisher.interfaces.browser import IDefaultBrowserLayer +from zope.configuration.exceptions import ConfigurationError + +from zope.app import zapi +from zope.app.component.interface import provideInterface +from zope.app.component.metaconfigure import handler +from zope.app.publisher.browser import metaconfigure +from zope.app.traversing.namespace import getResource + +IName = re.compile('I[A-Z][a-z]') + +class IconView(object): + + def __init__(self, context, request, rname, alt): + self.context = context + self.request = request + self.rname = rname + self.alt = alt + + def __call__(self): + # The context is important here, since it becomes the parent of the + # icon, which is needed to generate the absolute URL. + resource = getResource(self.context, self.rname, self.request) + src = resource() + + return ('%s' + % (src, self.alt)) + + def url(self): + resource = getResource(self.context, self.rname, self.request) + src = resource() + return src + +class IconViewFactory(object): + + def __init__(self, rname, alt): + self.rname = rname + self.alt = alt + + def __call__(self, context, request): + return IconView(context, request, self.rname, self.alt) + +def IconDirective(_context, name, for_, file=None, resource=None, + layer=IDefaultBrowserLayer, alt=None): + + iname = for_.getName() + + if alt is None: + alt = iname + if IName.match(alt): + alt = alt[1:] # Remove leading 'I' + + if file is not None and resource is not None: + raise ConfigurationError( + "Can't use more than one of file, and resource " + "attributes for icon directives" + ) + elif file is not None: + resource = '-'.join(for_.__module__.split('.')) + resource = "%s-%s-%s" % (resource, iname, name) + ext = os.path.splitext(file)[1] + if ext: + resource += ext + metaconfigure.resource(_context, image=file, + name=resource, layer=layer) + elif resource is None: + raise ConfigurationError( + "At least one of the file, and resource " + "attributes for resource directives must be specified" + ) + + vfactory = IconViewFactory(resource, alt) + + _context.action( + discriminator = ('view', name, vfactory, layer), + callable = handler, + args = ('provideAdapter', + (for_, layer), Interface, name, vfactory, _context.info) + ) + + _context.action( + discriminator = None, + callable = provideInterface, + args = (for_.__module__+'.'+for_.getName(), + for_) + ) + + diff --git a/browser/menu.py b/browser/menu.py new file mode 100644 index 0000000..ec8617b --- /dev/null +++ b/browser/menu.py @@ -0,0 +1,556 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Menu Registration code. + +$Id$ +""" +__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.checker import InterfaceChecker, CheckerPublic +from zope.security.interfaces import Unauthorized, Forbidden +from zope.security.proxy import ProxyFactory, removeSecurityProxy + +from zope.app import zapi +from zope.app.component.interface import provideInterface +from zope.app.component.metaconfigure import adapter, proxify +from zope.app.pagetemplate.engine import Engine +from zope.app.publication.browser import PublicationTraverser +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 IMenuItemType + +# Create special modules that contain all menu item types +from types import ModuleType as module +import sys +menus = module('menus') +sys.modules['zope.app.menus'] = menus + + +_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 + """ + implements(IBrowserMenuItem) + + # See zope.app.publisher.interfaces.browser.IBrowserMenuItem + title = u'' + description = u'' + action = u'' + extra = None + order = 0 + permission = None + filter = None + icon = None + _for = Interface + + def available(self): + """See zope.app.publisher.interfaces.browser.IBrowserMenuItem""" + # Make sure we have the permission needed to access the menu's action + if self.permission is not None: + # If we have an explicit permission, check that we + # can access it. + if not checkPermission(self.permission, self.context): + return False + + elif self.action != u'': + # Otherwise, test access by attempting access + path = self.action + l = self.action.find('?') + if l >= 0: + path = self.action[:l] + + traverser = PublicationTraverser() + 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 + + # Make sure that we really want to see this menu item + if self.filter is not None: + + try: + include = self.filter(Engine.getContext( + context = self.context, + nothing = None, + request = self.request, + modules = ProxyFactory(sys.modules), + )) + except Unauthorized: + return False + else: + if not include: + return False + + return True + + + def selected(self): + """See zope.app.publisher.interfaces.browser.IBrowserMenuItem""" + request_url = self.request.getURL() + + normalized_action = self.action + if self.action.startswith('@@'): + normalized_action = self.action[2:] + + if request_url.endswith('/'+normalized_action): + return True + if request_url.endswith('/++view++'+normalized_action): + return True + if request_url.endswith('/@@'+normalized_action): + return True + + return False + + +def getMenu(menuItemType, object, request, max=999999): + """Return menu item entries in a TAL-friendly form. + + >>> from zope.publisher.browser import TestRequest + + >>> 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) + + >>> 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'] + """ + 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. + # + # (1) Look at the interfaces and put the more specific menu entries to the + # front. + # (2) Sort unabigious entries by order and then by title. + ifaces = list(providedBy(removeSecurityProxy(object)).__iro__) + result = [ + (ifaces.index(item._for or Interface), item.order, item.title, item) + 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] + return result + + +def getFirstMenuItem(menuItemType, object, request): + """Get the first item of a menu.""" + items = getMenu(menuItemType, object, request) + if items: + return items[0] + return None + +class MenuAccessView(BrowserView): + """A view allowing easy access to menus.""" + implements(IMenuAccessView) + + def __getitem__(self, typeString): + # Convert the menu item type identifyer string to the type interface + menuItemType = zapi.getUtility(IMenuItemType, typeString) + return getMenu(menuItemType, self.context, self.request) + + +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. + """ + if id is None and interface is None: + raise ConfigurationError( + "You must specify the 'id' or 'interface' attribute.") + + if interface is None: + interface = InterfaceClass(id, (), + __doc__='Menu Item Type: %s' %id, + __module__='zope.app.menus') + # Add the menu item type to the `menus` module. + # Note: We have to do this immediately, so that directives using the + # MenuField can find the menu item type. + setattr(menus, id, interface) + path = 'zope.app.menus.' + id + else: + path = interface.__module__ + '.' + interface.getName() + + # If an id was specified, make this menu available under this id. + # Note that the menu will be still available under its path, since it + # is an adapter, and the `MenuField` can resolve paths as well. + if id is None: + id = path + else: + # Make the interface available in the `zope.app.menus` module, so + # that other directives can find the interface under the name + # before the CA is setup. + _context.action( + discriminator = ('browser', 'MenuItemType', path), + callable = provideInterface, + args = (path, interface, IMenuItemType, _context.info) + ) + setattr(menus, id, interface) + + # Set the title and description of the menu item type + interface.setTaggedValue('title', title) + interface.setTaggedValue('description', description) + + # Register the layer interface as an interface + _context.action( + discriminator = ('interface', path), + callable = provideInterface, + args = (path, interface), + kw = {'info': _context.info} + ) + + # Register the menu item type interface as an IMenuItemType + _context.action( + discriminator = ('browser', 'MenuItemType', id), + callable = provideInterface, + args = (id, interface, IMenuItemType, _context.info) + ) + + +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. + """ + return menuItemsDirective(_context, menu, for_).menuItem( + _context, action, title, description, icon, filter, + permission, extra, order) + + +class menuItemsDirective(object): + """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 + + def menuItem(self, _context, action, title, description=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 + + def MenuItemFactory(context, request): + item = BrowserMenuItem(context, request) + item.title = title + item.description = description + item.icon = icon + item.action = action + item.filter = filter + item.permission = permission + item.extra = extra + item.order = order + item._for = self.for_ + + if permission is not None: + if permission == 'zope.Public': + perm = CheckerPublic + else: + perm = permission + checker = InterfaceChecker(IBrowserMenuItem, perm) + item = proxify(item, checker) + + return item + MenuItemFactory.factory = BrowserMenuItem + + adapter(_context, (MenuItemFactory,), self.menuItemType, + (self.for_, IBrowserRequest), name=title) + + def __call__(self, _context): + # Nothing to do. + pass diff --git a/browser/metaconfigure.py b/browser/metaconfigure.py new file mode 100644 index 0000000..d007c68 --- /dev/null +++ b/browser/metaconfigure.py @@ -0,0 +1,370 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser configuration code + +$Id$ +""" +from zope.component.interfaces import IDefaultViewName +from zope.configuration.exceptions import ConfigurationError +from zope.interface.interface import InterfaceClass +from zope.publisher.interfaces.browser import ILayer, ISkin, IDefaultSkin +from zope.publisher.interfaces.browser import IBrowserRequest + +from zope.app import zapi +from zope.app.component.metaconfigure import handler +from zope.app.container.interfaces import IAdding +from zope.app.publisher.browser.menu import menuItemDirective +from zope.app.component.contentdirective import ContentDirective +from zope.app.publisher.interfaces.browser import AddMenu + +# referred to through ZCML +from zope.app.publisher.browser.resourcemeta import resource +from zope.app.publisher.browser.resourcemeta import resourceDirectory +from zope.app.publisher.browser.i18nresourcemeta import I18nResource +from zope.app.publisher.browser.viewmeta import view +from zope.app.component.interface import provideInterface + +# Create special modules that contain all layers and skins +from types import ModuleType as module +import sys +layers = module('layers') +sys.modules['zope.app.layers'] = layers + +skins = module('skins') +sys.modules['zope.app.skins'] = skins + + +def layer(_context, name=None, interface=None, base=ILayer): + """Provides a new layer. + + >>> 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() + >>> layer(context, u'layer1') + >>> iface = context.actions[0]['args'][1] + >>> iface.getName() + u'layer1' + >>> iface.__bases__ + (,) + >>> hasattr(sys.modules['zope.app.layers'], 'layer1') + True + + >>> del sys.modules['zope.app.layers'].layer1 + + Possibility 2: Providing a custom base interface + ------------------------------------------------ + + >>> class BaseLayer(ILayer): + ... pass + >>> context = Context() + >>> layer(context, u'layer1', base=BaseLayer) + >>> iface = context.actions[0]['args'][1] + >>> iface.getName() + u'layer1' + >>> iface.__bases__ + (,) + >>> hasattr(sys.modules['zope.app.layers'], 'layer1') + True + + >>> del sys.modules['zope.app.layers'].layer1 + + Possibility 3: Define a Layer just through an Interface + ------------------------------------------------------- + + >>> class layer1(ILayer): + ... pass + >>> context = Context() + >>> layer(context, interface=layer1) + >>> context.actions[0]['args'][1] is layer1 + True + >>> hasattr(sys.modules['zope.app.layers'], 'layer1') + False + + Possibility 4: Use an Interface and a Name + ------------------------------------------ + + >>> context = Context() + >>> layer(context, name='layer1', interface=layer1) + >>> context.actions[0]['args'][1] is layer1 + True + >>> hasattr(sys.modules['zope.app.layers'], 'layer1') + True + >>> import pprint + >>> pprint.pprint([action['discriminator'] for action in context.actions]) + [('interface', 'zope.app.publisher.browser.metaconfigure.layer1'), + ('layer', 'layer1')] + + Here are some disallowed configurations. + + >>> context = Context() + >>> layer(context, 'foo,bar') + Traceback (most recent call last): + ... + TypeError: Commas are not allowed in layer names. + >>> layer(context) + Traceback (most recent call last): + ... + ConfigurationError: You must specify the 'name' or 'interface' attribute. + >>> layer(context, base=BaseLayer) + Traceback (most recent call last): + ... + ConfigurationError: You must specify the 'name' or 'interface' attribute. + + >>> layer(context, interface=layer1, base=BaseLayer) + Traceback (most recent call last): + ... + ConfigurationError: You cannot specify the 'interface' and 'base' together. + """ + if name is not None and ',' in name: + raise TypeError("Commas are not allowed in layer names.") + if name is None and interface is None: + raise ConfigurationError( + "You must specify the 'name' or 'interface' attribute.") + if interface is not None and base is not ILayer: + raise ConfigurationError( + "You cannot specify the 'interface' and 'base' together.") + + if interface is None: + interface = InterfaceClass(name, (base, ), + __doc__='Layer: %s' %name, + __module__='zope.app.layers') + # Add the layer to the layers module. + # Note: We have to do this immediately, so that directives using the + # InterfaceField can find the layer. + setattr(layers, name, interface) + path = 'zope.app.layers.'+name + else: + path = interface.__module__ + '.' + interface.getName() + + # If a name was specified, make this layer available under this name. + # Note that the layer will be still available under its path, since it + # is an adapter, and the `LayerField` can resolve paths as well. + if name is None: + name = path + else: + # Make the interface available in the `zope.app.layers` module, so + # that other directives can find the interface under the name + # before the CA is setup. + setattr(layers, name, interface) + + # Register the layer interface as an interface + _context.action( + discriminator = ('interface', path), + callable = provideInterface, + args = (path, interface), + kw = {'info': _context.info} + ) + + # Register the layer interface as a layer + _context.action( + discriminator = ('layer', name), + callable = provideInterface, + args = (name, interface, ILayer, _context.info) + ) + +def skin(_context, name=None, interface=None, layers=None): + """Provides a new skin. + + >>> import pprint + >>> class Context(object): + ... info = u'doc' + ... def __init__(self): self.actions = [] + ... def action(self, **kw): self.actions.append(kw) + + >>> class Layer1(ILayer): pass + >>> class Layer2(ILayer): pass + + Possibility 1: The Old Way + -------------------------- + + >>> context = Context() + >>> skin(context, u'skin1', layers=[Layer1, Layer2]) + >>> iface = context.actions[0]['args'][1] + >>> iface.getName() + u'skin1' + >>> pprint.pprint(iface.__bases__) + (, + ) + >>> hasattr(sys.modules['zope.app.skins'], 'skin1') + True + + >>> del sys.modules['zope.app.skins'].skin1 + + Possibility 2: Just specify an interface + ---------------------------------------- + + >>> class skin1(Layer1, Layer2): + ... pass + + >>> context = Context() + >>> skin(context, interface=skin1) + >>> context.actions[0]['args'][1] is skin1 + True + + Possibility 3: Specify an interface and a Name + ---------------------------------------------- + + >>> context = Context() + >>> skin(context, name='skin1', interface=skin1) + >>> context.actions[0]['args'][1] is skin1 + True + >>> import pprint + >>> pprint.pprint([action['discriminator'] for action in context.actions]) + [('skin', 'skin1'), + ('interface', 'zope.app.publisher.browser.metaconfigure.skin1'), + ('skin', 'zope.app.publisher.browser.metaconfigure.skin1')] + + Here are some disallowed configurations. + + >>> context = Context() + >>> skin(context) + Traceback (most recent call last): + ... + ConfigurationError: You must specify the 'name' or 'interface' attribute. + >>> skin(context, layers=[Layer1]) + Traceback (most recent call last): + ... + ConfigurationError: You must specify the 'name' or 'interface' attribute. + """ + if name is None and interface is None: + raise ConfigurationError( + "You must specify the 'name' or 'interface' attribute.") + + if name is not None and layers is not None: + interface = InterfaceClass(name, layers, + __doc__='Skin: %s' %name, + __module__='zope.app.skins') + # Add the layer to the skins module. + # Note: We have to do this immediately, so that directives using the + # InterfaceField can find the layer. + setattr(skins, name, interface) + path = 'zope.app.skins'+name + else: + path = interface.__module__ + '.' + interface.getName() + + # Register the skin interface as a skin using the passed name. + if name is not None: + _context.action( + discriminator = ('skin', name), + callable = provideInterface, + args = (name, interface, ISkin, _context.info) + ) + + name = path + + # Register the skin interface as an interface + _context.action( + discriminator = ('interface', path), + callable = provideInterface, + args = (path, interface), + kw = {'info': _context.info} + ) + + # Register the skin interface as a skin + _context.action( + discriminator = ('skin', name), + callable = provideInterface, + args = (name, interface, ISkin, _context.info) + ) + +def setDefaultSkin(name, info=''): + """Set the default skin. + + >>> from zope.interface import directlyProvides + >>> from zope.app.testing import ztapi + + >>> class Skin1: pass + >>> directlyProvides(Skin1, ISkin) + + >>> ztapi.provideUtility(ISkin, Skin1, 'Skin1') + >>> setDefaultSkin('Skin1') + >>> adapters = zapi.getSiteManager().adapters + + Lookup the default skin for a request that has the + + >>> adapters.lookup((IBrowserRequest,), IDefaultSkin, '') is Skin1 + True + """ + skin = zapi.getUtility(ISkin, name) + handler('provideAdapter', + (IBrowserRequest,), IDefaultSkin, '', skin, info), + +def defaultSkin(_context, name): + + _context.action( + discriminator = 'defaultSkin', + callable = setDefaultSkin, + args = (name, _context.info) + ) + +def defaultView(_context, name, for_=None): + + type = IBrowserRequest + + _context.action( + discriminator = ('defaultViewName', for_, type, name), + callable = handler, + args = ('provideAdapter', + (for_, type), IDefaultViewName, '', name, _context.info) + ) + + if for_ is not None: + _context.action( + discriminator = None, + callable = provideInterface, + args = ('', for_) + ) + + +def addMenuItem(_context, title, class_=None, factory=None, description='', + permission=None, filter=None, view=None): + """Create an add menu item for a given class or factory + + As a convenience, a class can be provided, in which case, a + factory is automatically defined based on the class. In this + case, the factory id is based on the class name. + + """ + + if class_ is None: + if factory is None: + raise ValueError("Must specify either class or factory") + else: + if factory is not None: + raise ValueError("Can't specify both class and factory") + if permission is None: + raise ValueError( + "A permission must be specified when a class is used") + factory = "BrowserAdd__%s.%s" % ( + class_.__module__, class_.__name__) + ContentDirective(_context, class_).factory( + _context, + id = factory) + + extra = {'factory': factory} + + if view: + action = view + else: + action = factory + + menuItemDirective(_context, AddMenu, IAdding, + action, title, description, None, filter, + permission, extra) diff --git a/browser/resource.py b/browser/resource.py new file mode 100644 index 0000000..dca6577 --- /dev/null +++ b/browser/resource.py @@ -0,0 +1,40 @@ +############################################################################## +# +# Copyright (c) 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser Resource + +$Id$ +""" +from zope.component.interfaces import IResource +from zope.interface import implements + +from zope.app import zapi +from zope.app.component.hooks import getSite +from zope.app.location import Location +from zope.app.traversing.browser.interfaces import IAbsoluteURL + + +class Resource(Location): + implements(IResource) + + def __init__(self, request): + self.request = request + + def __call__(self): + name = self.__name__ + if name.startswith('++resource++'): + name = name[12:] + + site = getSite() + url = str(zapi.getMultiAdapter((site, self.request), IAbsoluteURL)) + return "%s/@@/%s" % (url, name) diff --git a/browser/resourcemeta.py b/browser/resourcemeta.py new file mode 100644 index 0000000..7a1b193 --- /dev/null +++ b/browser/resourcemeta.py @@ -0,0 +1,84 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser configuration code + +$Id$ +""" +import os + +from zope.configuration.exceptions import ConfigurationError +from zope.interface import Interface +from zope.publisher.interfaces.browser import IBrowserRequest +from zope.publisher.interfaces.browser import IDefaultBrowserLayer +from zope.security.checker import CheckerPublic, NamesChecker + +from zope.app import zapi +from zope.app.component.metaconfigure import handler + +from fileresource import FileResourceFactory, ImageResourceFactory +from pagetemplateresource import PageTemplateResourceFactory +from directoryresource import DirectoryResourceFactory + +allowed_names = ('GET', 'HEAD', 'publishTraverse', 'browserDefault', + 'request', '__call__') + +def resource(_context, name, layer=IDefaultBrowserLayer, + permission='zope.Public', file=None, image=None, template=None): + + if permission == 'zope.Public': + permission = CheckerPublic + + checker = NamesChecker(allowed_names, permission) + + if ((file and image) or (file and template) or + (image and template) or not (file or image or template)): + raise ConfigurationError( + "Must use exactly one of file or image or template" + " attributes for resource directives" + ) + + if file: + factory = FileResourceFactory(file, checker, name) + elif image: + factory = ImageResourceFactory(image, checker, name) + else: + factory = PageTemplateResourceFactory(template, checker, name) + + _context.action( + discriminator = ('resource', name, IBrowserRequest, layer), + callable = handler, + args = ('provideAdapter', + (layer,), Interface, name, factory, _context.info), + ) + +def resourceDirectory(_context, name, directory, layer=IDefaultBrowserLayer, + permission='zope.Public'): + if permission == 'zope.Public': + permission = CheckerPublic + + checker = NamesChecker(allowed_names + ('__getitem__', 'get'), + permission) + + if not os.path.isdir(directory): + raise ConfigurationError( + "Directory %s does not exist" % directory + ) + + factory = DirectoryResourceFactory(directory, checker, name) + _context.action( + discriminator = ('resource', name, IBrowserRequest, layer), + callable = handler, + args = ('provideAdapter', + (layer,), Interface, name, factory, _context.info), + ) diff --git a/browser/resources.py b/browser/resources.py new file mode 100644 index 0000000..3040e4e --- /dev/null +++ b/browser/resources.py @@ -0,0 +1,52 @@ +############################################################################## +# +# Copyright (c) 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Resource URL acess + +$Id$ +""" +from zope.app.publisher.browser import BrowserView +from zope.publisher.interfaces.browser import IBrowserPublisher +from zope.publisher.interfaces import NotFound +from zope.interface import implements + +from zope.app import zapi +from zope.app.location import locate + +class Resources(BrowserView): + """Provide a URL-accessible resource namespace + """ + + implements(IBrowserPublisher) + + def publishTraverse(self, request, name): + '''See interface IBrowserPublisher''' + + resource = zapi.queryAdapter(request, name=name) + if resource is None: + raise NotFound(self, name) + + sm = zapi.getSiteManager() + locate(resource, sm, name) + return resource + + def browserDefault(self, request): + '''See IBrowserPublisher''' + return empty, () + + def __getitem__(self, name): + return self.publishTraverse(self.request, name) + + +def empty(): + return '' diff --git a/browser/tests/support.py b/browser/tests/support.py new file mode 100644 index 0000000..c51b8f3 --- /dev/null +++ b/browser/tests/support.py @@ -0,0 +1,44 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Support for tests that need a simple site to be provided. + +$Id$ +""" +from zope.interface import implements + +from zope.component.service import serviceManager + +from zope.app.component.hooks import setSite +from zope.app.component.interfaces import ISite +from zope.app.traversing.interfaces import IContainmentRoot + + +class Site: + implements(ISite, IContainmentRoot) + + def getSiteManager(self): + return serviceManager + +site = Site() + + +class SiteHandler(object): + + def setUp(self): + super(SiteHandler, self).setUp() + setSite(site) + + def tearDown(self): + setSite() + super(SiteHandler, self).tearDown() diff --git a/browser/tests/test_addMenuItem.py b/browser/tests/test_addMenuItem.py new file mode 100644 index 0000000..3108016 --- /dev/null +++ b/browser/tests/test_addMenuItem.py @@ -0,0 +1,209 @@ +############################################################################# +# +# Copyright (c) 2003 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Test the addMenuItem directive + +>>> context = Context() +>>> addMenuItem(context, class_=X, title="Add an X", +... permission="zope.ManageContent") +>>> context +((('utility', + , + 'BrowserAdd__zope.app.publisher.browser.tests.test_addMenuItem.X'), + , + ('provideUtility', + , + , + 'BrowserAdd__zope.app.publisher.browser.tests.test_addMenuItem.X')), + (None, + , + ('zope.component.interfaces.IFactory', + )), + (('adapter', + (, + ), + , + 'Add an X'), + , + ('provideAdapter', + (, + ), + , + 'Add an X', + , + '')), + (None, + , + ('', + )), + (None, + , + ('', + )), + (None, + , + ('', + ))) + +$Id$ +""" + +import unittest +from zope.testing.doctestunit import DocTestSuite +import re +import pprint +import cStringIO +from zope.app.publisher.browser.metaconfigure import addMenuItem + +atre = re.compile(' at [0-9a-fA-Fx]+') + +class X(object): + pass + +class Context(object): + actions = () + info = '' + + def action(self, discriminator, callable, args): + self.actions += ((discriminator, callable, args), ) + + def __repr__(self): + stream = cStringIO.StringIO() + pprinter = pprint.PrettyPrinter(stream=stream, width=60) + pprinter.pprint(self.actions) + r = stream.getvalue() + return (''.join(atre.split(r))).strip() + + +def test_w_factory(): + """ + >>> context = Context() + >>> addMenuItem(context, factory="x.y.z", title="Add an X", + ... permission="zope.ManageContent", description="blah blah", + ... filter="context/foo") + >>> context + ((('adapter', + (, + ), + , + 'Add an X'), + , + ('provideAdapter', + (, + ), + , + 'Add an X', + , + '')), + (None, + , + ('', + )), + (None, + , + ('', + )), + (None, + , + ('', + ))) + """ + +def test_w_factory_and_view(): + """ + >>> context = Context() + >>> addMenuItem(context, factory="x.y.z", title="Add an X", + ... permission="zope.ManageContent", description="blah blah", + ... filter="context/foo", view="AddX") + >>> context + ((('adapter', + (, + ), + , + 'Add an X'), + , + ('provideAdapter', + (, + ), + , + 'Add an X', + , + '')), + (None, + , + ('', + )), + (None, + , + ('', + )), + (None, + , + ('', + ))) + """ + +def test_w_factory_class_view(): + """ + >>> context = Context() + >>> addMenuItem(context, class_=X, title="Add an X", + ... permission="zope.ManageContent", description="blah blah", + ... filter="context/foo", view="AddX") + >>> import pprint + >>> context + ((('utility', + , + 'BrowserAdd__zope.app.publisher.browser.tests.test_addMenuItem.X'), + , + ('provideUtility', + , + , + 'BrowserAdd__zope.app.publisher.browser.tests.test_addMenuItem.X')), + (None, + , + ('zope.component.interfaces.IFactory', + )), + (('adapter', + (, + ), + , + 'Add an X'), + , + ('provideAdapter', + (, + ), + , + 'Add an X', + , + '')), + (None, + , + ('', + )), + (None, + , + ('', + )), + (None, + , + ('', + ))) + """ + + +def test_suite(): + return unittest.TestSuite(( + DocTestSuite(), + )) + +if __name__ == '__main__': unittest.main() diff --git a/browser/tests/test_directives.py b/browser/tests/test_directives.py new file mode 100644 index 0000000..3c9bc7c --- /dev/null +++ b/browser/tests/test_directives.py @@ -0,0 +1,1050 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""'browser' namespace directive tests + +$Id$ +""" +import os +import unittest +from cStringIO import StringIO + +from zope.interface import Interface, implements, directlyProvides, providedBy + +import zope.security.management +from zope.component.interfaces import IDefaultViewName +from zope.configuration.xmlconfig import xmlconfig, XMLConfig +from zope.configuration.exceptions import ConfigurationError +from zope.publisher.browser import TestRequest +from zope.publisher.interfaces.browser import IBrowserPublisher +from zope.publisher.interfaces.browser import IBrowserRequest +from zope.publisher.interfaces.browser import ISkin, IDefaultSkin +from zope.security.proxy import removeSecurityProxy, ProxyFactory +from zope.testing.doctestunit import DocTestSuite + +import zope.app.publisher.browser +from zope.app import zapi +from zope.app.component.tests.views import IC, V1, VZMI, R1, IV +from zope.app.publisher.browser.fileresource import FileResource +from zope.app.publisher.browser.i18nfileresource import I18nFileResource +from zope.app.publisher.browser.menu import getFirstMenuItem +from zope.app.publisher.interfaces.browser import IMenuItemType +from zope.app.security.permission import Permission +from zope.app.security.interfaces import IPermission +from zope.app.testing import placelesssetup, ztapi +from zope.app.traversing.adapters import DefaultTraversable +from zope.app.traversing.interfaces import ITraversable + + +tests_path = os.path.join( + os.path.dirname(zope.app.publisher.browser.__file__), + 'tests') + +template = """ + %s + """ + + +request = TestRequest() + +class VT(V1, object): + def publishTraverse(self, request, name): + try: + return int(name) + except: + return super(VT, self).publishTraverse(request, name) + +class Ob(object): + implements(IC) + +ob = Ob() + +class NCV(object): + "non callable view" + + def __init__(self, context, request): + pass + +class CV(NCV): + "callable view" + def __call__(self): + pass + + +class C_w_implements(NCV): + implements(Interface) + + def index(self): + return self + +class Test(placelesssetup.PlacelessSetup, unittest.TestCase): + + def setUp(self): + super(Test, self).setUp() + XMLConfig('meta.zcml', zope.app.publisher.browser)() + ztapi.provideAdapter(None, ITraversable, DefaultTraversable) + + + def testPage(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % ( + ''' + + ''' + ))) + + v = zapi.queryMultiAdapter((ob, request), name='test') + self.assert_(issubclass(v.__class__, V1)) + + def testPageWithClassWithMenu(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + testtemplate = os.path.join(tests_path, 'testfiles', 'test.pt') + + + xmlconfig(StringIO(template % ( + ''' + + + ''' % testtemplate + ))) + test_menu = zapi.getUtility(IMenuItemType, 'test_menu') + menuItem = getFirstMenuItem(test_menu, ob, TestRequest()) + self.assertEqual(menuItem["title"], "Test View") + self.assertEqual(menuItem["action"], "@@test") + v = zapi.queryMultiAdapter((ob, request), name='test') + self.assertEqual(v(), "

test

\n") + + + def testPageWithTemplateWithMenu(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + testtemplate = os.path.join(tests_path, 'testfiles', 'test.pt') + + xmlconfig(StringIO(template % ( + ''' + + + ''' % testtemplate + ))) + + test_menu = zapi.getUtility(IMenuItemType, 'test_menu') + menuItem = getFirstMenuItem(test_menu, ob, TestRequest()) + self.assertEqual(menuItem["title"], "Test View") + self.assertEqual(menuItem["action"], "@@test") + v = zapi.queryMultiAdapter((ob, request), name='test') + self.assertEqual(v(), "

test

\n") + + + def testPageInPagesWithTemplateWithMenu(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + testtemplate = os.path.join(tests_path, 'testfiles', 'test.pt') + + xmlconfig(StringIO(template % ( + ''' + + + + + ''' % testtemplate + ))) + + test_menu = zapi.getUtility(IMenuItemType, 'test_menu') + menuItem = getFirstMenuItem(test_menu, ob, TestRequest()) + self.assertEqual(menuItem["title"], "Test View") + self.assertEqual(menuItem["action"], "@@test") + v = zapi.queryMultiAdapter((ob, request), name='test') + self.assertEqual(v(), "

test

\n") + + + def testPageInPagesWithClassWithMenu(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + testtemplate = os.path.join(tests_path, 'testfiles', 'test.pt') + + + xmlconfig(StringIO(template % ( + ''' + + + + + ''' % testtemplate + ))) + + test_menu = zapi.getUtility(IMenuItemType, 'test_menu') + menuItem = getFirstMenuItem(test_menu, ob, TestRequest()) + self.assertEqual(menuItem["title"], "Test View") + self.assertEqual(menuItem["action"], "@@test") + v = zapi.queryMultiAdapter((ob, request), name='test') + self.assertEqual(v(), "

test

\n") + + def testDefaultView(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % ( + ''' + + ''' + ))) + + self.assertEqual( + zapi.getSiteManager().adapters.lookup( + map(providedBy, (ob, request)), IDefaultViewName), 'test') + + def testSkinResource(self): + self.assertEqual( + zapi.queryAdapter(Request(IV), name='test'), None) + + xmlconfig(StringIO(template % ( + ''' + + + + + ''' + ))) + + self.assertEqual( + zapi.queryAdapter(request, name='test').__class__, R1) + zmi = zapi.getUtility(ISkin, 'zmi') + self.assertEqual( + zapi.queryAdapter(TestRequest(skin=zmi), name='test').__class__, + RZMI) + + def testDefaultSkin(self): + request = TestRequest() + + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + xmlconfig(StringIO(template % ( + ''' + + + + + + ''' + ))) + + # Simulate Zope Publication behavior in beforeTraversal() + adapters = zapi.getSiteManager().adapters + skin = adapters.lookup((providedBy(request),), IDefaultSkin, '') + directlyProvides(request, skin) + + v = zapi.queryMultiAdapter((ob, request), name='test') + self.assert_(issubclass(v.__class__, VZMI)) + + def testSkinPage(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % ( + ''' + + + + + ''' + ))) + + v = zapi.queryMultiAdapter((ob, request), name='test') + self.assert_(issubclass(v.__class__, V1)) + zmi = zapi.getUtility(ISkin, 'zmi') + v = zapi.queryMultiAdapter((ob, TestRequest(skin=zmi)), name='test') + self.assert_(issubclass(v.__class__, VZMI)) + + def testI18nResource(self): + self.assertEqual(zapi.queryAdapter(request, name='test'), None) + + path1 = os.path.join(tests_path, 'testfiles', 'test.pt') + path2 = os.path.join(tests_path, 'testfiles', 'test2.pt') + + xmlconfig(StringIO(template % ( + ''' + + + + + ''' % (path1, path2) + ))) + + v = zapi.getAdapter(request, name='test') + self.assertEqual( + zapi.queryAdapter(request, name='test').__class__, + I18nFileResource) + self.assertEqual(v._testData('en'), open(path1, 'rb').read()) + self.assertEqual(v._testData('fr'), open(path2, 'rb').read()) + + # translation must be provided for the default language + config = StringIO(template % ( + ''' + + + + + ''' % (path1, path2) + )) + self.assertRaises(ConfigurationError, xmlconfig, config) + + # files and images can't be mixed + config = StringIO(template % ( + ''' + + + + + ''' % (path1, path2) + )) + self.assertRaises(ConfigurationError, xmlconfig, config) + + def testInterfaceProtectedPage(self): + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.getMultiAdapter((ob, request), name='test') + v = ProxyFactory(v) + self.assertEqual(v.index(), 'V1 here') + self.assertRaises(Exception, getattr, v, 'action') + + def testAttributeProtectedPage(self): + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.getMultiAdapter((ob, request), name='test') + v = ProxyFactory(v) + self.assertEqual(v.action(), 'done') + self.assertRaises(Exception, getattr, v, 'index') + + def testInterfaceAndAttributeProtectedPage(self): + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.getMultiAdapter((ob, request), name='test') + self.assertEqual(v.index(), 'V1 here') + self.assertEqual(v.action(), 'done') + + def testDuplicatedInterfaceAndAttributeProtectedPage(self): + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.getMultiAdapter((ob, request), name='test') + self.assertEqual(v.index(), 'V1 here') + self.assertEqual(v.action(), 'done') + + def test_class_w_implements(self): + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.getMultiAdapter((ob, request), name='test') + self.assertEqual(v.index(), v) + self.assert_(IBrowserPublisher.providedBy(v)) + + def testIncompleteProtectedPageNoPermission(self): + self.assertRaises( + ConfigurationError, + xmlconfig, + StringIO(template % + ''' + + ''' + )) + + + def testPageViews(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + test3 = os.path.join(tests_path, 'testfiles', 'test3.pt') + + xmlconfig(StringIO(template % + ''' + + + + + + + ''' % test3 + )) + + v = zapi.getMultiAdapter((ob, request), name='index.html') + self.assertEqual(v(), 'V1 here') + v = zapi.getMultiAdapter((ob, request), name='action.html') + self.assertEqual(v(), 'done') + v = zapi.getMultiAdapter((ob, request), name='test.html') + self.assertEqual(str(v()), '

done

\n') + + def testNamedViewPageViewsCustomTraversr(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % + ''' + + + + + + ''' + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + view = removeSecurityProxy(view) + self.assertEqual(view.browserDefault(request)[1], (u'index.html', )) + + + v = view.publishTraverse(request, 'index.html') + v = removeSecurityProxy(v) + self.assertEqual(v(), 'V1 here') + v = view.publishTraverse(request, 'action.html') + v = removeSecurityProxy(v) + self.assertEqual(v(), 'done') + + + def testNamedViewNoPagesForCallable(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % + ''' + + ''' + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + view = removeSecurityProxy(view) + self.assertEqual(view.browserDefault(request), (view, ())) + + def testNamedViewNoPagesForNonCallable(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % + ''' + + ''' + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + view = removeSecurityProxy(view) + self.assertEqual(getattr(view, 'browserDefault', None), None) + + def testNamedViewPageViewsNoDefault(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + test3 = os.path.join(tests_path, 'testfiles', 'test3.pt') + + xmlconfig(StringIO(template % + ''' + + + + + + + ''' % test3 + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + view = removeSecurityProxy(view) + self.assertEqual(view.browserDefault(request)[1], (u'index.html', )) + + + v = view.publishTraverse(request, 'index.html') + v = removeSecurityProxy(v) + self.assertEqual(v(), 'V1 here') + v = view.publishTraverse(request, 'action.html') + v = removeSecurityProxy(v) + self.assertEqual(v(), 'done') + v = view.publishTraverse(request, 'test.html') + v = removeSecurityProxy(v) + self.assertEqual(str(v()), '

done

\n') + + def testNamedViewPageViewsWithDefault(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + test3 = os.path.join(tests_path, 'testfiles', 'test3.pt') + + xmlconfig(StringIO(template % + ''' + + + + + + + + ''' % test3 + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + view = removeSecurityProxy(view) + self.assertEqual(view.browserDefault(request)[1], (u'test.html', )) + + + v = view.publishTraverse(request, 'index.html') + v = removeSecurityProxy(v) + self.assertEqual(v(), 'V1 here') + v = view.publishTraverse(request, 'action.html') + v = removeSecurityProxy(v) + self.assertEqual(v(), 'done') + v = view.publishTraverse(request, 'test.html') + v = removeSecurityProxy(v) + self.assertEqual(str(v()), '

done

\n') + + def testTraversalOfPageForView(self): + """Tests proper traversal of a page defined for a view.""" + + xmlconfig(StringIO(template % + ''' + + + + ''' + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + view = removeSecurityProxy(view) + view.publishTraverse(request, 'index.html') + + def testTraversalOfPageForViewWithPublishTraverse(self): + """Tests proper traversal of a page defined for a view. + + This test is different from testTraversalOfPageForView in that it + tests the behavior on a view that has a publishTraverse method -- + the implementation of the lookup is slightly different in such a + case. + """ + xmlconfig(StringIO(template % + ''' + + + + ''' + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + view = removeSecurityProxy(view) + view.publishTraverse(request, 'index.html') + + def testProtectedPageViews(self): + ztapi.provideUtility(IPermission, Permission('p', 'P'), 'p') + + request = TestRequest() + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % + ''' + + + + + + + + + + ''' + )) + + v = zapi.getMultiAdapter((ob, request), name='index.html') + v = ProxyFactory(v) + zope.security.management.getInteraction().add(request) + self.assertRaises(Exception, v) + v = zapi.getMultiAdapter((ob, request), name='action.html') + v = ProxyFactory(v) + self.assertRaises(Exception, v) + + def testProtectedNamedViewPageViews(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % + ''' + + + + + + + + + + ''' + )) + + view = zapi.getMultiAdapter((ob, request), name='test') + self.assertEqual(view.browserDefault(request)[1], (u'index.html', )) + + v = view.publishTraverse(request, 'index.html') + self.assertEqual(v(), 'V1 here') + + def testSkinnedPageView(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % + ''' + + + + + + + + + + + ''' + )) + + v = zapi.getMultiAdapter((ob, request), name='index.html') + self.assertEqual(v(), 'V1 here') + skinny = zapi.getUtility(ISkin, 'skinny') + v = zapi.getMultiAdapter((ob, TestRequest(skin=skinny)), + name='index.html') + self.assertEqual(v(), 'done') + + def testFile(self): + path = os.path.join(tests_path, 'testfiles', 'test.pt') + + self.assertEqual(zapi.queryAdapter(request, name='test'), None) + + xmlconfig(StringIO(template % + ''' + + ''' % path + )) + + r = zapi.getAdapter(request, name='index.html') + self.assertEquals(r.__class__, FileResource) + r = ProxyFactory(r) + self.assertEqual(r.__name__, "index.html") + + # Make sure we can access available attrs and not others + for n in ('GET', 'HEAD', 'publishTraverse', 'request', '__call__'): + getattr(r, n) + self.assertEqual(r.__name__, "index.html") + + self.assertRaises(Exception, getattr, r, '_testData') + + r = removeSecurityProxy(r) + self.assert_(r.__class__ is FileResource) + self.assertEqual(r._testData(), open(path, 'rb').read()) + + + def testSkinResource(self): + self.assertEqual(zapi.queryAdapter(request, name='test'), None) + + path = os.path.join(tests_path, 'testfiles', 'test.pt') + + xmlconfig(StringIO(template % ( + ''' + + + + ''' % path + ))) + + self.assertEqual(zapi.queryAdapter(request, name='test'), None) + + zmi = zapi.getUtility(ISkin, 'zmi') + r = zapi.getAdapter(TestRequest(skin=zmi), name='test') + r = removeSecurityProxy(r) + self.assertEqual(r._testData(), open(path, 'rb').read()) + + def test_template_page(self): + path = os.path.join(tests_path, 'testfiles', 'test.pt') + + self.assertEqual( + zapi.queryMultiAdapter((ob, request), name='index.html'), None) + + xmlconfig(StringIO(template % + ''' + + ''' % path + )) + + v = zapi.getMultiAdapter((ob, request), name='index.html') + self.assertEqual(v().strip(), '

test

') + + def testtemplateWClass(self): + path = os.path.join(tests_path, 'testfiles', 'test2.pt') + + self.assertEqual( + zapi.queryMultiAdapter((ob, request), name='index.html'), None) + + xmlconfig(StringIO(template % + ''' + + ''' % path + )) + + v = zapi.getMultiAdapter((ob, request), name='index.html') + self.assertEqual(v().strip(), '

42

') + + def testProtectedtemplate(self): + + path = os.path.join(tests_path, 'testfiles', 'test.pt') + + request = TestRequest() + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='test'), + None) + + xmlconfig(StringIO(template % + ''' + + + + + + ''' % path + )) + + xmlconfig(StringIO(template % + ''' + + ''' % path + )) + + v = zapi.getMultiAdapter((ob, request), name='xxx.html') + v = ProxyFactory(v) + zope.security.management.getInteraction().add(request) + self.assertRaises(Exception, v) + + v = zapi.getMultiAdapter((ob, request), name='index.html') + v = ProxyFactory(v) + self.assertEqual(v().strip(), '

test

') + + + def testtemplateNoName(self): + path = os.path.join(tests_path, 'testfiles', 'test.pt') + self.assertRaises( + ConfigurationError, + xmlconfig, + StringIO(template % + ''' + + ''' % path + )) + + def testtemplateAndPage(self): + path = os.path.join(tests_path, 'testfiles', 'test.pt') + self.assertRaises( + ConfigurationError, + xmlconfig, + StringIO(template % + ''' + + + + ''' % path + )) + + def testViewThatProvidesAnInterface(self): + request = TestRequest() + self.assertEqual( + zapi.queryMultiAdapter((ob, request), IV, name='test'), None) + + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.queryMultiAdapter((ob, request), IV, name='test') + self.assertEqual(v, None) + + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.queryMultiAdapter((ob, request), IV, name='test') + self.assert_(isinstance(v, V1)) + + def testUnnamedViewThatProvidesAnInterface(self): + request = TestRequest() + self.assertEqual(zapi.queryMultiAdapter((ob, request), IV), + None) + + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.queryMultiAdapter((ob, request), IV) + self.assertEqual(v, None) + + xmlconfig(StringIO(template % + ''' + + ''' + )) + + v = zapi.queryMultiAdapter((ob, request), IV) + + self.assert_(isinstance(v, V1)) + +def test_suite(): + return unittest.TestSuite(( + unittest.makeSuite(Test), + DocTestSuite('zope.app.publisher.browser.metaconfigure', + setUp=placelesssetup.setUp, + tearDown=placelesssetup.tearDown) + )) + +if __name__=='__main__': + unittest.main(defaultTest="test_suite") diff --git a/browser/tests/test_directoryresource.py b/browser/tests/test_directoryresource.py new file mode 100644 index 0000000..b5b8309 --- /dev/null +++ b/browser/tests/test_directoryresource.py @@ -0,0 +1,114 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Directory-based resources test + +$Id$ +""" +import os +from unittest import TestCase, main, makeSuite + +from zope.publisher.interfaces import NotFound +from zope.proxy import isProxy +from zope.security.proxy import removeSecurityProxy +from zope.publisher.browser import TestRequest +from zope.security.checker import NamesChecker, ProxyFactory +from zope.interface import implements + +from zope.app import zapi +from zope.app.testing.placelesssetup import PlacelessSetup +from zope.app.publisher.browser.directoryresource import \ + DirectoryResourceFactory +from zope.app.container.contained import Contained +from zope.app.publisher.browser.fileresource import FileResource +from zope.app.publisher.browser.pagetemplateresource import \ + PageTemplateResource +import zope.app.publisher.browser.tests as p +from zope.app.publisher.browser.tests import support + +test_directory = os.path.dirname(p.__file__) + +checker = NamesChecker( + ('get', '__getitem__', 'request', 'publishTraverse') + ) + +class Ob(Contained): pass + +ob = Ob() + +class Test(support.SiteHandler, PlacelessSetup, TestCase): + + def testNotFound(self): + path = os.path.join(test_directory, 'testfiles') + request = TestRequest() + factory = DirectoryResourceFactory(path, checker, 'testfiles') + resource = factory(request) + self.assertRaises(NotFound, resource.publishTraverse, + resource.request, 'doesnotexist') + self.assertRaises(NotFound, resource.get, 'doesnotexist') + + def testGetitem(self): + path = os.path.join(test_directory, 'testfiles') + request = TestRequest() + factory = DirectoryResourceFactory(path, checker, 'testfiles') + resource = factory(request) + self.assertRaises(KeyError, resource.__getitem__, 'doesnotexist') + file = resource['test.txt'] + + def testProxy(self): + path = os.path.join(test_directory, 'testfiles') + request = TestRequest() + factory = DirectoryResourceFactory(path, checker, 'testfiles') + resource = factory(request) + file = ProxyFactory(resource['test.txt']) + self.assert_(isProxy(file)) + + def testURL(self): + request = TestRequest() + request._vh_root = support.site + path = os.path.join(test_directory, 'testfiles') + files = DirectoryResourceFactory(path, checker, 'test_files')(request) + files.__parent__ = support.site + file = files['test.gif'] + self.assertEquals(file(), 'http://127.0.0.1/@@/test_files/test.gif') + + def testURL2Level(self): + request = TestRequest() + request._vh_root = support.site + ob.__parent__ = support.site + ob.__name__ = 'ob' + path = os.path.join(test_directory, 'testfiles') + files = DirectoryResourceFactory(path, checker, 'test_files')(request) + files.__parent__ = ob + file = files['test.gif'] + self.assertEquals(file(), 'http://127.0.0.1/@@/test_files/test.gif') + + def testCorrectFactories(self): + path = os.path.join(test_directory, 'testfiles') + request = TestRequest() + resource = DirectoryResourceFactory(path, checker, 'files')(request) + + image = resource['test.gif'] + self.assert_(zapi.isinstance(image, FileResource)) + template = resource['test.pt'] + self.assert_(zapi.isinstance(template, PageTemplateResource)) + file = resource['test.txt'] + self.assert_(zapi.isinstance(file, FileResource)) + file = resource['png'] + self.assert_(zapi.isinstance(file, FileResource)) + +def test_suite(): + return makeSuite(Test) + +if __name__ == '__main__': + main(defaultTest='test_suite') diff --git a/browser/tests/test_fields.py b/browser/tests/test_fields.py new file mode 100644 index 0000000..8aa3077 --- /dev/null +++ b/browser/tests/test_fields.py @@ -0,0 +1,30 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Test fields. + +$Id$ +""" +import unittest +from zope.testing.doctestunit import DocTestSuite +from zope.app.testing import placelesssetup + +def test_suite(): + return unittest.TestSuite(( + DocTestSuite('zope.app.publisher.browser.fields', + setUp=placelesssetup.setUp, + tearDown=placelesssetup.tearDown), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/browser/tests/test_fileresource.py b/browser/tests/test_fileresource.py new file mode 100644 index 0000000..40e968f --- /dev/null +++ b/browser/tests/test_fileresource.py @@ -0,0 +1,111 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""File-based browser resource tests. + +$Id$ +""" +import os +from unittest import TestCase, main, makeSuite + +from zope.publisher.interfaces import NotFound +from zope.i18n.interfaces import IUserPreferredCharsets +from zope.security.proxy import removeSecurityProxy +from zope.security.checker import NamesChecker + +from zope.app.testing.placelesssetup import PlacelessSetup +from zope.app.testing import ztapi + +from zope.publisher.http import IHTTPRequest +from zope.publisher.http import HTTPCharsets +from zope.publisher.browser import TestRequest + +from zope.app.publisher.browser.fileresource import FileResourceFactory +from zope.app.publisher.browser.fileresource import ImageResourceFactory +import zope.app.publisher.browser.tests as p + +checker = NamesChecker( + ('__call__', 'HEAD', 'request', 'publishTraverse', 'GET') + ) + +test_directory = os.path.dirname(p.__file__) + +class Test(PlacelessSetup, TestCase): + + def setUp(self): + super(Test, self).setUp() + ztapi.provideAdapter(IHTTPRequest, IUserPreferredCharsets, + HTTPCharsets) + + def testNoTraversal(self): + + path = os.path.join(test_directory, 'testfiles', 'test.txt') + factory = FileResourceFactory(path, checker, 'test.txt') + resource = factory(TestRequest()) + self.assertRaises(NotFound, + resource.publishTraverse, + resource.request, + '_testData') + + def testFileGET(self): + + path = os.path.join(test_directory, 'testfiles', 'test.txt') + + factory = FileResourceFactory(path, checker, 'test.txt') + resource = factory(TestRequest()) + self.assertEqual(resource.GET(), open(path, 'rb').read()) + + response = removeSecurityProxy(resource.request).response + self.assertEqual(response.getHeader('Content-Type'), 'text/plain') + + def testFileHEAD(self): + + path = os.path.join(test_directory, 'testfiles', 'test.txt') + factory = FileResourceFactory(path, checker, 'test.txt') + resource = factory(TestRequest()) + + self.assertEqual(resource.HEAD(), '') + + response = removeSecurityProxy(resource.request).response + self.assertEqual(response.getHeader('Content-Type'), 'text/plain') + + def testImageGET(self): + + path = os.path.join(test_directory, 'testfiles', 'test.gif') + + factory = ImageResourceFactory(path, checker, 'test.gif') + resource = factory(TestRequest()) + + self.assertEqual(resource.GET(), open(path, 'rb').read()) + + response = removeSecurityProxy(resource.request).response + self.assertEqual(response.getHeader('Content-Type'), 'image/gif') + + def testImageHEAD(self): + + path = os.path.join(test_directory, 'testfiles', 'test.gif') + factory = ImageResourceFactory(path, checker, 'test.gif') + resource = factory(TestRequest()) + + self.assertEqual(resource.HEAD(), '') + + response = removeSecurityProxy(resource.request).response + self.assertEqual(response.getHeader('Content-Type'), 'image/gif') + + + +def test_suite(): + return makeSuite(Test) + +if __name__=='__main__': + main(defaultTest='test_suite') diff --git a/browser/tests/test_icondirective.py b/browser/tests/test_icondirective.py new file mode 100644 index 0000000..6372681 --- /dev/null +++ b/browser/tests/test_icondirective.py @@ -0,0 +1,165 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Test Icon-Directive + +$Id$ +""" +import os +from StringIO import StringIO +from unittest import TestCase, main, makeSuite + +from zope.configuration.exceptions import ConfigurationError +from zope.configuration.xmlconfig import xmlconfig, XMLConfig +from zope.interface import implements +from zope.publisher.browser import TestRequest +from zope.security.checker import ProxyFactory, CheckerPublic +from zope.security.interfaces import Forbidden +from zope.security.proxy import removeSecurityProxy + +import zope.app.publisher.browser +from zope.app import zapi +from zope.app.component.tests.views import IC +from zope.app.component.interfaces import ISite +from zope.app.publisher.browser.tests import support +from zope.app.testing.placelesssetup import PlacelessSetup +from zope.app.traversing.interfaces import IContainmentRoot + + +template = """ + %s + """ + + +request = TestRequest() + +class Ob(object): + implements(IC) + +ob = Ob() +request._vh_root = support.site + +def defineCheckers(): + # define the appropriate checker for a FileResource for these tests + from zope.app.security.protectclass import protectName + from zope.app.publisher.browser.fileresource import FileResource + protectName(FileResource, '__call__', 'zope.Public') + + +class Test(support.SiteHandler, PlacelessSetup, TestCase): + + def setUp(self): + super(Test, self).setUp() + XMLConfig('meta.zcml', zope.app.publisher.browser)() + defineCheckers() + + def test(self): + self.assertEqual(zapi.queryMultiAdapter((ob, request), name='zmi_icon'), + None) + + import zope.app.publisher.browser.tests as p + path = os.path.dirname(p.__file__) + path = os.path.join(path, 'testfiles', 'test.gif') + + xmlconfig(StringIO(template % ( + ''' + + ''' % path + ))) + + view = zapi.getMultiAdapter((ob, request), name='zmi_icon') + rname = 'zope-app-component-tests-views-IC-zmi_icon.gif' + self.assertEqual( + view(), + 'IC' + % rname) + + resource = ProxyFactory(zapi.getAdapter(request, name=rname)) + self.assertRaises(Forbidden, getattr, resource, '_testData') + resource = removeSecurityProxy(resource) + self.assertEqual(resource._testData(), open(path, 'rb').read()) + + def testResource(self): + self.assertEqual( + zapi.queryMultiAdapter((ob, request), name='zmi_icon'), None) + + import zope.app.publisher.browser.tests as p + path = os.path.dirname(p.__file__) + path = os.path.join(path, 'testfiles', 'test.gif') + + xmlconfig(StringIO(template % ( + ''' + + + ''' % path + ))) + + view = zapi.getMultiAdapter((ob, request), name='zmi_icon') + rname = "zmi_icon_res" + self.assertEqual( + view(), + 'IC' + % rname) + + resource = ProxyFactory(zapi.getAdapter(request, name=rname)) + + self.assertRaises(Forbidden, getattr, resource, '_testData') + resource = removeSecurityProxy(resource) + self.assertEqual(resource._testData(), open(path, 'rb').read()) + + def testResourceErrors(self): + self.assertEqual( + zapi.queryMultiAdapter((ob, request), name='zmi_icon'), None) + + import zope.app.publisher.browser.tests as p + path = os.path.dirname(p.__file__) + path = os.path.join(path, 'testfiles', 'test.gif') + + config = StringIO(template % ( + ''' + + + ''' % (path, path) + )) + self.assertRaises(ConfigurationError, xmlconfig, config) + + config = StringIO(template % ( + """ + + """ + )) + self.assertRaises(ConfigurationError, xmlconfig, config) + + +def test_suite(): + return makeSuite(Test) + +if __name__=='__main__': + main(defaultTest='test_suite') diff --git a/browser/tests/test_menu.py b/browser/tests/test_menu.py new file mode 100644 index 0000000..2592ce9 --- /dev/null +++ b/browser/tests/test_menu.py @@ -0,0 +1,30 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser Menu Item Tests + +$Id$ +""" +import unittest +from zope.testing.doctestunit import DocTestSuite + +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') diff --git a/browser/tests/test_menudirectives.py b/browser/tests/test_menudirectives.py new file mode 100644 index 0000000..bc1e7a9 --- /dev/null +++ b/browser/tests/test_menudirectives.py @@ -0,0 +1,98 @@ +############################################################################## +# +# Copyright (c) 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser Menu Directives Tests + +$Id$ +""" +import unittest + +from zope.configuration.xmlconfig import XMLConfig +from zope.interface import Interface, implements +from zope.publisher.browser import TestRequest +from zope.publisher.interfaces.browser import IBrowserPublisher +from zope.security.interfaces import Unauthorized, Forbidden + +from zope.app.testing.placelesssetup import PlacelessSetup + +import zope.app.publisher.browser + +template = """ + %s + """ + +class I1(Interface): pass +class I11(I1): pass +class I12(I1): pass +class I111(I11): pass + +class C1(object): + implements(I1) + +class TestObject(object): + implements(IBrowserPublisher, I111) + + def f(self): + pass + + def browserDefault(self, r): + return self, () + + def publishTraverse(self, request, name): + if name[:1] == 'f': + raise Forbidden, name + if name[:1] == 'u': + raise Unauthorized, name + return self.f + + +class Test(PlacelessSetup, unittest.TestCase): + + def setUp(self): + super(Test, self).setUp() + XMLConfig('meta.zcml', zope.app.publisher.browser)() + + def test(self): + XMLConfig('tests/menus.zcml', zope.app.publisher.browser)() + + from zope.app.menus import test_id + + menu = zope.app.publisher.browser.menu.getMenu( + test_id, TestObject(), TestRequest()) + + def d(n): + return {'action': "a%s" % n, + 'title': "t%s" % n, + 'description': "", + 'selected': '', + 'icon': None, + 'extra': None} + + self.assertEqual(list(menu), [d(5), d(6), d(3), d(2), d(1)]) + + first = zope.app.publisher.browser.menu.getFirstMenuItem( + test_id, TestObject(), TestRequest()) + + self.assertEqual(first, d(5)) + + +def test_suite(): + return unittest.TestSuite(( + unittest.makeSuite(Test), + )) + +if __name__=='__main__': + unittest.main(defaultTest='test_suite') diff --git a/browser/tests/test_pagetemplateresource.py b/browser/tests/test_pagetemplateresource.py new file mode 100644 index 0000000..1708545 --- /dev/null +++ b/browser/tests/test_pagetemplateresource.py @@ -0,0 +1,65 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Page Template based Resources Test + +$Id$ +""" +import os +from unittest import TestCase, main, makeSuite + +from zope.publisher.interfaces import NotFound +from zope.app.testing import ztapi +from zope.security.checker import NamesChecker +from zope.publisher.browser import TestRequest + +from zope.app.testing.placelesssetup import PlacelessSetup +from zope.app.publisher.browser.pagetemplateresource import \ + PageTemplateResourceFactory +from zope.app.traversing.interfaces import ITraversable +from zope.app.traversing.adapters import DefaultTraversable +import zope.app.publisher.browser.tests as p + +test_directory = os.path.dirname(p.__file__) + +checker = NamesChecker( + ('__call__', 'request', 'publishTraverse') + ) + +class Test(PlacelessSetup, TestCase): + + def setUp(self): + super(Test, self).setUp() + ztapi.provideAdapter(None, ITraversable, DefaultTraversable) + + def testNoTraversal(self): + path = os.path.join(test_directory, 'testfiles', 'test.pt') + request = TestRequest() + factory = PageTemplateResourceFactory(path, checker, 'test.pt') + resource = factory(request) + self.assertRaises(NotFound, resource.publishTraverse, + resource.request, ()) + + def testCall(self): + path = os.path.join(test_directory, 'testfiles', 'testresource.pt') + test_data = "Foobar" + request = TestRequest(test_data=test_data) + factory = PageTemplateResourceFactory(path, checker, 'testresource.pt') + resource = factory(request) + self.assert_(resource(), test_data) + +def test_suite(): + return makeSuite(Test) + +if __name__=='__main__': + main(defaultTest='test_suite') diff --git a/browser/tests/test_resource.py b/browser/tests/test_resource.py new file mode 100644 index 0000000..c1dbea7 --- /dev/null +++ b/browser/tests/test_resource.py @@ -0,0 +1,53 @@ +############################################################################## +# +# Copyright (c) 2003 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Unit tests for Resource + +$Id$ +""" +import unittest + +from zope.publisher.browser import TestRequest + +from zope.app.publisher.browser.resource import Resource +from zope.app.publisher.browser.tests import support +from zope.app.testing.placelesssetup import PlacelessSetup + + +class TestResource(support.SiteHandler, PlacelessSetup, unittest.TestCase): + + def testGlobal(self): + req = TestRequest() + r = Resource(req) + req._vh_root = support.site + r.__parent__ = support.site + r.__name__ = 'foo' + self.assertEquals(r(), 'http://127.0.0.1/@@/foo') + r.__name__ = '++resource++foo' + self.assertEquals(r(), 'http://127.0.0.1/@@/foo') + + def testGlobalInVirtualHost(self): + req = TestRequest() + req.setVirtualHostRoot(['x', 'y']) + r = Resource(req) + req._vh_root = support.site + r.__parent__ = support.site + r.__name__ = 'foo' + self.assertEquals(r(), 'http://127.0.0.1/x/y/@@/foo') + + +def test_suite(): + return unittest.makeSuite(TestResource) + +if __name__ == '__main__': + unittest.main(defaultTest="test_suite") diff --git a/browser/tests/test_resources.py b/browser/tests/test_resources.py new file mode 100644 index 0000000..131d855 --- /dev/null +++ b/browser/tests/test_resources.py @@ -0,0 +1,78 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Test Browser Resources + +$Id$ +""" +from unittest import TestCase, main, makeSuite +from zope.app.testing import ztapi +from zope.app.testing.placelesssetup import PlacelessSetup + +from zope.i18n.interfaces import IUserPreferredCharsets + +from zope.publisher.http import IHTTPRequest +from zope.publisher.http import HTTPCharsets +from zope.publisher.browser import TestRequest +from zope.app.publisher.interfaces.browser import IBrowserView + +class Test(PlacelessSetup, TestCase): + + def setUp(self): + super(Test, self).setUp() + ztapi.provideAdapter(IHTTPRequest, IUserPreferredCharsets, + HTTPCharsets) + + def test_publishTraverse(self): + from zope.app.publisher.browser.resources import Resources + request = TestRequest() + + class Resource(object): + def __init__(self, request): pass + def __call__(self): return 42 + + ztapi.browserResource('test', Resource) + view = Resources(None, request) + resource = view.publishTraverse(request, 'test') + self.assertEqual(resource(), 42) + + def test_getitem(self): + from zope.app.publisher.browser.resources import Resources + request = TestRequest() + + class Resource(object): + def __init__(self, request): pass + def __call__(self): return 42 + + ztapi.browserResource('test', Resource) + view = Resources(None, request) + resource = view['test'] + self.assertEqual(resource(), 42) + + def testNotFound(self): + from zope.app.publisher.browser.resources import Resources + from zope.publisher.interfaces import NotFound + request = TestRequest() + view = Resources(None, request) + self.assertRaises(NotFound, + view.publishTraverse, + request, 'test' + ) + + + +def test_suite(): + return makeSuite(Test) + +if __name__=='__main__': + main(defaultTest='test_suite') diff --git a/browser/tests/testi18nfileresource.py b/browser/tests/testi18nfileresource.py new file mode 100644 index 0000000..98fe223 --- /dev/null +++ b/browser/tests/testi18nfileresource.py @@ -0,0 +1,152 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""I18n File-Resource Tests + +$Id$ +""" +from unittest import main, makeSuite +import os + +from zope.publisher.interfaces import NotFound + +from zope.app.testing.placelesssetup import PlacelessSetup +from zope.app.testing import ztapi + +from zope.i18n.interfaces import IUserPreferredCharsets, IUserPreferredLanguages + +from zope.publisher.http import IHTTPRequest, HTTPCharsets +from zope.publisher.browser import BrowserLanguages, TestRequest + +from zope.app.publisher.browser.i18nfileresource import I18nFileResource +from zope.app.publisher.browser.i18nfileresource import I18nFileResourceFactory +from zope.app.publisher.fileresource import File +import zope.app.publisher.browser.tests as p + +from zope.i18n.interfaces import INegotiator +from zope.i18n.negotiator import negotiator + +from zope.i18n.tests.testii18naware import TestII18nAware + +test_directory = os.path.dirname(p.__file__) + + +class Test(PlacelessSetup, TestII18nAware): + + def setUp(self): + super(Test, self).setUp() + TestII18nAware.setUp(self) + ztapi.provideAdapter(IHTTPRequest, IUserPreferredCharsets, + HTTPCharsets) + ztapi.provideAdapter(IHTTPRequest, IUserPreferredLanguages, + BrowserLanguages) + # Setup the negotiator utility + ztapi.provideUtility(INegotiator, negotiator) + + + def _createObject(self): + obj = I18nFileResource({'en':None, 'lt':None, 'fr':None}, + TestRequest(), 'fr') + return obj + + + def _createDict(self, filename1='test.pt', filename2='test2.pt'): + path1 = os.path.join(test_directory, 'testfiles', filename1) + path2 = os.path.join(test_directory, 'testfiles', filename2) + return { 'en': File(path1, filename1), + 'fr': File(path2, filename2) } + + + def testNoTraversal(self): + + resource = I18nFileResourceFactory(self._createDict(), 'en')\ + (TestRequest()) + + self.assertRaises(NotFound, + resource.publishTraverse, + resource.request, + '_testData') + + def testFileGET(self): + + # case 1: no language preference, should get en + path = os.path.join(test_directory, 'testfiles', 'test.txt') + + resource = I18nFileResourceFactory(self._createDict('test.txt'), 'en')\ + (TestRequest()) + + + self.assertEqual(resource.GET(), open(path, 'rb').read()) + + response = resource.request.response + self.assertEqual(response.getHeader('Content-Type'), 'text/plain') + + # case 2: prefer lt, have only en and fr, should get en + resource = I18nFileResourceFactory( + self._createDict('test.txt'), 'en')\ + (TestRequest(HTTP_ACCEPT_LANGUAGE='lt')) + + self.assertEqual(resource.GET(), open(path, 'rb').read()) + + response = resource.request.response + self.assertEqual(response.getHeader('Content-Type'), 'text/plain') + + # case 3: prefer fr, have it, should get fr + path = os.path.join(test_directory, 'testfiles', 'test2.pt') + resource = I18nFileResourceFactory( + self._createDict('test.pt', 'test2.pt'), 'en')\ + (TestRequest(HTTP_ACCEPT_LANGUAGE='fr')) + + self.assertEqual(resource.GET(), open(path, 'rb').read()) + + response = resource.request.response + self.assertEqual(response.getHeader('Content-Type'), 'text/html') + + + def testFileHEAD(self): + + # case 1: no language preference, should get en + resource = I18nFileResourceFactory(self._createDict('test.txt'), 'en')\ + (TestRequest()) + + self.assertEqual(resource.HEAD(), '') + + response = resource.request.response + self.assertEqual(response.getHeader('Content-Type'), 'text/plain') + + # case 2: prefer lt, have only en and fr, should get en + resource = I18nFileResourceFactory( + self._createDict('test.txt'), 'en')\ + (TestRequest(HTTP_ACCEPT_LANGUAGE='lt')) + + self.assertEqual(resource.HEAD(), '') + + response = resource.request.response + self.assertEqual(response.getHeader('Content-Type'), 'text/plain') + + # case 3: prefer fr, have it, should get fr + resource = I18nFileResourceFactory( + self._createDict('test.pt', 'test2.pt'), 'en')\ + (TestRequest(HTTP_ACCEPT_LANGUAGE='fr')) + + self.assertEqual(resource.HEAD(), '') + + response = resource.request.response + self.assertEqual(response.getHeader('Content-Type'), 'text/html') + + +def test_suite(): + return makeSuite(Test) + +if __name__=='__main__': + main(defaultTest='test_suite') diff --git a/browser/viewmeta.py b/browser/viewmeta.py new file mode 100644 index 0000000..63e385f --- /dev/null +++ b/browser/viewmeta.py @@ -0,0 +1,452 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Browser configuration code + +$Id$ +""" +import os + +from zope.component.exceptions import ComponentLookupError +from zope.component.interfaces import IDefaultViewName +from zope.interface import implements, classImplements, Interface +from zope.publisher.interfaces import NotFound +from zope.security.checker import CheckerPublic, Checker +from zope.security.checker import defineChecker +from zope.configuration.exceptions import ConfigurationError +from zope.app.component.interface import provideInterface +from zope.publisher.interfaces.browser import IBrowserRequest +from zope.publisher.interfaces.browser import IDefaultBrowserLayer +from zope.publisher.interfaces.browser import IBrowserPublisher +from zope.app import zapi +from zope.app.component.metaconfigure import handler +from zope.app.pagetemplate.simpleviewclass import SimpleViewClass +from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile +from zope.app.publisher.browser import BrowserView +from zope.app.publisher.browser.menu import menuItemDirective + + + +# There are three cases we want to suport: +# +# Named view without pages (single-page view) +# +# +# +# Unnamed view with pages (multi-page view) +# +# +# +# +# +# +# +# Named view with pages (add view is a special case of this) +# +# +# +# +# +# + +# We'll also provide a convenience directive for add views: +# +# +# +# +# +# + +# page + +def page(_context, name, permission, for_, + layer=IDefaultBrowserLayer, template=None, class_=None, + allowed_interface=None, allowed_attributes=None, + attribute='__call__', menu=None, title=None, + ): + + _handle_menu(_context, menu, title, [for_], name, permission) + required = {} + + permission = _handle_permission(_context, permission) + + if not (class_ or template): + raise ConfigurationError("Must specify a class or template") + + if attribute != '__call__': + if template: + raise ConfigurationError( + "Attribute and template cannot be used together.") + + if not class_: + raise ConfigurationError( + "A class must be provided if attribute is used") + + if template: + template = os.path.abspath(str(_context.path(template))) + if not os.path.isfile(template): + raise ConfigurationError("No such file", template) + required['__getitem__'] = permission + + if class_: + if attribute != '__call__': + if not hasattr(class_, attribute): + raise ConfigurationError( + "The provided class doesn't have the specified attribute " + ) + if template: + # class and template + new_class = SimpleViewClass( + template, bases=(class_, )) + else: + if not hasattr(class_, 'browserDefault'): + cdict = { + 'browserDefault': + lambda self, request: (getattr(self, attribute), ()) + } + else: + cdict = {} + + cdict['__page_attribute__'] = attribute + new_class = type(class_.__name__, + (class_, simple,), + cdict) + + if hasattr(class_, '__implements__'): + classImplements(new_class, IBrowserPublisher) + + else: + # template + new_class = SimpleViewClass(template) + + for n in (attribute, 'browserDefault', '__call__', 'publishTraverse'): + required[n] = permission + + _handle_allowed_interface(_context, allowed_interface, permission, + required) + _handle_allowed_attributes(_context, allowed_interface, permission, + required) + + _handle_for(_context, for_) + + defineChecker(new_class, Checker(required)) + + _context.action( + discriminator = ('view', for_, name, IBrowserRequest, layer), + callable = handler, + args = ('provideAdapter', + (for_, layer), Interface, name, new_class, _context.info), + ) + + +# pages, which are just a short-hand for multiple page directives. + +# Note that a class might want to access one of the defined +# templates. If it does though, it should use getMultiAdapter. + +class pages(object): + + def __init__(self, _context, for_, permission, + layer=IDefaultBrowserLayer, class_=None, + allowed_interface=None, allowed_attributes=None, + ): + self.opts = dict(for_=for_, permission=permission, + layer=layer, class_=class_, + allowed_interface=allowed_interface, + allowed_attributes=allowed_attributes, + ) + + def page(self, _context, name, attribute='__call__', template=None, + menu=None, title=None): + return page(_context, + name=name, + attribute=attribute, + template=template, + menu=menu, title=title, + **(self.opts)) + + def __call__(self): + return () + +# view (named view with pages) + +# This is a different case. We actually build a class with attributes +# for all of the given pages. + +class view(object): + + default = None + + def __init__(self, _context, for_, permission, + name='', layer=IDefaultBrowserLayer, class_=None, + allowed_interface=None, allowed_attributes=None, + menu=None, title=None, provides=Interface, + ): + + _handle_menu(_context, menu, title, [for_], name, permission) + + permission = _handle_permission(_context, permission) + + self.args = (_context, name, for_, permission, layer, class_, + allowed_interface, allowed_attributes) + + self.pages = [] + self.menu = menu + self.provides = provides + + def page(self, _context, name, attribute=None, template=None): + if template: + template = os.path.abspath(_context.path(template)) + if not os.path.isfile(template): + raise ConfigurationError("No such file", template) + else: + if not attribute: + raise ConfigurationError( + "Must specify either a template or an attribute name") + + self.pages.append((name, attribute, template)) + return () + + def defaultPage(self, _context, name): + self.default = name + return () + + def __call__(self): + (_context, name, for_, permission, layer, class_, + allowed_interface, allowed_attributes) = self.args + + required = {} + + cdict = {} + pages = {} + + for pname, attribute, template in self.pages: + + if template: + cdict[pname] = ViewPageTemplateFile(template) + if attribute and attribute != name: + cdict[attribute] = cdict[pname] + else: + if not hasattr(class_, attribute): + raise ConfigurationError("Undefined attribute", + attribute) + + attribute = attribute or pname + required[pname] = permission + + pages[pname] = attribute + + # This should go away, but noone seems to remember what to do. :-( + if hasattr(class_, 'publishTraverse'): + + def publishTraverse(self, request, name, + pages=pages, getattr=getattr): + + if name in pages: + return getattr(self, pages[name]) + view = zapi.queryMultiAdapter((self, request), name=name) + if view is not None: + return view + + m = class_.publishTraverse.__get__(self) + return m(request, name) + + else: + def publishTraverse(self, request, name, + pages=pages, getattr=getattr): + + if name in pages: + return getattr(self, pages[name]) + view = zapi.queryMultiAdapter((self, request), name=name) + if view is not None: + return view + + raise NotFound(self, name, request) + + cdict['publishTraverse'] = publishTraverse + + if not hasattr(class_, 'browserDefault'): + if self.default or self.pages: + default = self.default or self.pages[0][0] + cdict['browserDefault'] = ( + lambda self, request, default=default: + (self, (default, )) + ) + elif providesCallable(class_): + cdict['browserDefault'] = ( + lambda self, request: (self, ()) + ) + + if class_ is not None: + bases = (class_, simple) + else: + bases = (simple,) + + try: + cname = str(name) + except: + cname = "GeneratedClass" + + newclass = type(cname, bases, cdict) + + for n in ('publishTraverse', 'browserDefault', '__call__'): + required[n] = permission + + _handle_allowed_interface(_context, allowed_interface, permission, + required) + _handle_allowed_attributes(_context, allowed_interface, permission, + required) + _handle_for(_context, for_) + + defineChecker(newclass, Checker(required)) + + if self.provides is not None: + _context.action( + discriminator = None, + callable = provideInterface, + args = ('', self.provides) + ) + + _context.action( + discriminator = ('view', (for_, layer), name, self.provides), + callable = handler, + args = ('provideAdapter', + (for_, layer), self.provides, name, newclass, + _context.info), + ) + +def addview(_context, name, permission, + layer=IDefaultBrowserLayer, class_=None, + allowed_interface=None, allowed_attributes=None, + menu=None, title=None + ): + return view(_context, name, + 'zope.app.container.interfaces.IAdding', + permission, + layer, class_, + allowed_interface, allowed_attributes, + menu, title + ) + +def defaultView(_context, name, for_=None): + + _context.action( + discriminator = ('defaultViewName', for_, IBrowserRequest, name), + callable = handler, + args = ('provideAdapter', + (for_, IBrowserRequest), IDefaultViewName, '', name, + _context.info) + ) + + if for_ is not None: + _context.action( + discriminator = None, + callable = provideInterface, + args = ('', for_) + ) + + +def _handle_menu(_context, menu, title, for_, name, permission): + if menu or title: + if not (menu and title): + raise ConfigurationError( + "If either menu or title are specified, they must " + "both be specified.") + if len(for_) != 1: + raise ConfigurationError( + "Menus can be specified only for single-view, not for " + "multi-views.") + return menuItemDirective( + _context, menu, for_[0], '@@' + name, title, + permission=permission) + + return [] + + +def _handle_permission(_context, permission): + if permission == 'zope.Public': + permission = CheckerPublic + + return permission + +def _handle_allowed_interface(_context, allowed_interface, permission, + required): + # Allow access for all names defined by named interfaces + if allowed_interface: + for i in allowed_interface: + _context.action( + discriminator = None, + callable = provideInterface, + args = (None, i) + ) + + for name in i: + required[name] = permission + +def _handle_allowed_attributes(_context, allowed_attributes, permission, + required): + # Allow access for all named attributes + if allowed_attributes: + for name in allowed_attributes: + required[name] = permission + +def _handle_for(_context, for_): + if for_ is not None: + _context.action( + discriminator = None, + callable = provideInterface, + args = ('', for_) + ) + +class simple(BrowserView): + implements(IBrowserPublisher) + + def publishTraverse(self, request, name): + raise NotFound(self, name, request) + + def __call__(self, *a, **k): + # If a class doesn't provide it's own call, then get the attribute + # given by the browser default. + + attr = self.__page_attribute__ + if attr == '__call__': + raise AttributeError("__call__") + + meth = getattr(self, attr) + return meth(*a, **k) + +def providesCallable(class_): + if hasattr(class_, '__call__'): + for c in class_.__mro__: + if '__call__' in c.__dict__: + return True + return False diff --git a/xmlrpc/metaconfigure.py b/xmlrpc/metaconfigure.py new file mode 100644 index 0000000..e59f38a --- /dev/null +++ b/xmlrpc/metaconfigure.py @@ -0,0 +1,103 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""XMLRPC configuration code + +$Id$ +""" +import zope.interface +from zope.interface import Interface +from zope.security.checker import CheckerPublic, Checker +from zope.configuration.exceptions import ConfigurationError +from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest + +from zope.app import zapi +from zope.app.location import Location +from zope.app.component.interface import provideInterface +from zope.app.component.metaconfigure import handler + +def view(_context, for_=None, interface=None, methods=None, + class_=None, permission=None, name=None): + + interface = interface or [] + methods = methods or [] + + # If there were special permission settings provided, then use them + if permission == 'zope.Public': + permission = CheckerPublic + + require = {} + for attr_name in methods: + require[attr_name] = permission + + if interface: + for iface in interface: + for field_name in iface: + require[field_name] = permission + _context.action( + discriminator = None, + callable = provideInterface, + args = ('', for_) + ) + + if name: + # Register a single view + + if permission: + checker = Checker(require) + + def proxyView(context, request, class_=class_, checker=checker): + view = class_(context, request) + # We need this in case the resource gets unwrapped and + # needs to be rewrapped + view.__Security_checker__ = checker + return view + + class_ = proxyView + + # Register the new view. + _context.action( + discriminator = ('view', for_, name, IXMLRPCRequest), + callable = handler, + args = ('provideAdapter', + (for_, IXMLRPCRequest), Interface, name, class_, + _context.info) + ) + else: + if permission: + checker = Checker({'__call__': permission}) + else: + checker = None + + for name in require: + # create a new callable class with a security checker; mix + # in zope.app.location.Location so that the view inherits + # a security context + cdict = {'__Security_checker__': checker, + '__call__': getattr(class_, name)} + new_class = type(class_.__name__, (class_, Location), cdict) + _context.action( + discriminator = ('view', for_, name, IXMLRPCRequest), + callable = handler, + args = ('provideAdapter', + (for_, IXMLRPCRequest), Interface, name, new_class, + _context.info) + ) + + # Register the used interfaces with the interface service + if for_ is not None: + _context.action( + discriminator = None, + callable = provideInterface, + args = ('', for_) + ) diff --git a/xmlrpc/tests/test_directives.py b/xmlrpc/tests/test_directives.py new file mode 100644 index 0000000..1f20bf9 --- /dev/null +++ b/xmlrpc/tests/test_directives.py @@ -0,0 +1,97 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Test 'xmlrpc' ZCML Namespace directives. + +$Id$ +""" +import unittest + +from zope.configuration import xmlconfig +from zope.configuration.exceptions import ConfigurationError +from zope.app.component.tests.views import IC, V1 +from zope.app.testing.placelesssetup import PlacelessSetup +from zope.security.proxy import ProxyFactory + +from zope.component.tests.request import Request + +from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest + +from zope.app import zapi +from zope.app.publisher import xmlrpc +from zope.interface import implements + + +request = Request(IXMLRPCRequest) + +class Ob(object): + implements(IC) + +ob = Ob() + +class DirectivesTest(PlacelessSetup, unittest.TestCase): + + def testView(self): + self.assertEqual( + zapi.queryMultiAdapter((ob, request), name='test'), None) + xmlconfig.file("xmlrpc.zcml", xmlrpc.tests) + self.assertEqual( + zapi.queryMultiAdapter((ob, request), name='test').__class__, V1) + + def testInterfaceProtectedView(self): + xmlconfig.file("xmlrpc.zcml", xmlrpc.tests) + v = zapi.getMultiAdapter((ob, request), name='test2') + v = ProxyFactory(v) + self.assertEqual(v.index(), 'V1 here') + self.assertRaises(Exception, getattr, v, 'action') + + def testAttributeProtectedView(self): + xmlconfig.file("xmlrpc.zcml", xmlrpc.tests) + v = zapi.getMultiAdapter((ob, request), name='test3') + v = ProxyFactory(v) + self.assertEqual(v.action(), 'done') + self.assertRaises(Exception, getattr, v, 'index') + + def testInterfaceAndAttributeProtectedView(self): + xmlconfig.file("xmlrpc.zcml", xmlrpc.tests) + v = zapi.getMultiAdapter((ob, request), name='test4') + self.assertEqual(v.index(), 'V1 here') + self.assertEqual(v.action(), 'done') + + def testDuplicatedInterfaceAndAttributeProtectedView(self): + xmlconfig.file("xmlrpc.zcml", xmlrpc.tests) + v = zapi.getMultiAdapter((ob, request), name='test5') + self.assertEqual(v.index(), 'V1 here') + self.assertEqual(v.action(), 'done') + + def testIncompleteProtectedViewNoPermission(self): + self.assertRaises(ConfigurationError, xmlconfig.file, + "xmlrpc_error.zcml", xmlrpc.tests) + + def test_no_name(self): + xmlconfig.file("xmlrpc.zcml", xmlrpc.tests) + v = zapi.getMultiAdapter((ob, request), name='index') + self.assertEqual(v(), 'V1 here') + v = zapi.getMultiAdapter((ob, request), name='action') + self.assertEqual(v(), 'done') + + + + +def test_suite(): + return unittest.TestSuite(( + unittest.makeSuite(DirectivesTest), + )) + +if __name__ == '__main__': + unittest.main()