=================
Content Providers
=================

We want to mix fields and content providers.

This allow to enrich the form by interlacing html snippets produced by content
providers.

For instance, we might want to render the table of results in a search form.

We might also need to render HTML close to a widget as a handle used when
improving UI with Ajax.

Adding HTML outside the widgets avoids the systematic need of
subclassing or changing the full widget rendering.


Test setup
----------

  >>> from pyramid.testing import setUp, tearDown
  >>> config = setUp(hook_zca=True)

  >>> from cornice import includeme as include_cornice
  >>> include_cornice(config)
  >>> from pyams_utils import includeme as include_utils
  >>> include_utils(config)
  >>> from pyams_site import includeme as include_site
  >>> include_site(config)
  >>> from pyams_i18n import includeme as include_i18n
  >>> include_i18n(config)
  >>> from pyams_form import includeme as include_form
  >>> include_form(config)

  >>> from pyams_utils.testing import format_html
  >>> from pyams_form import util, testing
  >>> testing.setup_form_defaults(config.registry)

We define a simple test schema with fields:

  >>> import zope.interface
  >>> import zope.schema

  >>> class IPerson(zope.interface.Interface):
  ...
  ...     id = zope.schema.TextLine(
  ...         title='ID',
  ...         description="The person's ID.",
  ...         required=True)
  ...
  ...     fullname = zope.schema.TextLine(
  ...         title='FullName',
  ...         description="The person's name.",
  ...         required=True)
  ...

A class that implements the schema:

  >>> @zope.interface.implementer(IPerson)
  ... class Person(object):
  ...    id = 'james'
  ...    fullname = 'James Bond'

The usual request instance:

  >>> request = testing.TestRequest()

We want to insert a content provider in between fields.
We define a test content provider that renders extra help text:

  >>> from zope.contentprovider.interfaces import IContentProvider
  >>> class ExtendedHelp:
  ...   def __init__(self, context, request, view):
  ...       super().__init__()
  ...       self.context = context
  ...       self.request = request
  ...       self.__parent__ = view
  ...
  ...   def update(self):
  ...       self.person = self.context.id
  ...
  ...   def render(self):
  ...       return '<div class="ex-help">Help about person %s</div>' % self.person


Form definition
---------------

The meat of the tests begins here.

We define a form as usual by inheriting from ``form.Form``::

  >>> from pyams_form import field, form
  >>> from zope.interface import implementer

To enable content providers, the form class must:

  1. implement ``IFieldsAndContentProvidersForm``
  2. have a ``content_providers`` attribute that is
     an instance of the ``ContentProviders`` class.

  >>> from pyams_form.interfaces.form import IFieldsAndContentProvidersForm
  >>> from pyams_form.contentprovider import ContentProviders


Content provider assignment
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Content providers classes (factories) can be assigned directly to the
``ContentProviders`` container:

  >>> @implementer(IFieldsAndContentProvidersForm)
  ... class PersonForm(form.Form):
  ...     fields = field.Fields(IPerson)
  ...     ignore_context = True
  ...     content_providers = ContentProviders()
  ...     content_providers['longHelp'] = ExtendedHelp
  ...     content_providers['longHelp'].position = 1

Let's instantiate content and form instances:

  >>> person = Person()
  >>> personForm = PersonForm(person, request)

Once the widget manager has been updated, it holds the content provider:

  >>> from pyams_form.contentprovider import FieldWidgetsAndProviders
  >>> manager = FieldWidgetsAndProviders(personForm, request, person)
  >>> manager.ignore_context = True
  >>> manager.update()
  >>> widgets = manager
  >>> ids = sorted(widgets.keys())
  >>> ids
  ['fullname', 'id', 'longHelp']
  >>> widgets['longHelp']
  <...ExtendedHelp object at ...>
  >>> widgets['id']
  <TextWidget 'form.widgets.id'>
  >>> widgets['fullname']
  <TextWidget 'form.widgets.fullname'>
  >>> manager.get('longHelp').render()
  '<div class="ex-help">Help about person james</div>'

Content provider lookup
~~~~~~~~~~~~~~~~~~~~~~~

Forms can also refer by name to content providers.

Let's register a content provider by name as usual:

  >>> from zope.contentprovider.interfaces import IContentProvider
  >>> from pyams_layer.interfaces import IFormLayer
  >>> config.registry.registerAdapter(ExtendedHelp,
  ...                (zope.interface.Interface, IFormLayer, zope.interface.Interface),
  ...                IContentProvider, name='longHelp')

Let the form refer to it:

  >>> @implementer(IFieldsAndContentProvidersForm)
  ... class LookupPersonForm(form.Form):
  ...     prefix = 'form.'
  ...     fields = field.Fields(IPerson)
  ...     ignore_context = True
  ...     content_providers = ContentProviders(['longHelp'])
  ...     content_providers['longHelp'].position = 2

  >>> lookupForm = LookupPersonForm(person, request)

After update, the widget manager refers to the content provider:

  >>> from pyams_form.contentprovider import FieldWidgetsAndProviders
  >>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
  >>> manager.ignore_context = True
  >>> manager.update()
  >>> widgets = manager
  >>> ids = sorted(widgets.keys())
  >>> ids
  ['fullname', 'id', 'longHelp']
  >>> widgets['longHelp']
  <...ExtendedHelp object at ...>
  >>> widgets['id']
  <TextWidget 'form.widgets.id'>
  >>> widgets['fullname']
  <TextWidget 'form.widgets.fullname'>
  >>> manager.get('longHelp').render()
  '<div class="ex-help">Help about person james</div>'

Providers position
~~~~~~~~~~~~~~~~~~

Until here, we have defined position for content providers without explaining
how it is used.

A position needs to be defined for each provider. Let's forget to define a
position:

  >>> @implementer(IFieldsAndContentProvidersForm)
  ... class UndefinedPositionForm(form.Form):
  ...     prefix = 'form.'
  ...     fields = field.Fields(IPerson)
  ...     ignore_context = True
  ...     content_providers = ContentProviders(['longHelp'])

  >>> form = UndefinedPositionForm(person, request)
  >>> manager = FieldWidgetsAndProviders(form, request, person)
  >>> manager.ignore_context = True

When updating the widget manager, we get an exception:

  >>> manager.update()
  Traceback (most recent call last):
  ...
  ValueError: Position of the following content provider should be an integer: 'longHelp'.

Let's check positioning of content providers:

  >>> LookupPersonForm.content_providers['longHelp'].position = 0
  >>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
  >>> manager.ignore_context = True
  >>> manager.update()
  >>> list(manager.values())
  [<...ExtendedHelp object at ...>, <TextWidget 'form.widgets.id'>, <TextWidget 'form.widgets.fullname'>]

  >>> LookupPersonForm.content_providers['longHelp'].position = 1
  >>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
  >>> manager.ignore_context = True
  >>> manager.update()
  >>> list(manager.values())
  [<TextWidget 'form.widgets.id'>, <...ExtendedHelp object at ...>, <TextWidget 'form.widgets.fullname'>]

  >>> LookupPersonForm.content_providers['longHelp'].position = 2
  >>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
  >>> manager.ignore_context = True
  >>> manager.update()
  >>> list(manager.values())
  [<TextWidget 'form.widgets.id'>, <TextWidget 'form.widgets.fullname'>, <...ExtendedHelp object at ...>]

Using value larger than sequence length implies end of sequence::

  >>> LookupPersonForm.content_providers['longHelp'].position = 3
  >>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
  >>> manager.ignore_context = True
  >>> manager.update()
  >>> list(manager.values())
  [<TextWidget 'form.widgets.id'>, <TextWidget 'form.widgets.fullname'>, <...ExtendedHelp object at ...>]

A negative value is interpreted same as ``insert`` method of Python lists::

  >>> LookupPersonForm.content_providers['longHelp'].position = -1
  >>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
  >>> manager.ignore_context = True
  >>> manager.update()
  >>> list(manager.values())
  [<TextWidget 'form.widgets.id'>, <...ExtendedHelp object at ...>, <TextWidget 'form.widgets.fullname'>]


Rendering the form
------------------

Once the form has been updated, it can be rendered.

Since we have not assigned a template yet, we have to do it now.
We have a small template as part of this example:

  >>> import os
  >>> from pyams_template.interfaces import IContentTemplate
  >>> from pyams_template.template import TemplateFactory
  >>> from pyams_layer.interfaces import IFormLayer
  >>> from pyams_form import tests
  >>> factory = TemplateFactory(os.path.join(os.path.dirname(tests.__file__),
  ...                           'templates', 'simple-edit-with-providers.pt'), 'text/html')
  >>> config.registry.registerAdapter(factory, (None, IFormLayer, PersonForm), IContentTemplate)

To enable form updating, all widget adapters must be registered:

  >>> from pyams_form.testing import setup_form_defaults
  >>> setup_form_defaults(config.registry)

``FieldWidgetsAndProviders`` is registered as widget manager for
``IFieldsAndContentProvidersForm``:

  >>> personForm.update()
  >>> personForm.widgets
  FieldWidgetsAndProviders([...])

Let's render the form:

  >>> print(format_html(personForm.render()))
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
  <body>
     <form action=".">
          <div class="row">
              <label for="form-widgets-id">ID</label>
              <input type="text"
           id="form-widgets-id"
           name="form.widgets.id"
           class="text-widget required textline-field"
           title="The person's ID."
           value="" />
          </div>
          <div class="row">
            <div class="ex-help">Help about person james</div>
          </div>
          <div class="row">
              <label for="form-widgets-fullname">FullName</label>
              <input type="text"
           id="form-widgets-fullname"
           name="form.widgets.fullname"
           class="text-widget required textline-field"
           title="The person's name."
           value="" />
        </div>
     </form>
  </body>
  </html>


Tests cleanup:

  >>> tearDown()
