[Zope-Checkins] SVN: Products.Five/branches/1.4/ Backporting
viewlet support from trunk
Alec Mitchell
apm13 at columbia.edu
Tue May 2 13:56:18 EDT 2006
Log message for revision 67844:
Backporting viewlet support from trunk
Changed:
U Products.Five/branches/1.4/CHANGES.txt
U Products.Five/branches/1.4/README.txt
A Products.Five/branches/1.4/browser/ProviderExpression.py
U Products.Five/branches/1.4/browser/TrustedExpression.py
U Products.Five/branches/1.4/browser/pagetemplatefile.py
A Products.Five/branches/1.4/browser/tests/provider.txt
A Products.Five/branches/1.4/browser/tests/provider.zcml
A Products.Five/branches/1.4/browser/tests/provider_error.pt
A Products.Five/branches/1.4/browser/tests/provider_messagebox.pt
A Products.Five/branches/1.4/browser/tests/provider_namespace.pt
A Products.Five/branches/1.4/browser/tests/provider_namespace2.pt
A Products.Five/branches/1.4/browser/tests/test_provider.py
A Products.Five/branches/1.4/viewlet/
A Products.Five/branches/1.4/viewlet/README.txt
A Products.Five/branches/1.4/viewlet/__init__.py
A Products.Five/branches/1.4/viewlet/configure.zcml
A Products.Five/branches/1.4/viewlet/css_viewlet.pt
A Products.Five/branches/1.4/viewlet/directives.txt
A Products.Five/branches/1.4/viewlet/javascript_viewlet.pt
A Products.Five/branches/1.4/viewlet/manager.py
A Products.Five/branches/1.4/viewlet/meta.zcml
A Products.Five/branches/1.4/viewlet/metaconfigure.py
A Products.Five/branches/1.4/viewlet/tests.py
A Products.Five/branches/1.4/viewlet/viewlet.py
-=-
Modified: Products.Five/branches/1.4/CHANGES.txt
===================================================================
--- Products.Five/branches/1.4/CHANGES.txt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/CHANGES.txt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -2,6 +2,14 @@
Five Changes
============
+Five 1.5 (unreleased)
+=====================
+
+Features
+--------
+
+* Added Viewlet and Content Provider support.
+
Five 1.4 (unreleased)
=====================
Modified: Products.Five/branches/1.4/README.txt
===================================================================
--- Products.Five/branches/1.4/README.txt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/README.txt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -36,6 +36,8 @@
* Zope 2 security declarations in ZCML instead of in Python code.
+* Content Providers and Viewlets
+
Together with another product, CMFonFive, Five can integrate into CMF.
For more information, see ``doc/features.txt``.
Added: Products.Five/branches/1.4/browser/ProviderExpression.py
===================================================================
--- Products.Five/branches/1.4/browser/ProviderExpression.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/ProviderExpression.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,62 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""Provider tales expression registrations
+
+$Id: tales.py 39606 2005-10-25 02:59:26Z srichter $
+"""
+__docformat__ = 'restructuredtext'
+from Products.PageTemplates.Expressions import StringExpr
+from Products.PageTemplates.Expressions import getEngine
+from AccessControl.ZopeGuards import guarded_hasattr
+from AccessControl.ZopeSecurityPolicy import getRoles
+import Products.Five.security
+
+import zope.component
+import zope.schema
+import zope.interface
+from zope.contentprovider import interfaces
+from zope.contentprovider.tales import addTALNamespaceData
+
+_noroles = []
+
+class ProviderExpr(StringExpr):
+ """A provider expression for Zope2 templates.
+ """
+
+ zope.interface.implements(interfaces.ITALESProviderExpression)
+ def __call__(self, econtext):
+ name = StringExpr.__call__(self, econtext)
+ context = econtext.vars['context']
+ request = econtext.vars['request']
+ view = econtext.vars['view']
+
+ # Try to look up the provider.
+ provider = zope.component.queryMultiAdapter(
+ (context, request, view), interfaces.IContentProvider, name)
+
+ # Provide a useful error message, if the provider was not found.
+ if provider is None:
+ raise interfaces.ContentProviderLookupError(name)
+
+ # Insert the data gotten from the context
+ addTALNamespaceData(provider, econtext)
+
+ # Stage 1: Do the state update.
+ provider.update()
+
+ # Stage 2: Render the HTML content.
+ return provider.render()
+
+# Register Provider expression
+getEngine().registerType('provider', ProviderExpr)
Modified: Products.Five/branches/1.4/browser/TrustedExpression.py
===================================================================
--- Products.Five/branches/1.4/browser/TrustedExpression.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/TrustedExpression.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -25,6 +25,8 @@
getEngine, installHandlers,\
SecureModuleImporter
+from Products.Five.browser.ProviderExpression import ProviderExpr
+
from ReuseUtils import rebindFunction
ModuleImporter = SecureModuleImporter
@@ -91,18 +93,21 @@
__init__ = rebindFunction(StringExpr.__init__.im_func,
PathExpr=PathExpr,
)
-
+
+
installHandlers = rebindFunction(installHandlers,
PathExpr=PathExpr,
StringExpr=StringExpr,
PythonExpr=PythonExpr,
)
+def installHandlers2(engine):
+ installHandlers(engine)
+ engine.registerType('provider', ProviderExpr)
+
_engine=None
getEngine = rebindFunction(getEngine,
_engine=_engine,
- installHandlers=installHandlers
+ installHandlers=installHandlers2
)
-
-
Modified: Products.Five/branches/1.4/browser/pagetemplatefile.py
===================================================================
--- Products.Five/branches/1.4/browser/pagetemplatefile.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/pagetemplatefile.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -34,10 +34,10 @@
Uses Zope 2's engine, but with security disabled and with some
initialization and API from Zope 3.
"""
-
+
def __init__(self, filename, _prefix=None, content_type=None):
# XXX doesn't use content_type yet
-
+
self.ZBindings_edit(self._default_bindings)
path = self.get_path_from_prefix(_prefix)
@@ -55,24 +55,24 @@
if _prefix is None:
_prefix = sys._getframe(2).f_globals
path = package_home(_prefix)
- return path
+ return path
_cook = rebindFunction(PageTemplateFile._cook,
getEngine=getEngine)
-
+
pt_render = rebindFunction(PageTemplateFile.pt_render,
getEngine=getEngine)
def _pt_getContext(self):
try:
root = self.getPhysicalRoot()
- view = self._getContext()
except AttributeError:
- # self has no attribute getPhysicalRoot. This typically happens
- # when the template has no proper acquisition context.
- # That also means it has no view. /regebro
root = self.context.getPhysicalRoot()
- view = None
+ # Even if the context isn't a view (when would that be exaclty?),
+ # there shouldn't be any dange in applying a view, because it
+ # won't be used. However assuming that a lack of getPhysicalRoot
+ # implies a missing view causes problems.
+ view = self._getContext()
here = self.context.aq_inner
@@ -87,7 +87,7 @@
'request': request,
'modules': ModuleImporter,
}
- if view:
+ if view is not None:
c['view'] = view
c['views'] = ViewMapper(here, request)
Added: Products.Five/branches/1.4/browser/tests/provider.txt
===================================================================
--- Products.Five/branches/1.4/browser/tests/provider.txt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/tests/provider.txt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,191 @@
+=================
+Content Providers
+=================
+
+We need some tests for the Zope2 versions of the TAL directives for provider.
+To this end we have copied the tests from zope.contentprovider and made them
+work with Five. We have defined a muber of relevant views which use the
+new tal expression in providers.zcml:
+
+ >>> from zope.contentprovider import interfaces
+ >>> import Products.Five.browser.tests
+ >>> from Products.Five import zcml
+ >>> zcml.load_config("configure.zcml", Products.Five)
+ >>> zcml.load_config('provider.zcml', package=Products.Five.browser.tests)
+
+Content Providers
+-----------------
+
+Content Provider is a term from the Java world that refers to components that
+can provide HTML content. It means nothing more! How the content is found and
+returned is totally up to the implementation. The Zope 3 touch to the concept
+is that content providers are multi-adapters that are looked up by the
+context, request (and thus the layer/skin), and view they are displayed in.
+
+So let's create a simple content provider:
+
+ >>> import zope.interface
+ >>> import zope.component
+ >>> from zope.publisher.interfaces import browser
+
+ >>> class MessageBox(object):
+ ... zope.interface.implements(interfaces.IContentProvider)
+ ... zope.component.adapts(zope.interface.Interface,
+ ... browser.IDefaultBrowserLayer,
+ ... zope.interface.Interface)
+ ... message = u'My Message'
+ ...
+ ... def __init__(self, context, request, view):
+ ... self.__parent__ = view
+ ...
+ ... def update(self):
+ ... pass
+ ...
+ ... def render(self):
+ ... return u'<div class="box">%s</div>' %self.message
+
+The ``update()`` method is executed during phase one. Since no state needs to
+be calculated and no data is modified by this simple content provider, it is
+an empty implementation. The ``render()`` method implements phase 2 of the
+process. We can now instantiate the content provider (manually) and render it:
+
+ >>> box = MessageBox(None, None, None)
+ >>> box.render()
+ u'<div class="box">My Message</div>'
+
+Since our content provider did not require the context, request or view to
+create its HTML content, we were able to pass trivial dummy values into the
+constructor. Also note that the provider must have a parent (using the
+``__parent__`` attribute) specified at all times. The parent must be the view
+the provider appears in.
+
+The TALES ``provider`` Expression
+---------------------------------
+
+The ``provider`` expression will look up the name of the content provider,
+call it and return the HTML content. The first step, however, will be to
+register our content provider with the component architecture:
+
+ >>> zope.component.provideAdapter(MessageBox, name='mypage.MessageBox')
+
+The content provider must be registered by name, since the TALES expression
+uses the name to look up the provider at run time.
+
+ >>> from Products.Five.tests.testing.simplecontent import manage_addSimpleContent
+ >>> manage_addSimpleContent(self.folder, 'content_obj', 'ContentObj')
+ >>> content = self.folder.content_obj
+
+Finally we publish the view:
+
+ >>> print http(r'''
+ ... GET /test_folder_1_/content_obj/main.html HTTP/1.1
+ ... ''')
+ HTTP/1.1 200 OK
+ ...
+ <html>
+ <body>
+ <h1>My Web Page</h1>
+ <div class="left-column">
+ <div class="box">My Message</div>
+ </div>
+ <div class="main">
+ Content here
+ </div>
+ </body>
+ </html>
+
+
+Failure to lookup a Content Provider
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ >>> print http(r'''
+ ... GET /test_folder_1_/content_obj/error.html HTTP/1.1
+ ... ''')
+ HTTP/1.1 500 Internal Server Error
+ ...
+ ...ContentProviderLookupError: 'mypage.UnknownName'
+ ...
+
+Additional Data from TAL
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``provider`` expression allows also for transferring data from the TAL
+context into the content provider. This is accomplished by having the content
+provider implement an interface that specifies the attributes and provides
+``ITALNamespaceData``:
+
+ >>> import zope.schema
+ >>> class IMessageText(zope.interface.Interface):
+ ... message = zope.schema.Text(title=u'Text of the message box')
+
+ >>> zope.interface.directlyProvides(IMessageText,
+ ... interfaces.ITALNamespaceData)
+
+Now the message box can receive its text from the TAL environment:
+
+ >>> class DynamicMessageBox(MessageBox):
+ ... zope.interface.implements(IMessageText)
+
+ >>> zope.component.provideAdapter(
+ ... DynamicMessageBox, provides=interfaces.IContentProvider,
+ ... name='mypage.DynamicMessageBox')
+
+Now we should get two message boxes with different text:
+
+ >>> print http(r'''
+ ... GET /test_folder_1_/content_obj/namespace.html HTTP/1.1
+ ... ''')
+ HTTP/1.1 200 OK
+ ...
+ <html>
+ <body>
+ <h1>My Web Page</h1>
+ <div class="left-column">
+ <div class="box">Hello World!</div>
+ <div class="box">Hello World again!</div>
+ </div>
+ <div class="main">
+ Content here
+ </div>
+ </body>
+ </html>
+
+Finally, a content provider can also implement several ``ITALNamespaceData``:
+
+ >>> class IMessageType(zope.interface.Interface):
+ ... type = zope.schema.TextLine(title=u'The type of the message box')
+
+ >>> zope.interface.directlyProvides(IMessageType,
+ ... interfaces.ITALNamespaceData)
+
+We'll change our message box content provider implementation a bit, so the new
+information is used:
+
+ >>> class BetterDynamicMessageBox(DynamicMessageBox):
+ ... zope.interface.implements(IMessageType)
+ ... type = None
+ ...
+ ... def render(self):
+ ... return u'<div class="box,%s">%s</div>' %(self.type, self.message)
+
+ >>> zope.component.provideAdapter(
+ ... BetterDynamicMessageBox, provides=interfaces.IContentProvider,
+ ... name='mypage.MessageBox')
+
+ >>> print http(r'''
+ ... GET /test_folder_1_/content_obj/namespace2.html HTTP/1.1
+ ... ''')
+ HTTP/1.1 200 OK
+ ...
+ <html>
+ <body>
+ <h1>My Web Page</h1>
+ <div class="left-column">
+ <div class="box,error">Hello World!</div>
+ <div class="box,warning">Hello World again!</div>
+ </div>
+ <div class="main">
+ Content here
+ </div>
+ </body>
+ </html>
Added: Products.Five/branches/1.4/browser/tests/provider.zcml
===================================================================
--- Products.Five/branches/1.4/browser/tests/provider.zcml 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/tests/provider.zcml 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,33 @@
+<configure xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:meta="http://namespaces.zope.org/meta">
+
+ <!-- make the zope2.Public permission work -->
+ <meta:redefinePermission from="zope2.Public" to="zope.Public" />
+
+ <!-- stuff for content providers -->
+ <browser:page
+ for="Products.Five.tests.testing.simplecontent.ISimpleContent"
+ template="provider_messagebox.pt"
+ name="main.html"
+ permission="zope2.Public"
+ />
+ <browser:page
+ for="Products.Five.tests.testing.simplecontent.ISimpleContent"
+ template="provider_error.pt"
+ name="error.html"
+ permission="zope2.Public"
+ />
+ <browser:page
+ for="Products.Five.tests.testing.simplecontent.ISimpleContent"
+ template="provider_namespace.pt"
+ name="namespace.html"
+ permission="zope2.Public"
+ />
+ <browser:page
+ for="Products.Five.tests.testing.simplecontent.ISimpleContent"
+ template="provider_namespace2.pt"
+ name="namespace2.html"
+ permission="zope2.Public"
+ />
+
+</configure>
Added: Products.Five/branches/1.4/browser/tests/provider_error.pt
===================================================================
--- Products.Five/branches/1.4/browser/tests/provider_error.pt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/tests/provider_error.pt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,5 @@
+ <html>
+ <body>
+ <tal:block replace="structure provider:mypage.UnknownName" />
+ </body>
+ </html>
Added: Products.Five/branches/1.4/browser/tests/provider_messagebox.pt
===================================================================
--- Products.Five/branches/1.4/browser/tests/provider_messagebox.pt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/tests/provider_messagebox.pt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,11 @@
+ <html>
+ <body>
+ <h1>My Web Page</h1>
+ <div class="left-column">
+ <tal:block replace="structure provider:mypage.MessageBox" />
+ </div>
+ <div class="main">
+ Content here
+ </div>
+ </body>
+ </html>
\ No newline at end of file
Added: Products.Five/branches/1.4/browser/tests/provider_namespace.pt
===================================================================
--- Products.Five/branches/1.4/browser/tests/provider_namespace.pt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/tests/provider_namespace.pt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,14 @@
+ <html>
+ <body>
+ <h1>My Web Page</h1>
+ <div class="left-column">
+ <tal:block define="message string:Hello World!"
+ replace="structure provider:mypage.DynamicMessageBox" />
+ <tal:block define="message string:Hello World again!"
+ replace="structure provider:mypage.DynamicMessageBox" />
+ </div>
+ <div class="main">
+ Content here
+ </div>
+ </body>
+ </html>
Added: Products.Five/branches/1.4/browser/tests/provider_namespace2.pt
===================================================================
--- Products.Five/branches/1.4/browser/tests/provider_namespace2.pt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/tests/provider_namespace2.pt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,16 @@
+ <html>
+ <body>
+ <h1>My Web Page</h1>
+ <div class="left-column">
+ <tal:block define="message string:Hello World!;
+ type string:error"
+ replace="structure provider:mypage.MessageBox" />
+ <tal:block define="message string:Hello World again!;
+ type string:warning"
+ replace="structure provider:mypage.MessageBox" />
+ </div>
+ <div class="main">
+ Content here
+ </div>
+ </body>
+ </html>
Added: Products.Five/branches/1.4/browser/tests/test_provider.py
===================================================================
--- Products.Five/branches/1.4/browser/tests/test_provider.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/browser/tests/test_provider.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) 2005 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 pages
+
+$Id: test_skin.py 61865 2005-11-19 09:54:53Z philikon $
+"""
+import os, sys
+if __name__ == '__main__':
+ execfile(os.path.join(sys.path[0], 'framework.py'))
+
+def test_suite():
+ from Testing.ZopeTestCase import FunctionalDocFileSuite
+ return FunctionalDocFileSuite('provider.txt',
+ package='Products.Five.browser.tests')
+
+if __name__ == '__main__':
+ framework()
Added: Products.Five/branches/1.4/viewlet/README.txt
===================================================================
--- Products.Five/branches/1.4/viewlet/README.txt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/README.txt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,939 @@
+=============================
+Viewlets and Viewlet Managers
+=============================
+
+Let's start with some motivation. Using content providers allows us to insert
+one piece of HTML content. In most Web development, however, you are often
+interested in defining some sort of region and then allow developers to
+register content for those regions.
+
+ >>> from zope.viewlet import interfaces
+
+Setup traversal stuff
+
+ >>> import Products.Five
+ >>> from Products.Five import zcml
+ >>> zcml.load_config("configure.zcml", Products.Five)
+
+Set a loose security policy because these are unit tests, security will be
+tested in another file:
+
+ >>> from AccessControl import SecurityManager
+ >>> from Products.Five.viewlet.tests import UnitTestSecurityPolicy
+ >>> from AccessControl.SecurityManagement import newSecurityManager
+ >>> from AccessControl.SecurityManagement import noSecurityManager
+ >>> noSecurityManager()
+ >>> oldPolicy = SecurityManager.setSecurityPolicy(UnitTestSecurityPolicy())
+
+Design Notes
+------------
+
+As mentioned above, besides inserting snippets of HTML at places, we more
+frequently want to define a region in our page and allow specialized content
+providers to be inserted based on configuration. Those specialized content
+providers are known viewlets and are only available inside viewlet managers,
+which are just a more complex example of content providers.
+
+Unfortunately, the Java world does not implement this layer separately. The
+viewlet manager is most similar to a Java "channel", but we decided against
+using this name, since it is very generic and not very meaningful. The viewlet
+has no Java counterpart, since Java does not implement content providers using
+a component architecture and thus does not register content providers
+specifically for viewlet managers, which I believe makes the Java
+implementation less usefull as a generic concept. In fact, the main design
+goal in the Java world is the implementation of reusable and sharable
+portlets. The scope for Zope 3 is larger, since we want to provide a generic
+framework for building pluggable user interfaces.
+
+
+The Viewlet Manager
+-------------------
+
+In this implementation of viewlets, those regions are just content providers
+called viewlet managers that manage a special type of content providers known
+as viewlets. Every viewlet manager handles the viewlets registered for it:
+
+ >>> from Products.Five.viewlet.tests import ILeftColumn
+
+You can then create a viewlet manager using this interface now:
+
+ >>> from Products.Five.viewlet import manager
+ >>> LeftColumn = manager.ViewletManager('left', ILeftColumn)
+
+Now we have to instantiate it in the context of an actual zope object:
+
+ >>> import zope.interface
+ >>> from OFS import SimpleItem, Folder
+ >>> class Content(SimpleItem.SimpleItem):
+ ... zope.interface.implements(zope.interface.Interface)
+ >>> obj_id = self.folder._setObject('content1', Content())
+ >>> content = self.folder[obj_id]
+
+ >>> from Products.Five.traversable import FakeRequest
+ >>> request = FakeRequest()
+ >>> from zope.app.publication.browser import setDefaultSkin
+ >>> setDefaultSkin(request)
+
+ >>> from Products.Five.browser import BrowserView as View
+ >>> view = View(content, request)
+
+ >>> leftColumn = LeftColumn(content, request, view)
+
+So initially nothing gets rendered:
+
+ >>> leftColumn.update()
+ >>> leftColumn.render()
+ u''
+
+But now we register some viewlets for the manager
+
+ >>> import zope.component
+ >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+ >>> from zope.app.publisher.interfaces.browser import IBrowserView
+
+ >>> from Acquisition import Explicit
+ >>> class WeatherBox(Explicit):
+ ... zope.interface.implements(interfaces.IViewlet)
+ ...
+ ... def __init__(self, context, request, view, manager):
+ ... self.__parent__ = view
+ ... self.context = context
+ ...
+ ... def update(self):
+ ... pass
+ ...
+ ... def render(self):
+ ... return u'<div class="box">It is sunny today!</div>'
+
+ >>> zope.component.provideAdapter(
+ ... WeatherBox,
+ ... (zope.interface.Interface, IDefaultBrowserLayer,
+ ... IBrowserView, ILeftColumn),
+ ... interfaces.IViewlet, name='weather')
+
+ >>> class SportBox(Explicit):
+ ... zope.interface.implements(interfaces.IViewlet)
+ ...
+ ... def __init__(self, context, request, view, manager):
+ ... self.__parent__ = view
+ ... self.context = context
+ ...
+ ... def update(self):
+ ... pass
+ ...
+ ... def render(self):
+ ... return u'<div class="box">Patriots (23) : Steelers (7)</div>'
+
+ >>> zope.component.provideAdapter(
+ ... SportBox,
+ ... (zope.interface.Interface, IDefaultBrowserLayer,
+ ... IBrowserView, ILeftColumn),
+ ... interfaces.IViewlet, name='sport')
+
+and thus the left column is filled:
+
+ >>> leftColumn.update()
+ >>> print leftColumn.render()
+ <div class="box">Patriots (23) : Steelers (7)</div>
+ <div class="box">It is sunny today!</div>
+
+But this is of course pretty lame, since there is no way of specifying how the
+viewlets are put together. But we have a solution. The second argument of the
+``ViewletManager()`` function is a template in which we can specify how the
+viewlets are put together:
+
+ >>> import os, tempfile
+ >>> temp_dir = tempfile.mkdtemp()
+ >>> leftColTemplate = os.path.join(temp_dir, 'leftCol.pt')
+ >>> open(leftColTemplate, 'w').write('''
+ ... <div class="left-column">
+ ... <tal:block repeat="viewlet options/viewlets"
+ ... replace="structure viewlet/render" />
+ ... </div>
+ ... ''')
+
+ >>> LeftColumn = manager.ViewletManager('left', ILeftColumn,
+ ... template=leftColTemplate)
+ >>> leftColumn = LeftColumn(content, request, view)
+
+TODO: Fix this silly thing; viewlets should be directly available.
+
+As you can see, the viewlet manager provides a global ``options/viewlets``
+variable that is an iterable of all the avialable viewlets in the correct
+order:
+
+ >>> leftColumn.update()
+ >>> print leftColumn.render().strip()
+ <div class="left-column">
+ <div class="box">Patriots (23) : Steelers (7)</div>
+ <div class="box">It is sunny today!</div>
+ </div>
+
+You can also lookup the viewlets directly for management purposes:
+
+ >>> leftColumn['weather']
+ <WeatherBox ...>
+ >>> leftColumn.get('weather')
+ <WeatherBox ...>
+
+If the viewlet is not found, then the expected behavior is provided:
+
+ >>> leftColumn['stock']
+ Traceback (most recent call last):
+ ...
+ ComponentLookupError: 'No provider with name `stock` found.'
+
+ >>> leftColumn.get('stock') is None
+ True
+
+Customizing the default Viewlet Manager
+---------------------------------------
+
+One important feature of any viewlet manager is to be able to filter and sort
+the viewlets it is displaying. The default viewlet manager that we have been
+using in the tests above, supports filtering by access availability and
+sorting via the viewlet's ``__cmp__()`` method (default). You can easily
+override this default policy by providing a base viewlet manager class.
+
+In our case we will manage the viewlets using a global list:
+
+ >>> shown = ['weather', 'sport']
+
+The viewlet manager base class now uses this list:
+
+ >>> class ListViewletManager(object):
+ ...
+ ... def filter(self, viewlets):
+ ... viewlets = super(ListViewletManager, self).filter(viewlets)
+ ... return [(name, viewlet)
+ ... for name, viewlet in viewlets
+ ... if name in shown]
+ ...
+ ... def sort(self, viewlets):
+ ... viewlets = dict(viewlets)
+ ... return [(name, viewlets[name]) for name in shown]
+
+Let's now create a new viewlet manager:
+
+ >>> LeftColumn = manager.ViewletManager(
+ ... 'left', ILeftColumn, bases=(ListViewletManager,),
+ ... template=leftColTemplate)
+ >>> leftColumn = LeftColumn(content, request, view)
+
+So we get the weather box first and the sport box second:
+
+ >>> leftColumn.update()
+ >>> print leftColumn.render().strip()
+ <div class="left-column">
+ <div class="box">It is sunny today!</div>
+ <div class="box">Patriots (23) : Steelers (7)</div>
+ </div>
+
+Now let's change the order...
+
+ >>> shown.reverse()
+
+and the order should switch as well:
+
+ >>> leftColumn.update()
+ >>> print leftColumn.render().strip()
+ <div class="left-column">
+ <div class="box">Patriots (23) : Steelers (7)</div>
+ <div class="box">It is sunny today!</div>
+ </div>
+
+Of course, we also can remove a shown viewlet:
+
+ >>> weather = shown.pop()
+ >>> leftColumn.update()
+ >>> print leftColumn.render().strip()
+ <div class="left-column">
+ <div class="box">Patriots (23) : Steelers (7)</div>
+ </div>
+
+
+Viewlet Base Classes
+--------------------
+
+To make the creation of viewlets simpler, a set of useful base classes and
+helper functions are provided:
+
+ >>> from Products.Five.viewlet import viewlet
+
+The first class is a base class that simply defines the constructor:
+
+ >>> base = viewlet.ViewletBase('context', 'request', 'view', 'manager')
+ >>> base.context
+ 'context'
+ >>> base.request
+ 'request'
+ >>> base.__parent__
+ 'view'
+ >>> base.manager
+ 'manager'
+
+But a default ``render()`` method implementation is not provided:
+
+ >>> base.render()
+ Traceback (most recent call last):
+ ...
+ NotImplementedError: `render` method must be implemented by subclass.
+
+If you have already an existing class that produces the HTML content in some
+method, then the ``SimpleAttributeViewlet`` might be for you, since it can be
+used to convert any class quickly into a viewlet:
+
+ >>> class FooViewlet(viewlet.SimpleAttributeViewlet):
+ ... __page_attribute__ = 'foo'
+ ...
+ ... def foo(self):
+ ... return 'output'
+
+The `__page_attribute__` attribute provides the name of the function to call for
+rendering.
+
+ >>> foo = FooViewlet('context', 'request', 'view', 'manager')
+ >>> foo.foo()
+ 'output'
+ >>> foo.render()
+ 'output'
+
+If you specify `render` as the attribute an error is raised to prevent
+infinite recursion:
+
+ >>> foo.__page_attribute__ = 'render'
+ >>> foo.render()
+ Traceback (most recent call last):
+ ...
+ AttributeError: render
+
+The same is true if the specified attribute does not exist:
+
+ >>> foo.__page_attribute__ = 'bar'
+ >>> foo.render()
+ Traceback (most recent call last):
+ ...
+ AttributeError: bar
+
+To create simple template-based viewlets you can use the
+``SimpleViewletClass()`` function. This function is very similar to its view
+equivalent and is used by the ZCML directives to create viewlets. The result
+of this function call will be a fully functional viewlet class. Let's start by
+simply specifying a template only:
+
+ >>> template = os.path.join(temp_dir, 'demoTemplate.pt')
+ >>> open(template, 'w').write('''<div>contents</div>''')
+
+ >>> Demo = viewlet.SimpleViewletClass(template)
+ >>> print Demo(content, request, view, manager).render()
+ <div>contents</div>
+
+Now let's additionally specify a class that can provide additional features:
+
+ >>> class MyViewlet(object):
+ ... myAttribute = 8
+
+ >>> Demo = viewlet.SimpleViewletClass(template, bases=(MyViewlet,))
+ >>> MyViewlet in Demo.__bases__
+ True
+ >>> Demo(content, request, view, manager).myAttribute
+ 8
+
+The final important feature is the ability to pass in further attributes to
+the class:
+
+ >>> Demo = viewlet.SimpleViewletClass(
+ ... template, attributes={'here': 'now', 'lucky': 3})
+ >>> demo = Demo(content, request, view, manager)
+ >>> demo.here
+ 'now'
+ >>> demo.lucky
+ 3
+
+As for all views, they must provide a name that can also be passed to the
+function:
+
+ >>> Demo = viewlet.SimpleViewletClass(template, name='demoViewlet')
+ >>> demo = Demo(content, request, view, manager)
+ >>> demo.__name__
+ 'demoViewlet'
+
+In addition to the the generic viewlet code above, the package comes with two
+viewlet base classes and helper functions for inserting CSS and Javascript
+links into HTML headers, since those two are so very common. I am only going
+to demonstrate the helper functions here, since those demonstrations will
+fully demonstrate the functionality of the base classes as well.
+
+To make resource lookup work we need to make the content traversable:
+ >>> try:
+ ... from Products.Five.fiveconfigure import classTraversable
+ ... classTraversable(Content)
+ ...
+ ... except ImportError:
+ ... pass
+
+The viewlet will look up the resource it was given and tries to produce the
+absolute URL for it:
+
+ >>> class JSResource(Explicit):
+ ... def __init__(self, request):
+ ... self.request = request
+ ...
+ ... def __call__(self):
+ ... return '/@@/resource.js'
+
+ >>> from zope.app.testing import ztapi
+ >>> ztapi.browserResource('resource.js', JSResource)
+ >>> JSViewlet = viewlet.JavaScriptViewlet('resource.js')
+ >>> print JSViewlet(content, request, view, manager).render().strip()
+ <script type="text/javascript" src="/@@/resource.js">
+ </script>
+
+The same works for the CSS resource viewlet:
+
+ >>> class CSSResource(Explicit):
+ ... def __init__(self, request):
+ ... self.request = request
+ ...
+ ... def __call__(self):
+ ... return '/@@/resource.css'
+
+ >>> ztapi.browserResource('resource.css', CSSResource)
+
+ >>> CSSViewlet = viewlet.CSSViewlet('resource.css')
+ >>> print CSSViewlet(content, request, view, manager).render().strip()
+ <link type="text/css" rel="stylesheet"
+ href="/@@/resource.css" media="all" />
+
+You can also change the media type and the rel attribute:
+
+ >>> CSSViewlet = viewlet.CSSViewlet('resource.css', media='print', rel='css')
+ >>> print CSSViewlet(content, request, view, manager).render().strip()
+ <link type="text/css" rel="css" href="/@@/resource.css"
+ media="print" />
+
+
+A Complex Example
+-----------------
+
+The Data
+~~~~~~~~
+
+So far we have only demonstrated simple (maybe overly trivial) use cases of
+the viewlet system. In the following example, we are going to develop a
+generic contents view for files. The step is to create a file component:
+
+ >>> class IFile(zope.interface.Interface):
+ ... data = zope.interface.Attribute('Data of file.')
+
+ >>> class File(SimpleItem.SimpleItem):
+ ... zope.interface.implements(IFile)
+ ... def __init__(self, data=''):
+ ... self.__name__ = ''
+ ... self.data = data
+
+Since we want to also provide the size of a file, here a simple implementation
+of the ``ISized`` interface:
+
+ >>> from zope.app import size
+ >>> class FileSized(object):
+ ... zope.interface.implements(size.interfaces.ISized)
+ ... zope.component.adapts(IFile)
+ ...
+ ... def __init__(self, file):
+ ... self.file = file
+ ...
+ ... def sizeForSorting(self):
+ ... return 'byte', len(self.file.data)
+ ...
+ ... def sizeForDisplay(self):
+ ... return '%i bytes' %len(self.file.data)
+
+ >>> zope.component.provideAdapter(FileSized)
+
+We also need a container to which we can add files:
+
+ >>> class Container(Folder.Folder):
+ ... def __setitem__(self, name, value):
+ ... self._setObject(name, value)
+ ... value.__name__ = name
+
+Here is some sample data:
+
+ >>> container = Container()
+ >>> obj_id = self.folder._setObject('container', container)
+ >>> container = self.folder[obj_id]
+ >>> container['mypage.html'] = File('<html><body>Hello World!</body></html>')
+ >>> container['data.xml'] = File('<message>Hello World!</message>')
+ >>> container['test.txt'] = File('Hello World!')
+
+
+The View
+~~~~~~~~
+
+The contents view of the container should iterate through the container and
+represent the files in a table:
+
+ >>> contentsTemplate = os.path.join(temp_dir, 'contents.pt')
+ >>> open(contentsTemplate, 'w').write('''
+ ... <html>
+ ... <body>
+ ... <h1>Cotnents</h1>
+ ... <div tal:content="structure provider:contents" />
+ ... </body>
+ ... </html>
+ ... ''')
+
+ >>> from Products.Five.browser.metaconfigure import makeClassForTemplate
+ >>> Contents = makeClassForTemplate(contentsTemplate, name='contents.html')
+
+
+The Viewlet Manager
+~~~~~~~~~~~~~~~~~~~
+
+Now we have to write our own viewlet manager. In this case we cannot use the
+default implementation, since the viewlets will be looked up for each
+different item:
+
+ >>> shownColumns = []
+
+ >>> class ContentsViewletManager(Explicit):
+ ... zope.interface.implements(interfaces.IViewletManager)
+ ... index = None
+ ...
+ ... def __init__(self, context, request, view):
+ ... self.context = context
+ ... self.request = request
+ ... self.__parent__ = view
+ ...
+ ... def update(self):
+ ... rows = []
+ ... for name, value in self.context.objectItems():
+ ... rows.append(
+ ... [zope.component.getMultiAdapter(
+ ... (value, self.request, self.__parent__, self),
+ ... interfaces.IViewlet, name=colname)
+ ... for colname in shownColumns])
+ ... [entry.update() for entry in rows[-1]]
+ ... self.rows = rows
+ ...
+ ... def render(self, *args, **kw):
+ ... return self.index(*args, **kw)
+
+Now we need a template to produce the contents table:
+
+ >>> tableTemplate = os.path.join(temp_dir, 'table.pt')
+ >>> open(tableTemplate, 'w').write('''
+ ... <table>
+ ... <tr tal:repeat="row view/rows">
+ ... <td tal:repeat="column row">
+ ... <tal:block replace="structure column/render" />
+ ... </td>
+ ... </tr>
+ ... </table>
+ ... ''')
+
+From the two pieces above, we can generate the final viewlet manager class and
+register it (it's a bit tedious, I know):
+
+ >>> from Products.Five.browser.pagetemplatefile import ZopeTwoPageTemplateFile
+ >>> ContentsViewletManager = type(
+ ... 'ContentsViewletManager', (ContentsViewletManager,),
+ ... {'index': ZopeTwoPageTemplateFile('table.pt', temp_dir)})
+
+ >>> zope.component.provideAdapter(
+ ... ContentsViewletManager,
+ ... (Container, IDefaultBrowserLayer, zope.interface.Interface),
+ ... interfaces.IViewletManager, name='contents')
+
+Since we have not defined any viewlets yet, the table is totally empty:
+
+ >>> contents = Contents(container, request)
+ >>> print contents().strip()
+ <html>
+ <body>
+ <h1>Cotnents</h1>
+ <div>
+ <table>
+ <tr>
+ </tr>
+ <tr>
+ </tr>
+ <tr>
+ </tr>
+ </table>
+ </div>
+ </body>
+ </html>
+
+
+The Viewlets and the Final Result
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now let's create a first viewlet for the manager...
+
+ >>> class NameViewlet(Explicit):
+ ...
+ ... def __init__(self, context, request, view, manager):
+ ... self.__parent__ = view
+ ... self.context = context
+ ...
+ ... def update(self):
+ ... pass
+ ...
+ ... def render(self):
+ ... return self.context.__name__
+
+and register it:
+
+ >>> zope.component.provideAdapter(
+ ... NameViewlet,
+ ... (IFile, IDefaultBrowserLayer,
+ ... zope.interface.Interface, interfaces.IViewletManager),
+ ... interfaces.IViewlet, name='name')
+
+Note how you register the viewlet on ``IFile`` and not on the container. Now
+we should be able to see the name for each file in the container:
+
+ >>> print contents().strip()
+ <html>
+ <body>
+ <h1>Cotnents</h1>
+ <div>
+ <table>
+ <tr>
+ </tr>
+ <tr>
+ </tr>
+ <tr>
+ </tr>
+ </table>
+ </div>
+ </body>
+ </html>
+
+Waaa, nothing there! What happened? Well, we have to tell our user preferences
+that we want to see the name as a column in the table:
+
+ >>> shownColumns = ['name']
+ >>> print contents().strip()
+ <html>
+ <body>
+ <h1>Cotnents</h1>
+ <div>
+ <table>
+ <tr>
+ <td>
+ mypage.html
+ </td>
+ </tr>
+ <tr>
+ <td>
+ data.xml
+ </td>
+ </tr>
+ <tr>
+ <td>
+ test.txt
+ </td>
+ </tr>
+ </table>
+ </div>
+ </body>
+ </html>
+
+Let's now write a second viewlet that will display the size of the object for
+us:
+
+ >>> class SizeViewlet(Explicit):
+ ...
+ ... def __init__(self, context, request, view, manager):
+ ... self.__parent__ = view
+ ... self.context = context
+ ...
+ ... def update(self):
+ ... pass
+ ...
+ ... def render(self):
+ ... return size.interfaces.ISized(self.context).sizeForDisplay()
+
+ >>> zope.component.provideAdapter(
+ ... SizeViewlet,
+ ... (IFile, IDefaultBrowserLayer,
+ ... zope.interface.Interface, interfaces.IViewletManager),
+ ... interfaces.IViewlet, name='size')
+
+After we added it to the list of shown columns,
+
+ >>> shownColumns = ['name', 'size']
+
+we can see an entry for it:
+
+ >>> print contents().strip()
+ <html>
+ <body>
+ <h1>Cotnents</h1>
+ <div>
+ <table>
+ <tr>
+ <td>
+ mypage.html
+ </td>
+ <td>
+ 38 bytes
+ </td>
+ </tr>
+ <tr>
+ <td>
+ data.xml
+ </td>
+ <td>
+ 31 bytes
+ </td>
+ </tr>
+ <tr>
+ <td>
+ test.txt
+ </td>
+ <td>
+ 12 bytes
+ </td>
+ </tr>
+ </table>
+ </div>
+ </body>
+ </html>
+
+If we switch the two columns around,
+
+ >>> shownColumns = ['size', 'name']
+
+the result will be
+
+ >>> print contents().strip()
+ <html>
+ <body>
+ <h1>Cotnents</h1>
+ <div>
+ <table>
+ <tr>
+ <td>
+ 38 bytes
+ </td>
+ <td>
+ mypage.html
+ </td>
+ </tr>
+ <tr>
+ <td>
+ 31 bytes
+ </td>
+ <td>
+ data.xml
+ </td>
+ </tr>
+ <tr>
+ <td>
+ 12 bytes
+ </td>
+ <td>
+ test.txt
+ </td>
+ </tr>
+ </table>
+ </div>
+ </body>
+ </html>
+
+
+Supporting Sorting
+~~~~~~~~~~~~~~~~~~
+
+Oftentimes you also want to batch and sort the entries in a table. Since those
+two features are not part of the view logic, they should be treated with
+independent components. In this example, we are going to only implement
+sorting using a simple utility:
+
+ >>> class ISorter(zope.interface.Interface):
+ ...
+ ... def sort(values):
+ ... """Sort the values."""
+
+ >>> class SortByName(object):
+ ... zope.interface.implements(ISorter)
+ ...
+ ... def sort(self, values):
+ ... return sorted(values, lambda x, y: cmp(x.__name__, y.__name__))
+
+ >>> zope.component.provideUtility(SortByName(), name='name')
+
+ >>> class SortBySize(object):
+ ... zope.interface.implements(ISorter)
+ ...
+ ... def sort(self, values):
+ ... return sorted(
+ ... values,
+ ... lambda x, y: cmp(size.interfaces.ISized(x).sizeForSorting(),
+ ... size.interfaces.ISized(y).sizeForSorting()))
+
+ >>> zope.component.provideUtility(SortBySize(), name='size')
+
+Note that we decided to give the sorter utilities the same name as the
+corresponding viewlet. This convention will make our implementation of the
+viewlet manager much simpler:
+
+ >>> sortByColumn = ''
+
+ >>> class SortedContentsViewletManager(manager.ViewletManagerBase):
+ ... zope.interface.implements(interfaces.IViewletManager)
+ ... index = None
+ ...
+ ... def __init__(self, context, request, view):
+ ... self.context = context
+ ... self.request = request
+ ... self.__parent__ = view
+ ...
+ ... def update(self):
+ ... values = self.context.objectValues()
+ ...
+ ... if sortByColumn:
+ ... sorter = zope.component.queryUtility(ISorter, sortByColumn)
+ ... if sorter:
+ ... values = sorter.sort(values)
+ ...
+ ... rows = []
+ ... for value in values:
+ ... rows.append(
+ ... [zope.component.getMultiAdapter(
+ ... (value, self.request, self.__parent__, self),
+ ... interfaces.IViewlet, name=colname)
+ ... for colname in shownColumns])
+ ... [entry.update() for entry in rows[-1]]
+ ... self.rows = rows
+ ...
+ ... def render(self, *args, **kw):
+ ... return self.index(*args, **kw)
+
+As you can see, the concern of sorting is cleanly separated from generating
+the view code. In MVC terms that means that the controller (sort) is logically
+separated from the view (viewlets). Let's now do the registration dance for
+the new viewlet manager. We simply override the existing registration:
+
+ >>> SortedContentsViewletManager = type(
+ ... 'SortedContentsViewletManager', (SortedContentsViewletManager,),
+ ... {'index': ZopeTwoPageTemplateFile('table.pt', temp_dir)})
+
+ >>> zope.component.provideAdapter(
+ ... SortedContentsViewletManager,
+ ... (Container, IDefaultBrowserLayer, zope.interface.Interface),
+ ... interfaces.IViewletManager, name='contents')
+
+Finally we sort the contents by name:
+
+ >>> shownColumns = ['name', 'size']
+ >>> sortByColumn = 'name'
+
+ >>> print contents().strip()
+ <html>
+ <body>
+ <h1>Cotnents</h1>
+ <div>
+ <table>
+ <tr>
+ <td>
+ data.xml
+ </td>
+ <td>
+ 31 bytes
+ </td>
+ </tr>
+ <tr>
+ <td>
+ mypage.html
+ </td>
+ <td>
+ 38 bytes
+ </td>
+ </tr>
+ <tr>
+ <td>
+ test.txt
+ </td>
+ <td>
+ 12 bytes
+ </td>
+ </tr>
+ </table>
+ </div>
+ </body>
+ </html>
+
+Now let's sort by size:
+
+ >>> sortByColumn = 'size'
+
+ >>> print contents().strip()
+ <html>
+ <body>
+ <h1>Cotnents</h1>
+ <div>
+ <table>
+ <tr>
+ <td>
+ test.txt
+ </td>
+ <td>
+ 12 bytes
+ </td>
+ </tr>
+ <tr>
+ <td>
+ data.xml
+ </td>
+ <td>
+ 31 bytes
+ </td>
+ </tr>
+ <tr>
+ <td>
+ mypage.html
+ </td>
+ <td>
+ 38 bytes
+ </td>
+ </tr>
+ </table>
+ </div>
+ </body>
+ </html>
+
+That's it! As you can see, in a few steps we have built a pretty flexible
+contents view with selectable columns and sorting. However, there is a lot of
+room for extending this example:
+
+- Table Header: The table header cell for each column should be a different
+ type of viewlet, but registered under the same name. The column header
+ viewlet also adapts the container not the item. The header column should
+ also be able to control the sorting.
+
+- Batching: A simple implementation of batching should work very similar to
+ the sorting feature. Of course, efficient implementations should somehow
+ combine batching and sorting more effectively.
+
+- Sorting in ascending and descending order: Currently, you can only sort from
+ the smallest to the highest value; however, this limitation is almost
+ superficial and can easily be removed by making the sorters a bit more
+ flexible.
+
+- Further Columns: For a real application, you would want to implement other
+ columns, of course. You would also probably want some sort of fallback for
+ the case that a viewlet is not found for a particular container item and
+ column.
+
+
+Cleanup
+-------
+
+ >>> ignore = SecurityManager.setSecurityPolicy(oldPolicy)
+ >>> import shutil
+ >>> shutil.rmtree(temp_dir)
Added: Products.Five/branches/1.4/viewlet/__init__.py
===================================================================
--- Products.Five/branches/1.4/viewlet/__init__.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/__init__.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1 @@
+# A package for viewlet support
\ No newline at end of file
Added: Products.Five/branches/1.4/viewlet/configure.zcml
===================================================================
--- Products.Five/branches/1.4/viewlet/configure.zcml 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/configure.zcml 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,12 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser">
+
+ <interface
+ interface="zope.viewlet.interfaces.IViewletManager"
+ />
+
+ <interface
+ interface="zope.viewlet.interfaces.IViewletManager"
+ />
+
+</configure>
\ No newline at end of file
Added: Products.Five/branches/1.4/viewlet/css_viewlet.pt
===================================================================
--- Products.Five/branches/1.4/viewlet/css_viewlet.pt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/css_viewlet.pt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,4 @@
+<link type="text/css" rel="stylesheet" href="somestyle.css" media="all"
+ tal:attributes="rel view/getRel;
+ href view/getURL;
+ media view/getMedia" />
Added: Products.Five/branches/1.4/viewlet/directives.txt
===================================================================
--- Products.Five/branches/1.4/viewlet/directives.txt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/directives.txt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,502 @@
+================================
+The ``viewletManager`` Directive
+================================
+
+Setup traversal stuff
+
+ >>> import Products.Five
+ >>> from Products.Five import zcml
+ >>> zcml.load_config("configure.zcml", Products.Five)
+
+The ``viewletManager`` directive allows you to quickly register a new viewlet
+manager without worrying about the details of the ``adapter``
+directive. Before we can use the directives, we have to register their
+handlers by executing the package's meta configuration:
+
+ >>> from Products.Five import zcml
+ >>> context = zcml.load_string('''
+ ... <configure i18n_domain="zope">
+ ... <include package="Products.Five.viewlet" file="meta.zcml" />
+ ... </configure>
+ ... ''')
+
+Now we can register a viewlet manager:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewletManager
+ ... name="defaultmanager"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''')
+
+Let's make sure the directive has really issued a sensible adapter
+registration; to do that, we create some dummy content, request and view
+objects:
+
+ >>> from Products.Five.viewlet.tests import Content
+ >>> content = Content()
+ >>> obj_id = self.folder._setObject('content1', Content())
+ >>> content = self.folder[obj_id]
+
+ >>> from Products.Five.traversable import FakeRequest
+ >>> request = FakeRequest()
+ >>> from zope.app.publication.browser import setDefaultSkin
+ >>> setDefaultSkin(request)
+
+ >>> from Products.Five.browser import BrowserView as View
+ >>> view = View(content, request)
+
+Now let's lookup the manager. This particular registration is pretty boring:
+
+ >>> import zope.component
+ >>> from zope.viewlet import interfaces
+ >>> manager = zope.component.getMultiAdapter(
+ ... (content, request, view),
+ ... interfaces.IViewletManager, name='defaultmanager')
+
+ >>> manager
+ <Products.Five.viewlet.manager.<ViewletManager providing IViewletManager> object ...>
+ >>> interfaces.IViewletManager.providedBy(manager)
+ True
+ >>> manager.template is None
+ True
+ >>> manager.update()
+ >>> manager.render()
+ u''
+
+However, this registration is not very useful, since we did specify a specific
+viewlet manager interface, a specific content interface, specific view or
+specific layer. This means that all viewlets registered will be found.
+
+The first step to effectively using the viewlet manager directive is to define
+a special viewlet manager interface:
+
+ >>> from Products.Five.viewlet.tests import ILeftColumn
+
+Now we can register register a manager providing this interface:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewletManager
+ ... name="leftcolumn"
+ ... permission="zope.Public"
+ ... provides="Products.Five.viewlet.tests.ILeftColumn"
+ ... />
+ ... </configure>
+ ... ''')
+
+ >>> manager = zope.component.getMultiAdapter(
+ ... (content, request, view), ILeftColumn, name='leftcolumn')
+
+ >>> manager
+ <Products.Five.viewlet.manager.<ViewletManager providing ILeftColumn> object ...>
+ >>> ILeftColumn.providedBy(manager)
+ True
+ >>> manager.template is None
+ True
+ >>> manager.update()
+ >>> manager.render()
+ u''
+
+Next let's see what happens, if we specify a template for the viewlet manager:
+
+ >>> import os, tempfile
+ >>> temp_dir = tempfile.mkdtemp()
+
+ >>> leftColumnTemplate = os.path.join(temp_dir, 'leftcolumn.pt')
+ >>> open(leftColumnTemplate, 'w').write('''
+ ... <div class="column">
+ ... <div class="entry"
+ ... tal:repeat="viewlet options/viewlets"
+ ... tal:content="structure viewlet" />
+ ... </div>
+ ... ''')
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewletManager
+ ... name="leftcolumn"
+ ... permission="zope.Public"
+ ... provides="Products.Five.viewlet.tests.ILeftColumn"
+ ... template="%s"
+ ... />
+ ... </configure>
+ ... ''' %leftColumnTemplate)
+
+ >>> manager = zope.component.getMultiAdapter(
+ ... (content, request, view), ILeftColumn, name='leftcolumn')
+
+ >>> manager
+ <Products.Five.viewlet.manager.<ViewletManager providing ILeftColumn> object ...>
+ >>> ILeftColumn.providedBy(manager)
+ True
+ >>> manager.template.meta_type
+ 'Page Template (File)'
+ >>> manager.update()
+ >>> print manager.render().strip()
+ <div class="column">
+ </div>
+
+Additionally you can specify a class that will serve as a base to the default
+viewlet manager or be a viewlet manager in its own right. In our case we will
+provide a custom implementation of the ``sort()`` method, which will sort by a
+weight attribute in the viewlet:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewletManager
+ ... name="leftcolumn"
+ ... permission="zope.Public"
+ ... provides="Products.Five.viewlet.tests.ILeftColumn"
+ ... template="%s"
+ ... class="Products.Five.viewlet.tests.WeightBasedSorting"
+ ... />
+ ... </configure>
+ ... ''' %leftColumnTemplate)
+
+ >>> manager = zope.component.getMultiAdapter(
+ ... (content, request, view), ILeftColumn, name='leftcolumn')
+
+ >>> manager
+ <Products.Five.viewlet.manager.<ViewletManager providing ILeftColumn> object ...>
+ >>> manager.__class__.__bases__
+ (<class 'Products.Five.viewlet.tests.WeightBasedSorting'>,
+ <class 'Products.Five.viewlet.manager.ViewletManagerBase'>)
+ >>> ILeftColumn.providedBy(manager)
+ True
+ >>> manager.template.meta_type
+ 'Page Template (File)'
+ >>> manager.update()
+ >>> print manager.render().strip()
+ <div class="column">
+ </div>
+
+Finally, if a non-existent template is specified, an error is raised:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewletManager
+ ... name="leftcolumn"
+ ... permission="zope.Public"
+ ... template="foo.pt"
+ ... />
+ ... </configure>
+ ... ''')
+ Traceback (most recent call last):
+ ...
+ ZopeXMLConfigurationError: File "<string>", line 3.2-7.8
+ ConfigurationError: ('No such file', '...foo.pt')
+
+
+=========================
+The ``viewlet`` Directive
+=========================
+
+Now that we have a viewlet manager, we have to register some viewlets for
+it. The ``viewlet`` directive is similar to the ``viewletManager`` directive,
+except that the viewlet is also registered for a particular manager interface,
+as seen below:
+
+ >>> weatherTemplate = os.path.join(temp_dir, 'weather.pt')
+ >>> open(weatherTemplate, 'w').write('''
+ ... <div>sunny</div>
+ ... ''')
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="weather"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... template="%s"
+ ... permission="zope.Public"
+ ... extra_string_attributes="can be specified"
+ ... />
+ ... </configure>
+ ... ''' % weatherTemplate)
+
+If we look into the adapter registry, we will find the viewlet:
+
+ >>> viewlet = zope.component.getMultiAdapter(
+ ... (content, request, view, manager), interfaces.IViewlet,
+ ... name='weather')
+ >>> viewlet.render().strip()
+ '<div>sunny</div>'
+ >>> viewlet.extra_string_attributes
+ u'can be specified'
+
+The manager now also gives us the output of the one and only viewlet:
+
+ >>> manager.update()
+ >>> print manager.render().strip()
+ <div class="column">
+ <div class="entry">
+ <div>sunny</div>
+ </div>
+ </div>
+
+Let's now ensure that we can also specify a viewlet class:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="weather2"
+ ... for="*"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... template="%s"
+ ... class="Products.Five.viewlet.tests.Weather"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''' % weatherTemplate)
+
+ >>> viewlet = zope.component.getMultiAdapter(
+ ... (content, request, view, manager), interfaces.IViewlet,
+ ... name='weather2')
+ >>> viewlet().strip()
+ '<div>sunny</div>'
+
+Okay, so the template-driven cases work. But just specifying a class should
+also work:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="sport"
+ ... for="*"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... class="Products.Five.viewlet.tests.Sport"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''')
+
+ >>> viewlet = zope.component.getMultiAdapter(
+ ... (content, request, view, manager), interfaces.IViewlet, name='sport')
+ >>> viewlet()
+ u'Red Sox vs. White Sox'
+
+It should also be possible to specify an alternative attribute of the class to
+be rendered upon calling the viewlet:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="stock"
+ ... for="*"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... class="Products.Five.viewlet.tests.Stock"
+ ... attribute="getStockTicker"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''')
+
+ >>> viewlet = zope.component.getMultiAdapter(
+ ... (content, request, view, manager), interfaces.IViewlet,
+ ... name='stock')
+ >>> viewlet.render()
+ u'SRC $5.19'
+
+A final feature the ``viewlet`` directive supports is the additional
+specification of any amount keyword arguments:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="stock2"
+ ... permission="zope.Public"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... class="Products.Five.viewlet.tests.Stock"
+ ... weight="8"
+ ... />
+ ... </configure>
+ ... ''')
+
+ >>> viewlet = zope.component.getMultiAdapter(
+ ... (content, request, view, manager), interfaces.IViewlet,
+ ... name='stock2')
+ >>> viewlet.weight
+ u'8'
+
+
+Error Scenarios
+---------------
+
+Neither the class or template have been specified:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="testviewlet"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''')
+ Traceback (most recent call last):
+ ...
+ ZopeXMLConfigurationError: File "<string>", line 3.2-7.8
+ ConfigurationError: Must specify a class or template
+
+The specified attribute is not ``__call__``, but also a template has been
+specified:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="testviewlet"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... template="test_viewlet.pt"
+ ... attribute="faux"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''')
+ Traceback (most recent call last):
+ ...
+ ZopeXMLConfigurationError: File "<string>", line 3.2-9.8
+ ConfigurationError: Attribute and template cannot be used together.
+
+Now, we are not specifying a template, but a class that does not have the
+specified attribute:
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="testviewlet"
+ ... manager="Products.Five.viewlet.tests.ILeftColumn"
+ ... class="Products.Five.viewlet.tests.Sport"
+ ... attribute="faux"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''')
+ Traceback (most recent call last):
+ ...
+ ZopeXMLConfigurationError: File "<string>", line 3.2-9.8
+ ConfigurationError: The provided class doesn't have the specified attribute
+
+================================
+Viewlet Directive Security
+================================
+
+Before we can begin, we need to set up a few things. We need a
+manager account:
+
+ >>> uf = self.folder.acl_users
+ >>> uf._doAddUser('manager', 'r00t', ['Manager'], [])
+
+Finally, we need to setup a traversable folder. Otherwise, Five won't
+get do its view lookup magic:
+
+ >>> from OFS.Folder import manage_addFolder
+ >>> manage_addFolder(self.folder, 'ftf')
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/five" i18n_domain="zope">
+ ... <traversable class="OFS.Folder.Folder"
+ ... />
+ ... </configure>
+ ... ''')
+
+Now we can register another simple viewlet manager:
+
+ >>> from Products.Five.viewlet.tests import INewColumn
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewletManager
+ ... name="newcolumn"
+ ... permission="zope.Public"
+ ... provides="Products.Five.viewlet.tests.INewColumn"
+ ... />
+ ... </configure>
+ ... ''')
+
+And a view to call our new content provider:
+
+ >>> testTemplate = os.path.join(temp_dir, 'test.pt')
+ >>> open(testTemplate, 'w').write('''
+ ... <html>
+ ... <body>
+ ... <h1>Weather</h1>
+ ... <div tal:content="structure provider:newcolumn" />
+ ... </body>
+ ... </html>
+ ... ''')
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <page
+ ... for="*"
+ ... name="securitytest_view"
+ ... template="%s"
+ ... permission="zope.Public"
+ ... />
+ ... </configure>
+ ... ''' % testTemplate)
+
+
+We now register some viewlets with different permissions:
+
+ >>> weatherTemplate = os.path.join(temp_dir, 'weather2.pt')
+ >>> open(weatherTemplate, 'w').write('''
+ ... <div>sunny</div>
+ ... ''')
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="weather3"
+ ... manager="Products.Five.viewlet.tests.INewColumn"
+ ... template="%s"
+ ... permission="zope.Public"
+ ... extra_string_attributes="can be specified"
+ ... />
+ ... </configure>
+ ... ''' % weatherTemplate)
+
+ >>> context = zcml.load_string('''
+ ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
+ ... <viewlet
+ ... name="weather4"
+ ... manager="Products.Five.viewlet.tests.INewColumn"
+ ... template="%s"
+ ... permission="zope2.ViewManagementScreens"
+ ... />
+ ... </configure>
+ ... ''' % weatherTemplate)
+
+If we make the request as a manager, we should see both viewlets:
+
+ >>> print http(r"""
+ ... GET /test_folder_1_/ftf/@@securitytest_view HTTP/1.1
+ ... Authorization: Basic manager:r00t
+ ... """, handle_errors=False)
+ HTTP/1.1 200 OK
+ ...
+ <h1>Weather</h1>
+ <div>
+ <div>sunny</div>
+ <div>sunny</div>
+ </div>
+ ...
+
+But when we make an anonymous request, we will only see the public viewlet:
+
+ >>> print http(r"""
+ ... GET /test_folder_1_/ftf/@@securitytest_view HTTP/1.1
+ ... """, handle_errors=False)
+ HTTP/1.1 200 OK
+ ...
+ <h1>Weather</h1>
+ <div>
+ <div>sunny</div>
+ </div>
+ ...
+
+Cleanup
+-------
+
+ >>> import shutil
+ >>> shutil.rmtree(temp_dir)
Added: Products.Five/branches/1.4/viewlet/javascript_viewlet.pt
===================================================================
--- Products.Five/branches/1.4/viewlet/javascript_viewlet.pt 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/javascript_viewlet.pt 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,3 @@
+<script type="text/javascript" src="some-library.js"
+ tal:attributes="src view/getURL">
+</script>
Added: Products.Five/branches/1.4/viewlet/manager.py
===================================================================
--- Products.Five/branches/1.4/viewlet/manager.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/manager.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,65 @@
+import Acquisition
+from AccessControl.ZopeGuards import guarded_hasattr
+import zope.interface
+import zope.security
+from zope.viewlet import interfaces
+from zope.viewlet.manager import ViewletManagerBase as origManagerBase
+
+from Products.Five.browser.pagetemplatefile import ZopeTwoPageTemplateFile
+
+
+class ViewletManagerBase(origManagerBase, Acquisition.Explicit):
+ """A base class for Viewlet managers to work in Zope2"""
+
+ def __getitem__(self, name):
+ """See zope.interface.common.mapping.IReadMapping"""
+ # Find the viewlet
+ viewlet = zope.component.queryMultiAdapter(
+ (self.context, self.request, self.__parent__, self),
+ interfaces.IViewlet, name=name)
+
+ # If the viewlet was not found, then raise a lookup error
+ if viewlet is None:
+ raise zope.component.interfaces.ComponentLookupError(
+ 'No provider with name `%s` found.' %name)
+
+ # If the viewlet cannot be accessed, then raise an
+ # unauthorized error
+ if not guarded_hasattr(viewlet.__of__(viewlet.context), 'render'):
+ raise zope.security.interfaces.Unauthorized(
+ 'You are not authorized to access the provider '
+ 'called `%s`.' %name)
+
+ # Return the viewlet.
+ return viewlet
+
+ def filter(self, viewlets):
+ """Sort out all content providers
+
+ ``viewlets`` is a list of tuples of the form (name, viewlet).
+ """
+ # Only return viewlets accessible to the principal
+ # We need to wrap each viewlet in its context to make sure that
+ # the object has a real context from which to determine owner
+ # security.
+ return [(name, viewlet) for name, viewlet in viewlets if
+ guarded_hasattr(viewlet.__of__(viewlet.context), 'render')]
+
+def ViewletManager(name, interface, template=None, bases=()):
+
+ if template is not None:
+ template = ZopeTwoPageTemplateFile(template)
+
+ if ViewletManagerBase not in bases:
+ # Make sure that we do not get a default viewlet manager mixin, if the
+ # provided base is already a full viewlet manager implementation.
+ if not (len(bases) == 1 and
+ interfaces.IViewletManager.implementedBy(bases[0])):
+ bases = bases + (ViewletManagerBase,)
+
+ ViewletManager = type(
+ '<ViewletManager providing %s>' % interface.getName(),
+ bases,
+ {'template': template, '__name__': name})
+ zope.interface.classImplements(ViewletManager, interface)
+ return ViewletManager
Added: Products.Five/branches/1.4/viewlet/meta.zcml
===================================================================
--- Products.Five/branches/1.4/viewlet/meta.zcml 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/meta.zcml 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,21 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:meta="http://namespaces.zope.org/meta">
+
+ <meta:directives namespace="http://namespaces.zope.org/browser">
+
+ <meta:directive
+ name="viewlet"
+ schema="zope.viewlet.metadirectives.IViewletDirective"
+ handler=".metaconfigure.viewletDirective"
+ />
+
+ <meta:directive
+ name="viewletManager"
+ schema="zope.viewlet.metadirectives.IViewletManagerDirective"
+ handler=".metaconfigure.viewletManagerDirective"
+ />
+
+ </meta:directives>
+
+</configure>
\ No newline at end of file
Added: Products.Five/branches/1.4/viewlet/metaconfigure.py
===================================================================
--- Products.Five/branches/1.4/viewlet/metaconfigure.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/metaconfigure.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,177 @@
+import os
+from zope.configuration.exceptions import ConfigurationError
+from zope.viewlet import interfaces
+from zope.interface import Interface
+from zope.interface import classImplements
+from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+from zope.app.component import metaconfigure
+from zope.app.publisher.browser import viewmeta
+from zope.app.publisher.interfaces.browser import IBrowserView
+
+from Products.Five.security import getSecurityInfo, protectClass, protectName
+from Products.Five.viewlet import viewlet
+from Products.Five.viewlet import manager
+
+
+from Globals import InitializeClass as initializeClass
+
+def viewletManagerDirective(
+ _context, name, permission,
+ for_=Interface, layer=IDefaultBrowserLayer, view=IBrowserView,
+ provides=interfaces.IViewletManager, class_=None, template=None,
+ allowed_interface=None, allowed_attributes=None):
+
+ # If class is not given we use the basic viewlet manager.
+ if class_ is None:
+ class_ = manager.ViewletManagerBase
+
+ # Iterate over permissions
+ if allowed_attributes is None:
+ allowed_attributes = ['render', 'update']
+ if allowed_interface is not None:
+ for interface in allowed_interface:
+ allowed_attributes.extend(interface.names())
+
+ # Make sure that the template exists and that all low-level API methods
+ # have the right permission.
+ if template:
+ template = os.path.abspath(str(_context.path(template)))
+ if not os.path.isfile(template):
+ raise ConfigurationError("No such file", template)
+ allowed_attributes.append('__getitem__')
+
+ # Create a new class based on the template and class.
+ new_class = manager.ViewletManager(
+ name, provides, template=template, bases=(class_, ))
+ else:
+ # Create a new class based on the class.
+ new_class = manager.ViewletManager(name, provides, bases=(class_, ))
+
+ # Register interfaces
+ viewmeta._handle_for(_context, for_)
+ metaconfigure.interface(_context, view)
+
+ # register a viewlet manager
+ _context.action(
+ discriminator = ('viewletManager', for_, layer, view, name),
+ callable = metaconfigure.handler,
+ args = ('provideAdapter',
+ (for_, layer, view), provides, name,
+ new_class, _context.info),)
+ _context.action(
+ discriminator = ('five:protectClass', new_class),
+ callable = protectClass,
+ args = (new_class, permission)
+ )
+ if allowed_attributes:
+ for attr in allowed_attributes:
+ _context.action(
+ discriminator = ('five:protectName', new_class, attr),
+ callable = protectName,
+ args = (new_class, attr, permission)
+ )
+ _context.action(
+ discriminator = ('five:initialize:class', new_class),
+ callable = initializeClass,
+ args = (new_class,)
+ )
+
+
+def viewletDirective(
+ _context, name, permission,
+ for_=Interface, layer=IDefaultBrowserLayer, view=IBrowserView,
+ manager=interfaces.IViewletManager, class_=None, template=None,
+ attribute='render', allowed_interface=None, allowed_attributes=None,
+ **kwargs):
+
+ # Either the class or template must be specified.
+ if not (class_ or template):
+ raise ConfigurationError("Must specify a class or template")
+
+ # Make sure that all the non-default attribute specifications are correct.
+ if attribute != 'render':
+ if template:
+ raise ConfigurationError(
+ "Attribute and template cannot be used together.")
+
+ # Note: The previous logic forbids this condition to evere occur.
+ if not class_:
+ raise ConfigurationError(
+ "A class must be provided if attribute is used")
+
+ # Iterate over permissions
+ if allowed_attributes is None:
+ allowed_attributes = ['render', 'update']
+ if allowed_interface is not None:
+ for interface in allowed_interface:
+ allowed_attributes.extend(interface.names())
+
+ # Make sure that the template exists and that all low-level API methods
+ # have the right permission.
+ if template:
+ template = os.path.abspath(str(_context.path(template)))
+ if not os.path.isfile(template):
+ raise ConfigurationError("No such file", template)
+ allowed_attributes.append('__getitem__')
+
+ # Make sure the has the right form, if specified.
+ if class_:
+ if attribute != 'render':
+ if not hasattr(class_, attribute):
+ raise ConfigurationError(
+ "The provided class doesn't have the specified attribute "
+ )
+ if template:
+ # Create a new class for the viewlet template and class.
+ new_class = viewlet.SimpleViewletClass(
+ template, bases=(class_, ), attributes=kwargs)
+ else:
+ if not hasattr(class_, 'browserDefault'):
+ cdict = {'browserDefault':
+ lambda self, request: (getattr(self, attribute), ())}
+ else:
+ cdict = {}
+
+ cdict['__name__'] = name
+ cdict['__page_attribute__'] = attribute
+ cdict.update(kwargs)
+ new_class = type(class_.__name__,
+ (class_, viewlet.SimpleAttributeViewlet), cdict)
+
+ if hasattr(class_, '__implements__'):
+ classImplements(new_class, IBrowserPublisher)
+
+ else:
+ # Create a new class for the viewlet template alone.
+ new_class = viewlet.SimpleViewletClass(template, name=name,
+ attributes=kwargs)
+
+ # Register the interfaces.
+ viewmeta._handle_for(_context, for_)
+ metaconfigure.interface(_context, view)
+
+ # register viewlet
+ _context.action(
+ discriminator = ('viewlet', for_, layer, view, manager, name),
+ callable = metaconfigure.handler,
+ args = ('provideAdapter',
+ (for_, layer, view, manager), interfaces.IViewlet,
+ name, new_class, _context.info),)
+
+ _context.action(
+ discriminator = ('five:protectClass', new_class),
+ callable = protectClass,
+ args = (new_class, permission)
+ )
+ if allowed_attributes:
+ for attr in allowed_attributes:
+ _context.action(
+ discriminator = ('five:protectName', new_class, attr),
+ callable = protectName,
+ args = (new_class, attr, permission)
+ )
+ _context.action(
+ discriminator = ('five:initialize:class', new_class),
+ callable = initializeClass,
+ args = (new_class,)
+ )
\ No newline at end of file
Added: Products.Five/branches/1.4/viewlet/tests.py
===================================================================
--- Products.Five/branches/1.4/viewlet/tests.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/tests.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,91 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""Viewlet tests
+
+$Id: tests.py 39461 2005-10-15 10:45:13Z srichter $
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+from Testing.ZopeTestCase import FunctionalDocFileSuite
+from zope.app.testing import setup
+from zope.interface import Interface
+from zope.interface import implements
+from zope.viewlet import interfaces
+from OFS.SimpleItem import SimpleItem
+
+class Content(SimpleItem):
+ implements(Interface)
+
+class UnitTestSecurityPolicy:
+ """
+ Stub out the existing security policy for unit testing purposes.
+ """
+ #
+ # Standard SecurityPolicy interface
+ #
+ def validate( self
+ , accessed=None
+ , container=None
+ , name=None
+ , value=None
+ , context=None
+ , roles=None
+ , *args
+ , **kw):
+ return 1
+
+ def checkPermission( self, permission, object, context) :
+ return 1
+
+class ILeftColumn(interfaces.IViewletManager):
+ """Left column of my page."""
+
+class INewColumn(interfaces.IViewletManager):
+ """Left column of my page."""
+
+class WeightBasedSorting(object):
+ def sort(self, viewlets):
+ return sorted(viewlets,
+ lambda x, y: cmp(x[1].weight, y[1].weight))
+
+class Weather(object):
+ weight = 0
+
+class Stock(object):
+ weight = 0
+ def getStockTicker(self):
+ return u'SRC $5.19'
+
+class Sport(object):
+ weight = 0
+ def __call__(self):
+ return u'Red Sox vs. White Sox'
+
+def setUp(test):
+ setup.placefulSetUp()
+
+def tearDown(test):
+ setup.placefulTearDown()
+
+def test_suite():
+ return unittest.TestSuite((
+ FunctionalDocFileSuite('README.txt'),
+ FunctionalDocFileSuite('directives.txt',
+ setUp=setUp, tearDown=tearDown
+ ),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Added: Products.Five/branches/1.4/viewlet/viewlet.py
===================================================================
--- Products.Five/branches/1.4/viewlet/viewlet.py 2006-05-02 17:40:45 UTC (rev 67843)
+++ Products.Five/branches/1.4/viewlet/viewlet.py 2006-05-02 17:56:17 UTC (rev 67844)
@@ -0,0 +1,68 @@
+import os, sys
+from Acquisition import Explicit
+from zope.viewlet import interfaces
+from zope.viewlet import viewlet as orig_viewlet
+
+from Products.Five.browser.pagetemplatefile import ZopeTwoPageTemplateFile
+
+# We add Acquisition to all the base classes to enable security machinery
+class ViewletBase(orig_viewlet.ViewletBase, Explicit):
+ pass
+
+class SimpleAttributeViewlet(orig_viewlet.SimpleAttributeViewlet, Explicit):
+ pass
+
+class simple(orig_viewlet.simple):
+ # We need to ensure that the proper __init__ is called.
+ __init__ = ViewletBase.__init__.im_func
+
+def SimpleViewletClass(template, bases=(), attributes=None,
+ name=u''):
+ """A function that can be used to generate a viewlet from a set of
+ information.
+ """
+
+ # Create the base class hierarchy
+ bases += (simple, ViewletBase)
+
+ attrs = {'index' : ZopeTwoPageTemplateFile(template),
+ '__name__' : name}
+ if attributes:
+ attrs.update(attributes)
+
+ # Generate a derived view class.
+ class_ = type("SimpleViewletClass from %s" % template, bases, attrs)
+
+ return class_
+
+
+class ResourceViewletBase(orig_viewlet.ResourceViewletBase, Explicit):
+ pass
+
+def JavaScriptViewlet(path):
+ """Create a viewlet that can simply insert a javascript link."""
+ src = os.path.join(os.path.dirname(__file__), 'javascript_viewlet.pt')
+
+ klass = type('JavaScriptViewlet',
+ (ResourceViewletBase, ViewletBase),
+ {'index': ZopeTwoPageTemplateFile(src),
+ '_path': path})
+
+ return klass
+
+
+class CSSResourceViewletBase(orig_viewlet.CSSResourceViewletBase):
+ pass
+
+def CSSViewlet(path, media="all", rel="stylesheet"):
+ """Create a viewlet that can simply insert a javascript link."""
+ src = os.path.join(os.path.dirname(__file__), 'css_viewlet.pt')
+
+ klass = type('CSSViewlet',
+ (CSSResourceViewletBase, ViewletBase),
+ {'index': ZopeTwoPageTemplateFile(src),
+ '_path': path,
+ '_media':media,
+ '_rel':rel})
+
+ return klass
More information about the Zope-Checkins
mailing list