================================
The ``viewletManager`` Directive
================================

Setup traversal stuff

  >>> import Products.Five
  >>> from Zope2.App 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:

  >>> 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 zope.publisher.browser import TestRequest
  >>> request = TestRequest()

  >>> 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() == ''
  True

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() == ''
  True

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 fd:
  ...     _ = fd.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.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.update()
  >>> print(manager.render().strip())
  <div class="column">
  </div>

Finally, if a non-existent template is specified, an error is raised:

  >>> from zope.configuration.exceptions import ConfigurationError
  >>> zcml_string = ('''
  ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
  ...   <viewletManager
  ...       name="leftcolumn"
  ...       permission="zope.Public"
  ...       template="foo.pt"
  ...       />
  ... </configure>
  ... ''')
  >>> try:
  ...     context = zcml.load_string(zcml_string)
  ... except ConfigurationError as exc:
  ...     msg = str(exc)
  ...     print('No such file' in msg)
  ...     print('foo.pt' in msg)
  ... else:
  ...     print('ConfigurationError not raised.')
  True
  True


=========================
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 fd:
  ...     _ = fd.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>'
  True
  >>> viewlet.extra_string_attributes == 'can be specified'
  True

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>'
  True

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() == 'Red Sox vs. White Sox'
  True

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() == 'SRC $5.19'
  True

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 == '8'
  True


Error Scenarios
---------------

Neither the class or template have been specified:

  >>> zcml_string = ('''
  ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
  ...   <viewlet
  ...       name="testviewlet"
  ...       manager="Products.Five.viewlet.tests.ILeftColumn"
  ...       permission="zope.Public"
  ...       />
  ... </configure>
  ... ''')

  >>> try:
  ...     zcml.load_string(zcml_string)
  ... except ConfigurationError as exc:
  ...     print('Must specify a class or template' in str(exc))
  ... else:
  ...     print('ConfigurationError not raised.')
  True

The specified attribute is not ``__call__``, but also a template has been
specified:

  >>> zcml_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>
  ... ''')

  >>> try:
  ...     zcml.load_string(zcml_string)
  ... except ConfigurationError as exc:
  ...     print('Attribute and template cannot be used together.' in str(exc))
  ... else:
  ...     print('ConfigurationError not raised.')
  True

Now, we are not specifying a template, but a class that does not have the
specified attribute:

  >>> zcml_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>
  ... ''')

  >>> try:
  ...     zcml.load_string(zcml_string)
  ... except ConfigurationError as exc:
  ...     print("The provided class doesn't have "
  ...           "the specified attribute." in str(exc))
  ... else:
  ...     print('ConfigurationError not raised.')
  True


================================
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
  >>> _ignored = 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')

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="zope2.View"
  ...       provides="Products.Five.viewlet.tests.INewColumn"
  ...       />
  ... </configure>
  ... ''')

And a view to call our new content provider:

  >>> testTemplate = os.path.join(temp_dir, 'test.pt')
  >>> with open(testTemplate, 'w') as fd:
  ...     _ = fd.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')
  >>> with open(weatherTemplate, 'w') as fd:
  ...     _ = fd.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"
  ...       />
  ... </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>
  ...

A Dynamic Viewlet
-----------------

A viewlet template can of course contain some dynamic code, let's see how
that works:

  >>> dynWeatherTemplate = os.path.join(temp_dir, 'dynamic_weather.pt')
  >>> with open(dynWeatherTemplate, 'w') as fd:
  ...     _ = fd.write(u'''
  ... <div tal:define="city view/city;"><span tal:replace="string:${city/name}: ${city/temp} F" /></div>'''
  ... )

  >>> context = zcml.load_string('''
  ... <configure xmlns="http://namespaces.zope.org/browser" i18n_domain="zope">
  ...   <viewlet
  ...       name="dynweather"
  ...       for="*"
  ...       manager="Products.Five.viewlet.tests.INewColumn"
  ...       class="Products.Five.viewlet.tests.DynamicTempBox"
  ...       template="%s"
  ...       permission="zope2.View"
  ...       />
  ... </configure>
  ... ''' % dynWeatherTemplate)

Now we request the view to ensure that we can see the dynamic template
rendered as expected:

  >>> 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>Los Angeles, CA: 78 F</div>
       <div>sunny</div>
       </div>
  ...

Cleanup
-------

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

Clear registries:

  >>> from zope.testing.cleanup import cleanUp
  >>> cleanUp()
