Viewlet-related ZCML Directives¶
The viewletManager
Directive¶
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 zope.configuration import xmlconfig
>>> context = xmlconfig.string('''
... <configure i18n_domain="zope">
... <include package="zope.viewlet" file="meta.zcml" />
... </configure>
... ''')
Now we can register a viewlet manager:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewletManager
... name="defaultmanager"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
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:
>>> import zope.interface
>>> @zope.interface.implementer(zope.interface.Interface)
... class Content(object):
... pass
>>> content = Content()
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> from zope.publisher.browser import BrowserView
>>> view = BrowserView(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
<zope.viewlet.manager.<ViewletManager providing IViewletManager> object ...>
>>> interfaces.IViewletManager.providedBy(manager)
True
>>> manager.template is None
True
>>> manager.update()
>>> manager.render()
''
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:
>>> class ILeftColumn(interfaces.IViewletManager):
... """Left column of my page."""
Now we can register register a manager providing this interface:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewletManager
... name="leftcolumn"
... permission="zope.Public"
... provides="zope.viewlet.directives.ILeftColumn"
... />
... </configure>
... ''', context=context)
>>> manager = zope.component.getMultiAdapter(
... (content, request, view), ILeftColumn, name='leftcolumn')
>>> manager
<zope.viewlet.manager.<ViewletManager providing ILeftColumn> object ...>
>>> ILeftColumn.providedBy(manager)
True
>>> manager.template is None
True
>>> manager.update()
>>> manager.render()
''
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')
>>> with open(leftColumnTemplate, 'w') as file:
... _ = file.write('''
... <div class="column">
... <div class="entry"
... tal:repeat="viewlet options/viewlets"
... tal:content="structure viewlet" />
... </div>
... ''')
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewletManager
... name="leftcolumn"
... permission="zope.Public"
... provides="zope.viewlet.directives.ILeftColumn"
... template="%s"
... />
... </configure>
... ''' %leftColumnTemplate, context=context)
>>> manager = zope.component.getMultiAdapter(
... (content, request, view), ILeftColumn, name='leftcolumn')
>>> manager
<zope.viewlet.manager.<ViewletManager providing ILeftColumn> object ...>
>>> ILeftColumn.providedBy(manager)
True
>>> manager.template
<BoundPageTemplateFile of ...<ViewletManager providing ILeftColumn> ...>>
>>> 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:
>>> class WeightBasedSorting(object):
... def sort(self, viewlets):
... return sorted(viewlets, key=lambda x: getattr(x[1], 'weight', 0))
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewletManager
... name="leftcolumn"
... permission="zope.Public"
... provides="zope.viewlet.directives.ILeftColumn"
... template="%s"
... class="zope.viewlet.directives.WeightBasedSorting"
... />
... </configure>
... ''' %leftColumnTemplate, context=context)
>>> manager = zope.component.getMultiAdapter(
... (content, request, view), ILeftColumn, name='leftcolumn')
>>> manager
<zope.viewlet.manager.<ViewletManager providing ILeftColumn> object ...>
>>> manager.__class__.__bases__
(<class 'zope.viewlet.directives.WeightBasedSorting'>,
<class 'zope.viewlet.manager.ViewletManagerBase'>)
>>> ILeftColumn.providedBy(manager)
True
>>> manager.template
<BoundPageTemplateFile of ...<ViewletManager providing ILeftColumn> ...>>
>>> manager.update()
>>> print(manager.render().strip())
<div class="column">
</div>
Finally, if a non-existent template is specified, an error is raised:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewletManager
... name="leftcolumn"
... permission="zope.Public"
... template="foo.pt"
... />
... </configure>
... ''', context=context)
Traceback (most recent call last):
...
ConfigurationError: ('No such file', '...foo.pt')
File "<string>", line 3.2-7.8
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')
>>> with open(weatherTemplate, 'w') as file:
... _ = file.write('''
... <div>sunny</div>
... ''')
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="weather"
... manager="zope.viewlet.directives.ILeftColumn"
... template="%s"
... permission="zope.Public"
... extra_string_attributes="can be specified"
... />
... </configure>
... ''' % weatherTemplate, context=context)
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
'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:
>>> class Weather(object):
... weight = 0
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="weather2"
... for="*"
... manager="zope.viewlet.directives.ILeftColumn"
... template="%s"
... class="zope.viewlet.directives.Weather"
... permission="zope.Public"
... />
... </configure>
... ''' % weatherTemplate, context=context)
>>> 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:
>>> class Sport(object):
... weight = 0
... def __call__(self):
... return 'Red Sox vs. White Sox'
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="sport"
... for="*"
... manager="zope.viewlet.directives.ILeftColumn"
... class="zope.viewlet.directives.Sport"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
>>> viewlet = zope.component.getMultiAdapter(
... (content, request, view, manager), interfaces.IViewlet, name='sport')
>>> viewlet()
'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:
>>> class Stock(object):
... weight = 0
... def getStockTicker(self):
... return 'SRC $5.19'
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="stock"
... for="*"
... manager="zope.viewlet.directives.ILeftColumn"
... class="zope.viewlet.directives.Stock"
... attribute="getStockTicker"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
>>> viewlet = zope.component.getMultiAdapter(
... (content, request, view, manager), interfaces.IViewlet,
... name='stock')
>>> viewlet.render()
'SRC $5.19'
If the class mentions that it implements any interfaces using the
old-fashioned style, the resulting viewlet will
implement IBrowserPublisher
:
>>> from zope.publisher.interfaces.browser import IBrowserPublisher
>>> from zope.interface import classImplements
>>> Stock.__implements__ = ()
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="stock"
... for="*"
... manager="zope.viewlet.directives.ILeftColumn"
... class="zope.viewlet.directives.Stock"
... attribute="getStockTicker"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
>>> viewlet = zope.component.getMultiAdapter(
... (content, request, view, manager), interfaces.IViewlet,
... name='stock')
>>> IBrowserPublisher.providedBy(viewlet)
True
A final feature the viewlet
directive is that it supports the
specification of any number of keyword arguments:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="stock2"
... permission="zope.Public"
... class="zope.viewlet.directives.Stock"
... weight="8"
... />
... </configure>
... ''', context=context)
>>> viewlet = zope.component.getMultiAdapter(
... (content, request, view, manager), interfaces.IViewlet,
... name='stock2')
>>> viewlet.weight
'8'
Error Scenarios¶
Neither the class or template have been specified:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="testviewlet"
... manager="zope.viewlet.directives.ILeftColumn"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
Traceback (most recent call last):
...
ConfigurationError: Must specify a class or template
File "<string>", line 3.2-7.8
The specified attribute is not __call__
, but also a template has been
specified:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="testviewlet"
... manager="zope.viewlet.directives.ILeftColumn"
... template="test_viewlet.pt"
... attribute="faux"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
Traceback (most recent call last):
...
ConfigurationError: Attribute and template cannot be used together.
File "<string>", line 3.2-9.8
Now, we are not specifying a template, but a class that does not have the specified attribute:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="testviewlet"
... manager="zope.viewlet.directives.ILeftColumn"
... class="zope.viewlet.directives.Sport"
... attribute="faux"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
Traceback (most recent call last):
...
ConfigurationError: The provided class doesn't have the specified attribute
File "<string>", line 3.2-9.8
Now for a template that doesn’t exist:
>>> context = xmlconfig.string('''
... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
... <viewlet
... name="weather2"
... for="*"
... manager="zope.viewlet.directives.ILeftColumn"
... template="this template is not here"
... class="zope.viewlet.directives.Weather"
... permission="zope.Public"
... />
... </configure>
... ''', context=context)
Traceback (most recent call last):
...
ConfigurationError: ('No such file', '...this template is not here')
File "<string>", line 3.2-10.8
Cleanup¶
>>> import shutil
>>> shutil.rmtree(temp_dir)