================================
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()
  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:

  >>> 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()
  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')
  >>> 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):
  ...
  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')
  >>> 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()
  u'<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:

  >>> 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()
  u'<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 u'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()
  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:

  >>> class Stock(object):
  ...     weight = 0
  ...     def getStockTicker(self):
  ...         return u'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()
  u'SRC $5.19'

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
  u'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):
  ...
  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 = 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):
  ...
  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 = 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):
  ...
  ZopeXMLConfigurationError: File "<string>", line 3.2-9.8
    ConfigurationError: The provided class doesn't have the specified attribute


Cleanup
-------

  >>> import shutil
  >>> shutil.rmtree(temp_dir)
