3. 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.

3.1. Test setup

Before we can use a widget manager, the IFieldWidget adapter has to be registered for the ITextLine field:

>>> import zope.component
>>> import zope.interface
>>> from z3c.form import interfaces, widget
>>> from z3c.form.browser import text
>>> from z3c.form.testing import TestRequest

>>> @zope.component.adapter(zope.schema.TextLine, TestRequest)
... @zope.interface.implementer(interfaces.IFieldWidget)
... def TextFieldWidget(field, request):
...     return widget.FieldWidget(field, text.TextWidget(request))

>>> zope.component.provideAdapter(TextFieldWidget)

>>> from z3c.form import converter
>>> zope.component.provideAdapter(converter.FieldDataConverter)
>>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)

We define a simple test schema with fields:

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

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

A class that implements the schema:

>>> class Person(object):
...    id = 'james'
...    fullname = 'James Bond'

The usual request instance:

>>> request = TestRequest()

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

>>> from zope.publisher.browser import BrowserView
>>> from zope.contentprovider.interfaces import IContentProvider
>>> class ExtendedHelp(BrowserView):
...   def __init__(self, context, request, view):
...       super(ExtendedHelp, self).__init__(context, 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

3.2. Form definition

The meat of the tests begins here.

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

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

To enable content providers, the form class must :

  1. implement IFieldsAndContentProvidersForm
  2. have a contentProviders attribute that is an instance of the ContentProviders class.
>>> from z3c.form.interfaces import IFieldsAndContentProvidersForm
>>> from z3c.form.contentprovider import ContentProviders

3.2.1. 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)
...     ignoreContext = True
...     contentProviders = ContentProviders()
...     contentProviders['longHelp'] = ExtendedHelp
...     contentProviders['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 z3c.form.contentprovider import FieldWidgetsAndProviders
>>> manager = FieldWidgetsAndProviders(personForm, request, person)
>>> manager.ignoreContext = 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>'

3.2.2. Content provider lookup

Forms can also refer by name to content providers.

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

>>> from zope.component import provideAdapter
>>> from zope.contentprovider.interfaces import IContentProvider
>>> from z3c.form.interfaces import IFormLayer
>>> provideAdapter(ExtendedHelp,
...                (zope.interface.Interface,
...                 IFormLayer,
...                 zope.interface.Interface),
...                provides=IContentProvider, name='longHelp')

Let the form refer to it:

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

>>> lookupForm = LookupPersonForm(person, request)

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

>>> from z3c.form.contentprovider import FieldWidgetsAndProviders
>>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
>>> manager.ignoreContext = 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>'

3.2.3. 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)
...     ignoreContext = True
...     contentProviders = ContentProviders(['longHelp'])

>>> form = UndefinedPositionForm(person, request)
>>> manager = FieldWidgetsAndProviders(form, request, person)
>>> manager.ignoreContext = 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.contentProviders['longHelp'].position = 0
>>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
>>> manager.ignoreContext = True
>>> manager.update()
>>> list(manager.values())
[<ExtendedHelp object at ...>, <TextWidget 'form.widgets.id'>, <TextWidget 'form.widgets.fullname'>]

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

>>> LookupPersonForm.contentProviders['longHelp'].position = 2
>>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
>>> manager.ignoreContext = 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.contentProviders['longHelp'].position = 3
>>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
>>> manager.ignoreContext = 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.contentProviders['longHelp'].position = -1
>>> manager = FieldWidgetsAndProviders(lookupForm, request, person)
>>> manager.ignoreContext = True
>>> manager.update()
>>> list(manager.values())
[<TextWidget 'form.widgets.id'>, <ExtendedHelp object at ...>, <TextWidget 'form.widgets.fullname'>]

3.3. 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 zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
>>> from zope.browserpage.viewpagetemplatefile import BoundPageTemplate
>>> from z3c.form import tests
>>> def personTemplate(form):
...     form.template = BoundPageTemplate(
...         ViewPageTemplateFile(
...             'simple_edit_with_providers.pt',
...             os.path.dirname(tests.__file__)), form)
>>> personTemplate(personForm)

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

>>> from z3c.form.testing import setupFormDefaults
>>> setupFormDefaults()

FieldWidgetsAndProviders is registered as widget manager for IFieldsAndContentProvidersForm:

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

Let’s render the form:

>>> print(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 id="form-widgets-id" name="form.widgets.id"
               class="text-widget required textline-field"
               value="" type="text" />
      </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 id="form-widgets-fullname"
               name="form.widgets.fullname"
               class="text-widget required textline-field"
               value="" type="text" />
      </div>
    </form>
  </body>
</html>