z3c.form 1.9.0
An advanced form and widget framework for Zope 3
This package provides an implementation for HTML forms and widgets. The goal
is to provide a simple API but with the ability to easily customize any data or
steps.
Detailed Documentation
**********************
=================
Forms and Widgets
=================
This package provides an implementation for HTML forms and widgets. The goal
is to provide a simple API but with the ability to easily customize any data or
steps. This document, provides the content of this package's documentation
files. The documents are ordered in the way they should be read:
- ``form.txt`` [must read]
Describes the setup and usage of forms in the most common usages. Some
details are provided to the structure of form components.
- ``group.txt`` [must read]
This document describes how widget groups are implemented within this
package and how they can be used.
- ``subform.txt`` [must read]
Introduces the complexities surrounding sub-forms and details two classes of
sub-forms, including code examples.
- ``field.txt`` [must read]
Provides a comprehensive explanation of the field manager API and how it is
to be used.
- ``button.txt`` [must read]
Provides a comprehensive explanation of the button manager API. It also
outlines how to create buttons within schemas and how buttons are converted
to actions.
- ``zcml.txt`` [must read]
Explains the ZCML directives defines by this package, which are designed to
make it easier to register new templates without writing Python code.
- ``validator.txt`` [advanced users]
Validators are used to validate converted form data. This document provides
a comprehensive overview of the API and how to use it effectively.
- ``widget.txt`` [advanced users]
Explains in detail the design goals surrounding widgets and widget managers
and how they were realized with the implemented API.
- ``action.txt`` [advanced users]
Explains in detail the design goals surrounding action managers and
actions. The execution of actions using action handlers is also covered. The
document demonstrates how actions can be created without the use of buttons.
- ``value.txt`` [informative]
The concept of attribute value adapters is introduced and fully
explained. Some motivation for this new and powerful pattern is given as
well.
- ``datamanager.txt`` [informative]
Data managers are resposnsible for accessing and writing the data. While
attribute access is the most common case, data managers can also manage
other data structures, such as dictionaries.
- ``converter.txt`` [informative]
Data converters convert data between internal and widget values and vice
versa.
- ``term.txt`` [informative]
Terms are wrappers around sources and vocabularies to provide a common
interface for choices in this package.
- ``util.txt`` [informative]
The ``util`` module provides several helper functions and classes. The
components not tested otherwise are explained in this file.
- ``adding.txt`` [informative]
This module provides a base class for add forms that work with the
``IAdding`` interface.
Browser Documentation
---------------------
There are several documentation files in the ``browser/`` sub-package. They
mainly document the basic widgets provided by the package.
- ``README.txt`` [advanced users]
This file contains a checklist, ensuring that all fields have a widget.
- ``<fieldname>.txt``
Each field name documentation file comprehensively explains the widget and
how it is ensured to work properly.
=====
Forms
=====
The purpose of this package is to make development of forms as simple
as possible, while still providing all the hooks to do customization
at any level as required by our real-world use cases. Thus, once the
system is set up with all its default registrations, it should be
trivial to develop a new form.
The strategy of this document is to provide the most common, and thus
simplest, case first and then demonstrate the available customization
options. In order to not overwhelm you with our set of well-chosen defaults,
all the default component registrations have been made prior to doing those
examples:
>>> from z3c.form import testing
>>> testing.setupFormDefaults()
Before we can start writing forms, we must have the content to work with:
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
...
... id = zope.schema.TextLine(
... title=u'ID',
... readonly=True,
... required=True)
...
... name = zope.schema.TextLine(
... title=u'Name',
... required=True)
...
... gender = zope.schema.Choice(
... title=u'Gender',
... values=('male', 'female'),
... required=False)
...
... age = zope.schema.Int(
... title=u'Age',
... description=u"The person's age.",
... min=0,
... default=20,
... required=False)
...
... @zope.interface.invariant
... def ensureIdAndNameNotEqual(person):
... if person.id == person.name:
... raise zope.interface.Invalid(
... "The id and name cannot be the same.")
>>> from zope.schema.fieldproperty import FieldProperty
>>> class Person(object):
... zope.interface.implements(IPerson)
... id = FieldProperty(IPerson['id'])
... name = FieldProperty(IPerson['name'])
... gender = FieldProperty(IPerson['gender'])
... age = FieldProperty(IPerson['age'])
...
... def __init__(self, id, name, gender=None, age=None):
... self.id = id
... self.name = name
... if gender:
... self.gender = gender
... if age:
... self.age = age
...
... def __repr__(self):
... return '<%s %r>' % (self.__class__.__name__, self.name)
Okay, that should suffice for now.
What's next? Well, first things first. Let's create an add form for the
person. Since practice showed that the ``IAdding`` interface is overkill for
most projects, the default add form of ``z3c.form`` requires you to define the
creation and adding mechanism.
**Note**:
If it is not done, ``NotImplementedError[s]`` are raised:
>>> from z3c.form.testing import TestRequest
>>> from z3c.form import form, field
>>> abstract = form.AddForm(None, TestRequest())
>>> abstract.create({})
Traceback (most recent call last):
...
NotImplementedError
>>> abstract.add(1)
Traceback (most recent call last):
...
NotImplementedError
>>> abstract.nextURL()
Traceback (most recent call last):
...
NotImplementedError
Thus let's now create a working add form:
>>> class PersonAddForm(form.AddForm):
...
... fields = field.Fields(IPerson)
...
... def create(self, data):
... return Person(**data)
...
... def add(self, object):
... self.context[object.id] = object
...
... def nextURL(self):
... return 'index.html'
This is as simple as it gets. We explicitly define the pieces that
are custom to every situation and let the default setup of the
framework do the rest. This is intentionally similar to
``zope.formlib``, because we really like the simplicity of
``zope.formlib``'s way of dealing with the common use cases.
Let's try to add a new person object to the root folder (which
was created during test setup). For this add form, of course, the
context is now the root folder:
>>> request = TestRequest()
>>> addForm = PersonAddForm(root, request)
Since forms are not necessarily pages -- in fact often they are not --
they must not have a ``__call__`` method that does all the processing
and rendering at once. Instead, we use the update/render
pattern. Thus, we first call the ``update()`` method.
>>> addForm.update()
Actually a lot of things happen during this stage. Let us step through it one
by one pointing out the effects.
Find a widget manager and update it
-----------------------------------
The default widget manager knows to look for the ``fields`` attribute in the
form, since it implements ``IFieldsForm``:
>>> from z3c.form import interfaces
>>> interfaces.IFieldsForm.providedBy(addForm)
True
The widget manager is then stored in the ``widgets`` attribute as promised by
the ``IForm`` interface:
>>> addForm.widgets
<z3c.form.field.FieldWidgets object at ...>
The widget manager will have four widgets, one for each field:
>>> addForm.widgets.keys()
['id', 'name', 'gender', 'age']
When the widget manager updates itself, several sub-tasks are processed. The
manager goes through each field, trying to create a fully representative
widget for the field.
Field Availability
~~~~~~~~~~~~~~~~~~
Just because a field is requested in the field manager, does not mean that a
widget has to be created for the field. There are cases when a field
declaration might be ignored. The following reasons come to mind:
* No widget is created if the data is not accessible in the content.
* A custom widget manager has been registered to specifically ignore a field.
In our simple example, all fields will be converted to widgets.
Widget Creation
~~~~~~~~~~~~~~~
During the widget creation process, several pieces of information are
transferred from the field to the widget:
>>> age = addForm.widgets['age']
# field.title -> age.label
>>> age.label
u'Age'
# field.required -> age.required
>>> age.required
False
All these values can be overridden at later stages of the updating
process.
Widget Value
~~~~~~~~~~~~
The next step is to determine the value that should be displayed by the
widget. This value could come from three places (looked up in this order):
1. The field's default value.
2. The content object that the form is representing.
3. The request in case a form has not been submitted or an error occurred.
Since we are currently building an add form and not an edit form,
there is no content object to represent, so the second step is not
applicable. The third step is also not applicable as we do not have
anything in the request. Therefore, the value should be the field's
default value, or be empty. In this case the field provides a default
value:
>>> age.value
u'20'
While the default of the age field is actually the integer ``20``, the
widget has converted the value to the output-ready string ``'20'``
using a data converter.
Widget Mode
~~~~~~~~~~~
Now the widget manager looks at the field to determine the widget mode -- in
other words whether the widget is a display or edit widget. In this case all
fields are input fields:
>>> age.mode
'input'
Deciding which mode to use, however, might not be a trivial operation. It
might depend on several factors (items listed later override earlier ones):
* The global ``mode`` flag of the widget manager
* The permission to the content's data value
* The ``readonly`` flag in the schema field
* The ``mode`` flag in the field
Widget Attribute Values
~~~~~~~~~~~~~~~~~~~~~~~
As mentioned before, several widget attributes are optionally overridden when
the widget updates itself:
* label
* required
* mode
Since we have no customization components registered, all of those fields will
remain as set before.
Find an action manager, update and execute it
---------------------------------------------
After all widgets have been instantiated and the ``update()`` method has been
called successfully, the actions are set up. By default, the form machinery
uses the button declaration on the form to create its actions. For the add
form, an add button is defined by default, so that we did not need to create
our own. Thus, there should be one action:
>>> len(addForm.actions)
1
The add button is an action and a widget at the same time:
>>> addAction = addForm.actions['add']
>>> addAction.title
u'Add'
>>> addAction.value
u'Add'
After everything is set up, all pressed buttons are executed. Once a submitted
action is detected, a special action handler adapter is used to determine the
actions to take. Since the add button has not been pressed yet, no action
occurred.
Rendering the form
------------------
Once the update is complete we can render the form. Since we have not
specified a template yet, we have to do this now. We have prepared a small and
very simple template as part of this example:
>>> import os
>>> from zope.app.pagetemplate import viewpagetemplatefile
>>> from z3c.form import tests
>>> def addTemplate(form):
... form.template = viewpagetemplatefile.BoundPageTemplate(
... viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_edit.pt', os.path.dirname(tests.__file__)), form)
>>> addTemplate(addForm)
Let's now render the page:
>>> print addForm.render()
<html>
<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"
value="" />
</div>
<div class="row">
<label for="form-widgets-name">Name</label>
<input type="text" id="form-widgets-name" name="form.widgets.name"
class="text-widget required textline-field"
value="" />
</div>
<div class="row">
<label for="form-widgets-gender">Gender</label>
<select id="form-widgets-gender" name="form.widgets.gender:list"
class="select-widget choice-field" size="1">
<option id="form-widgets-gender-novalue"
value="--NOVALUE--">no value</option>
<option id="form-widgets-gender-0" value="male">male</option>
<option id="form-widgets-gender-1" value="female">female</option>
</select>
<input name="form.widgets.gender-empty-marker" type="hidden"
value="1" />
</div>
<div class="row">
<label for="form-widgets-age">Age</label>
<input type="text" id="form-widgets-age" name="form.widgets.age"
class="text-widget int-field" value="20" />
</div>
<div class="action">
<input type="submit" id="form-buttons-add" name="form.buttons.add"
class="submit-widget button-field" value="Add" />
</div>
</form>
</body>
</html>
Submitting an add form successfully
-----------------------------------
Initially the root folder of the application is empty:
>>> sorted(root)
[]
Let's now fill the request with all the right values so that upon submitting
the form with the "Add" button, the person should be added to the root folder:
>>> request = TestRequest(form={
... 'form.widgets.id': u'srichter',
... 'form.widgets.name': u'Stephan Richter',
... 'form.widgets.gender': ['male'],
... 'form.widgets.age': u'20',
... 'form.buttons.add': u'Add'}
... )
>>> addForm = PersonAddForm(root, request)
>>> addForm.update()
>>> sorted(root)
[u'srichter']
>>> stephan = root[u'srichter']
>>> stephan.id
u'srichter'
>>> stephan.name
u'Stephan Richter'
>>> stephan.gender
'male'
>>> stephan.age
20
Submitting an add form with invalid data
----------------------------------------
Next we try to submit the add form with the required name missing. Thus, the
add form should not complete with the addition, but return with the add form
pointing out the error.
>>> request = TestRequest(form={
... 'form.widgets.id': u'srichter',
... 'form.widgets.gender': ['male'],
... 'form.widgets.age': u'23',
... 'form.buttons.add': u'Add'}
... )
>>> addForm = PersonAddForm(root, request)
>>> addForm.update()
The widget manager and the widget causing the error should have an error
message:
>>> [(error.widget.__name__, error) for error in addForm.widgets.errors]
[('name', <ErrorViewSnippet for RequiredMissing>)]
>>> addForm.widgets['name'].error
<ErrorViewSnippet for RequiredMissing>
Let's now render the form:
>>> addTemplate(addForm)
>>> print addForm.render()
<html>
<body>
<i>There were some errors.</i>
<ul>
<li>
Name: <div class="error">Required input is missing.</div>
</li>
</ul>
<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"
value="srichter" />
</div>
<div class="row">
<b><div class="error">Required input is missing.</div>
</b><label for="form-widgets-name">Name</label>
<input type="text" id="form-widgets-name" name="form.widgets.name"
class="text-widget required textline-field" value="" />
</div>
<div class="row">
<label for="form-widgets-gender">Gender</label>
<select id="form-widgets-gender" name="form.widgets.gender:list"
class="select-widget choice-field" size="1">
<option id="form-widgets-gender-novalue"
value="--NOVALUE--">no value</option>
<option id="form-widgets-gender-0" value="male"
selected="selected">male</option>
<option id="form-widgets-gender-1" value="female">female</option>
</select>
<input name="form.widgets.gender-empty-marker" type="hidden"
value="1" />
</div>
<div class="row">
<label for="form-widgets-age">Age</label>
<input type="text" id="form-widgets-age" name="form.widgets.age"
class="text-widget int-field" value="23" />
</div>
<div class="action">
<input type="submit" id="form-buttons-add" name="form.buttons.add"
class="submit-widget button-field" value="Add" />
</div>
</form>
</body>
</html>
Note that the values of the field are now extracted from the request.
Another way to receive an error is by not fulfilling the invariants of the
schema. In our case, the id and name cannot be the same. So let's provoke the
error now:
>>> request = TestRequest(form={
... 'form.widgets.id': u'Stephan',
... 'form.widgets.name': u'Stephan',
... 'form.widgets.gender': ['male'],
... 'form.widgets.age': u'23',
... 'form.buttons.add': u'Add'}
... )
>>> addForm = PersonAddForm(root, request)
>>> addTemplate(addForm)
>>> addForm.update()
and see how the form looks like:
>>> print addForm.render()
<html>
<body>
<i>There were some errors.</i>
<ul>
<li>
<div class="error">The id and name cannot be the same.</div>
</li>
</ul>
...
</body>
</html>
Let's try to provide a negative age, which is not possible either:
>>> request = TestRequest(form={
... 'form.widgets.id': u'srichter',
... 'form.widgets.gender': ['male'],
... 'form.widgets.age': u'-5',
... 'form.buttons.add': u'Add'}
... )
>>> addForm = PersonAddForm(root, request)
>>> addForm.update()
>>> [(view.widget.label, view) for view in addForm.widgets.errors]
[(u'Name', <ErrorViewSnippet for RequiredMissing>),
(u'Age', <ErrorViewSnippet for TooSmall>)]
But the error message for a negative age is too generic:
>>> print addForm.widgets['age'].error.render()
<div class="error">Value is too small</div>
It would be better to say that negative values are disallowed. So let's
register a new error view snippet for the ``TooSmall`` error:
>>> from z3c.form import error
>>> class TooSmallView(error.ErrorViewSnippet):
... zope.component.adapts(
... zope.schema.interfaces.TooSmall, None, None, None, None, None)
...
... def update(self):
... super(TooSmallView, self).update()
... if self.field.min == 0:
... self.message = u'The value cannot be a negative number.'
>>> zope.component.provideAdapter(TooSmallView)
>>> addForm = PersonAddForm(root, request)
>>> addForm.update()
>>> print addForm.widgets['age'].error.render()
<div class="error">The value cannot be a negative number.</div>
Note: The ``adapts()`` declaration might look strange. An error view
snippet is actually a multiadapter that adapts a combination of 6
objects -- error, request, widget, field, form, content. By specifying
only the error, we tell the system that we do not care about the other
discriminators, which then can be anything. We could also have used
``zope.interface.Interface`` instead, which would be equivalent.
Additional Form Attributes and API
----------------------------------
Since we are talking about HTML forms here, add and edit forms support all
relevant FORM element attributes as attributes on the class.
>>> addForm.method
'post'
>>> addForm.enctype
'multipart/form-data'
>>> addForm.acceptCharset
>>> addForm.accept
The ``action`` attribute is computed. By default it is the current URL:
>>> addForm.action
'http://127.0.0.1'
The name is also computed. By default it takes the prefix and removes any
trailing ".".
>>> addForm.name
'form'
The template can then use those attributes, if it likes to.
In the examples previously we set the template manually. If no
template is specified, the system tries to find an adapter. Without
any special configuration, there is no adapter, so rendering the form
fails:
>>> addForm.template = None
>>> addForm.render()
Traceback (most recent call last):
...
ComponentLookupError: ((...), <InterfaceClass ...IPageTemplate>, u'')
The form module provides a simple component to create adapter
factories from templates:
>>> factory = form.FormTemplateFactory(
... testing.getPath('../tests/simple_edit.pt'), form=PersonAddForm)
Let's register our new template-based adapter factory:
>>> zope.component.provideAdapter(factory)
Now the factory will be used to provide a template:
>>> print addForm.render()
<html>
...
</html>
Since a form can also be used as a page itself, it is callable. When
you call it will invoke both the ``update()`` and ``render()``
methods:
>>> print addForm()
<html>
...
</html>
Changing Widget Attribute Values
--------------------------------
It frequently happens that a customer comes along and wants to
slightly or totally change some of the text shown in forms or make
optional fields required. It does not make sense to always have to
adjust the schema or implement a custom schema for these use
cases. With the z3c.form framework all attributes -- for which it is
sensible to replace a value without touching the code -- are
customizable via an attribute value adapter.
To demonstrate this feature, let's change the label of the name widget
from "Name" to "Full Name":
>>> from z3c.form import widget
>>> NameLabel = widget.StaticWidgetAttribute(
... u'Full Name', field=IPerson['name'])
>>> zope.component.provideAdapter(NameLabel, name='label')
When the form renders, the label has now changed:
>>> addForm = PersonAddForm(root, TestRequest())
>>> addTemplate(addForm)
>>> addForm.update()
>>> print addForm.render()
<html>
...
<div class="row">
<label for="form-widgets-name">Full Name</label>
<input type="text" id="form-widgets-name" name="form.widgets.name"
class="text-widget required textline-field" value="" />
</div>
...
Adding a "Cancel" button
------------------------
Let's say a client requests that all add forms should have a "Cancel"
button. When the button is pressed, the user is forwarded to the next URL of
the add form. As always, the goal is to not touch the core implementation of
the code, but make those changes externally.
Adding a button/action is a little bit more involved than changing a value,
because you have to insert the additional action and customize the action
handler. Based on your needs of flexibility, multiple approaches could be
chosen. Here we demonstrate the simplest one.
The first step is to create a custom action manager that always inserts a
cancel action:
>>> from z3c.form import button
>>> class AddActions(button.ButtonActions):
... zope.component.adapts(
... interfaces.IAddForm,
... zope.interface.Interface,
... zope.interface.Interface)
...
... def update(self):
... self.form.buttons = button.Buttons(
... self.form.buttons,
... button.Button('cancel', u'Cancel'))
... super(AddActions, self).update()
After registering the new action manager,
>>> zope.component.provideAdapter(AddActions)
the add form should display a cancel button:
>>> addForm.update()
>>> print addForm.render()
<html>
...
<div class="action">
<input type="submit" id="form-buttons-add" name="form.buttons.add"
class="submit-widget button-field" value="Add" />
</div>
<div class="action">
<input type="submit" id="form-buttons-cancel" name="form.buttons.cancel"
class="submit-widget button-field" value="Cancel" />
</div>
...
But showing the button does not mean it does anything. So we also need a
custom action handler to handle the cancel action:
>>> class AddActionHandler(button.ButtonActionHandler):
... zope.component.adapts(
... interfaces.IAddForm,
... zope.interface.Interface,
... zope.interface.Interface,
... button.ButtonAction)
...
... def __call__(self):
... if self.action.name == 'form.buttons.cancel':
... self.form._finishedAdd = True
... return
... super(AddActionHandler, self).__call__()
After registering the action handler,
>>> zope.component.provideAdapter(AddActionHandler)
we can press the cancel button and we will be forwarded:
>>> request = TestRequest(form={'form.buttons.cancel': u'Cancel'})
>>> addForm = PersonAddForm(root, request)
>>> addTemplate(addForm)
>>> addForm.update()
>>> addForm.render()
''
>>> request.response.getStatus()
302
>>> request.response.getHeader('Location')
'index.html'
Eventually, we might have action managers and handlers that are much more
powerful and some of the manual labor in this example would become
unnecessary.
Creating an Edit Form
---------------------
Now that we have exhaustively covered the customization possibilities of add
forms, let's create an edit form. Edit forms are even simpler than add forms,
since all actions are completely automatic:
>>> class PersonEditForm(form.EditForm):
...
... fields = field.Fields(IPerson)
We can use the created person from the successful addition above.
>>> editForm = PersonEditForm(root[u'srichter'], TestRequest())
After adding a template, we can look at the form:
>>> addTemplate(editForm)
>>> editForm.update()
>>> print editForm.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="form-widgets-id">ID</label>
<span id="form-widgets-id"
class="text-widget required textline-field">srichter</span>
</div>
<div class="row">
<label for="form-widgets-name">Full Name</label>
<input type="text" id="form-widgets-name" name="form.widgets.name"
class="text-widget required textline-field"
value="Stephan Richter" />
</div>
<div class="row">
<label for="form-widgets-gender">Gender</label>
<select id="form-widgets-gender" name="form.widgets.gender:list"
class="select-widget choice-field" size="1">
<option id="form-widgets-gender-novalue"
value="--NOVALUE--">no value</option>
<option id="form-widgets-gender-0" value="male"
selected="selected">male</option>
<option id="form-widgets-gender-1" value="female">female</option>
</select>
<input name="form.widgets.gender-empty-marker" type="hidden"
value="1" />
</div>
<div class="row">
<label for="form-widgets-age">Age</label>
<input type="text" id="form-widgets-age" name="form.widgets.age"
class="text-widget int-field" value="20" />
</div>
<div class="action">
<input type="submit" id="form-buttons-apply" name="form.buttons.apply"
class="submit-widget button-field" value="Apply" />
</div>
</form>
</body>
</html>
As you can see, the data is being pulled in from the context for the edit
form. Next we will look at the behavior when submitting the form.
Failure Upon Submission of Edit Form
------------------------------------
Let's now submit the form having some invalid data.
>>> request = TestRequest(form={
... 'form.widgets.name': u'Claudia Richter',
... 'form.widgets.gender': ['female'],
... 'form.widgets.age': u'-1',
... 'form.buttons.apply': u'Apply'}
... )
>>> editForm = PersonEditForm(root[u'srichter'], request)
>>> addTemplate(editForm)
>>> editForm.update()
>>> print editForm.render()
<html>
<body>
<i>There were some errors.</i>
<ul>
<li>
Age: <div class="error">The value cannot be a negative number.</div>
</li>
</ul>
<form action=".">
<div class="row">
<label for="form-widgets-id">ID</label>
<span id="form-widgets-id"
class="text-widget required textline-field">srichter</span>
</div>
<div class="row">
<label for="form-widgets-name">Full Name</label>
<input type="text" id="form-widgets-name" name="form.widgets.name"
class="text-widget required textline-field"
value="Claudia Richter" />
</div>
<div class="row">
<label for="form-widgets-gender">Gender</label>
<select id="form-widgets-gender" name="form.widgets.gender:list"
class="select-widget choice-field" size="1">
<option id="form-widgets-gender-novalue"
value="--NOVALUE--">no value</option>
<option id="form-widgets-gender-0" value="male">male</option>
<option id="form-widgets-gender-1" value="female"
selected="selected">female</option>
</select>
<input name="form.widgets.gender-empty-marker" type="hidden"
value="1" />
</div>
<div class="row">
<b><div class="error">The value cannot be a negative number.</div>
</b><label for="form-widgets-age">Age</label>
<input type="text" id="form-widgets-age" name="form.widgets.age"
class="text-widget int-field" value="-1" />
</div>
<div class="action">
<input type="submit" id="form-buttons-apply" name="form.buttons.apply"
class="submit-widget button-field" value="Apply" />
</div>
</form>
</body>
</html>
Successfully Editing Content
----------------------------
Let's now resubmit the form with valid data, so the data should be updated.
>>> request = TestRequest(form={
... 'form.widgets.name': u'Claudia Richter',
... 'form.widgets.gender': ['female'],
... 'form.widgets.age': u'27',
... 'form.buttons.apply': u'Apply'}
... )
>>> editForm = PersonEditForm(root[u'srichter'], request)
>>> addTemplate(editForm)
>>> editForm.update()
>>> print editForm.render()
<html>
...
<i>Data successfully updated.</i>
...
>>> stephan = root[u'srichter']
>>> stephan.name
u'Claudia Richter'
>>> stephan.gender
'female'
>>> stephan.age
27
When an edit form is successfully committed, a detailed object-modified event
is sent out telling the system about the changes. To see the error, let's
create an event subscriber for object-modified events:
>>> eventlog = []
>>> import zope.lifecycleevent
>>> @zope.component.adapter(zope.lifecycleevent.ObjectModifiedEvent)
... def logEvent(event):
... eventlog.append(event)
>>> zope.component.provideHandler(logEvent)
Let's now submit the form again, successfully changing the age:
>>> request = TestRequest(form={
... 'form.widgets.name': u'Claudia Richter',
... 'form.widgets.gender': ['female'],
... 'form.widgets.age': u'29',
... 'form.buttons.apply': u'Apply'}
... )
>>> editForm = PersonEditForm(root[u'srichter'], request)
>>> addTemplate(editForm)
>>> editForm.update()
We can now look at the event:
>>> event = eventlog[-1]
>>> event
<zope.app.event.objectevent.ObjectModifiedEvent object at ...>
>>> attrs = event.descriptions[0]
>>> attrs.interface
<InterfaceClass __builtin__.IPerson>
>>> attrs.attributes
('age',)
Successful Action with No Changes
---------------------------------
When submitting the form without any changes, the form will tell you so.
>>> request = TestRequest(form={
... 'form.widgets.name': u'Claudia Richter',
... 'form.widgets.gender': ['female'],
... 'form.widgets.age': u'29',
... 'form.buttons.apply': u'Apply'}
... )
>>> editForm = PersonEditForm(root[u'srichter'], request)
>>> addTemplate(editForm)
>>> editForm.update()
>>> print editForm.render()
<html>
...
<i>No changes were applied.</i>
...
Changing Status Messages
------------------------
Depending on the project, it is often desirable to change the status messages
to fit the application. In ``zope.formlib`` this was hard to do, since the
messages were buried within fairly complex methods that one did not want to
touch. In this package all those messages are exposed as form attributes.
There are three messages for the edit form:
* ``formErrorsMessage`` -- Indicates that an error occurred while
applying the changes. This message is also available for the add form.
* ``successMessage`` -- The form data was successfully applied.
* ``noChangesMessage`` -- No changes were found in the form data.
Let's now change the ``noChangesMessage``:
>>> editForm.noChangesMessage = u'No changes were detected in the form data.'
>>> editForm.update()
>>> print editForm.render()
<html>
...
<i>No changes were detected in the form data.</i>
...
When even more flexibility is required within a project, one could also
implement these messages as properties looking up an attribute value. However,
we have found this to be a rare case.
Creating Edit Forms for Dictionaries
------------------------------------
Sometimes it is not desirable to edit a class instance that implements the
fields, but other types of object. A good example is the need to modify a
simple dictionary, where the field names are the keys. To do that, a special
data manager for dictionaries is available:
>>> from z3c.form import datamanager
>>> zope.component.provideAdapter(datamanager.DictionaryField)
The only step the developer has to complete is to re-implement the form's
``getContent()`` method to return the dictionary:
>>> personDict = {'id': u'rineichen', 'name': u'Roger Ineichen',
... 'gender': None, 'age': None}
>>> class PersonDictEditForm(PersonEditForm):
... def getContent(self):
... return personDict
We can now use the form as usual:
>>> editForm = PersonDictEditForm(None, TestRequest())
>>> addTemplate(editForm)
>>> editForm.update()
>>> print editForm.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="form-widgets-id">ID</label>
<span id="form-widgets-id"
class="text-widget required textline-field">rineichen</span>
</div>
<div class="row">
<label for="form-widgets-name">Full Name</label>
<input type="text" id="form-widgets-name"
name="form.widgets.name"
class="text-widget required textline-field"
value="Roger Ineichen" />
</div>
<div class="row">
<label for="form-widgets-gender">Gender</label>
<select id="form-widgets-gender" name="form.widgets.gender:list"
class="select-widget choice-field" size="1">
<option id="form-widgets-gender-novalue"
value="--NOVALUE--" selected="selected">no value</option>
<option id="form-widgets-gender-0" value="male">male</option>
<option id="form-widgets-gender-1" value="female">female</option>
</select>
<input name="form.widgets.gender-empty-marker" type="hidden"
value="1" />
</div>
<div class="row">
<label for="form-widgets-age">Age</label>
<input type="text" id="form-widgets-age"
name="form.widgets.age" class="text-widget int-field"
value="20" />
</div>
<div class="action">
<input type="submit" id="form-buttons-apply"
name="form.buttons.apply" class="submit-widget button-field"
value="Apply" />
</div>
</form>
</body>
</html>
Note that the name displayed in the form is identical to the one in the
dictionary. Let's now submit a form to ensure that the data is also written to
the dictionary:
>>> request = TestRequest(form={
... 'form.widgets.name': u'Jesse Ineichen',
... 'form.widgets.gender': ['male'],
... 'form.widgets.age': u'5',
... 'form.buttons.apply': u'Apply'}
... )
>>> editForm = PersonDictEditForm(None, request)
>>> editForm.update()
>>> from zope.testing.doctestunit import pprint
>>> pprint(personDict)
{'age': 5,
'gender': 'male',
'id': u'rineichen',
'name': u'Jesse Ineichen'}
Creating a Display Form
-----------------------
Creating a display form is simple; just instantiate, update and render it:
>>> class PersonDisplayForm(form.DisplayForm):
... fields = field.Fields(IPerson)
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_display.pt', os.path.dirname(tests.__file__))
>>> display = PersonDisplayForm(stephan, TestRequest())
>>> display.update()
>>> print display.render()
<html>
<body>
<div class="row">
<span id="form-widgets-id"
class="text-widget required textline-field">srichter</span>
</div>
<div class="row">
<span id="form-widgets-name"
class="text-widget required textline-field">Claudia Richter</span>
</div>
<div class="row">
<span id="form-widgets-gender"
class="select-widget choice-field"><span
class="selected-option">female</span></span>
</div>
<div class="row">
<span id="form-widgets-age" class="text-widget int-field">29</span>
</div>
</body>
</html>
Simple Form Customization
-------------------------
The form exposes several of the widget manager's attributes as attributes on
the form. They are: ``mode``, ``ignoreContext``, ``ignoreRequest``, and
``ignoreReadonly``.
Here are the values for the display form we just created:
>>> display.mode
'display'
>>> display.ignoreContext
False
>>> display.ignoreRequest
True
>>> display.ignoreReadonly
False
These values should be equal to the ones of the widget manager:
>>> display.widgets.mode
'display'
>>> display.widgets.ignoreContext
False
>>> display.widgets.ignoreRequest
True
>>> display.widgets.ignoreReadonly
False
Now, if we change those values before updating the widgets, ...
>>> display.mode = interfaces.INPUT_MODE
>>> display.ignoreContext = True
>>> display.ignoreRequest = False
>>> display.ignoreReadonly = True
... the widget manager will have the same values after updating the widgets:
>>> display.updateWidgets()
>>> display.widgets.mode
'input'
>>> display.widgets.ignoreContext
True
>>> display.widgets.ignoreRequest
False
>>> display.widgets.ignoreReadonly
True
Extending Forms
---------------
One very common use case is to extend forms. For example, you would like to
use the edit form and its defined "Apply" button, but add another button
yourself. Unfortunately, just inheriting the form is not enough, because the
new button and handler declarations will override the inherited ones. Let me
demonstrate the problem:
>>> class BaseForm(form.Form):
... fields = field.Fields(IPerson).select('name')
...
... @button.buttonAndHandler(u'Apply')
... def handleApply(self, action):
... print 'success'
>>> BaseForm.fields.keys()
['name']
>>> BaseForm.buttons.keys()
['apply']
>>> BaseForm.handlers
<Handlers [<Handler for <Button 'apply' u'Apply'>>]>
Let's now derive a form from the base form:
>>> class DerivedForm(BaseForm):
... fields = field.Fields(IPerson).select('gender')
...
... @button.buttonAndHandler(u'Cancel')
... def handleCancel(self, action):
... print 'cancel'
>>> DerivedForm.fields.keys()
['gender']
>>> DerivedForm.buttons.keys()
['cancel']
>>> DerivedForm.handlers
<Handlers [<Handler for <Button 'cancel' u'Cancel'>>]>
The obvious method to "inherit" the base form's information is to copy it
over:
>>> class DerivedForm(BaseForm):
... fields = BaseForm.fields.copy()
... buttons = BaseForm.buttons.copy()
... handlers = BaseForm.handlers.copy()
...
... fields += field.Fields(IPerson).select('gender')
...
... @button.buttonAndHandler(u'Cancel')
... def handleCancel(self, action):
... print 'cancel'
>>> DerivedForm.fields.keys()
['name', 'gender']
>>> DerivedForm.buttons.keys()
['apply', 'cancel']
>>> DerivedForm.handlers
<Handlers
[<Handler for <Button 'apply' u'Apply'>>,
<Handler for <Button 'cancel' u'Cancel'>>]>
But this is pretty clumsy. Instead, the ``form`` module provides a helper
method that will do the extending for you:
>>> class DerivedForm(BaseForm):
... form.extends(BaseForm)
...
... fields += field.Fields(IPerson).select('gender')
...
... @button.buttonAndHandler(u'Cancel')
... def handleCancel(self, action):
... print 'cancel'
>>> DerivedForm.fields.keys()
['name', 'gender']
>>> DerivedForm.buttons.keys()
['apply', 'cancel']
>>> DerivedForm.handlers
<Handlers
[<Handler for <Button 'apply' u'Apply'>>,
<Handler for <Button 'cancel' u'Cancel'>>]>
If you, for example do not want to extend the buttons, you can turn that off:
>>> class DerivedForm(BaseForm):
... form.extends(BaseForm, ignoreButtons=True)
...
... fields += field.Fields(IPerson).select('gender')
...
... @button.buttonAndHandler(u'Cancel')
... def handleCancel(self, action):
... print 'cancel'
>>> DerivedForm.fields.keys()
['name', 'gender']
>>> DerivedForm.buttons.keys()
['cancel']
>>> DerivedForm.handlers
<Handlers
[<Handler for <Button 'apply' u'Apply'>>,
<Handler for <Button 'cancel' u'Cancel'>>]>
If you, for example do not want to extend the handlers, you can turn that off:
>>> class DerivedForm(BaseForm):
... form.extends(BaseForm, ignoreHandlers=True)
...
... fields += field.Fields(IPerson).select('gender')
...
... @button.buttonAndHandler(u'Cancel')
... def handleCancel(self, action):
... print 'cancel'
>>> DerivedForm.fields.keys()
['name', 'gender']
>>> DerivedForm.buttons.keys()
['apply', 'cancel']
>>> DerivedForm.handlers
<Handlers [<Handler for <Button 'cancel' u'Cancel'>>]>
Custom widget factories
-----------------------
Another important part of a form is that we can use custom widgets. We can do
this in a form by defining a widget factory for a field. We can get the field
from the fields collection e.g. ``fields['foo']``. This means, we can define
new widget factories by defining ``fields['foo'].widgetFactory = MyWidget``.
Let's show a sample and define a custom widget:
>>> from z3c.form.browser import text
>>> class MyWidget(text.TextWidget):
... """My new widget."""
... klass = u'MyCSS'
Now we can define a field widget factory:
>>> def MyFieldWidget(field, request):
... """IFieldWidget factory for MyWidget."""
... return widget.FieldWidget(field, MyWidget(request))
We register the ``MyWidget`` in a form like:
>>> class MyEditForm(form.EditForm):
...
... fields = field.Fields(IPerson)
... fields['name'].widgetFactory = MyFieldWidget
We can see that the custom widget gets used in the rendered form:
>>> myEdit = MyEditForm(root[u'srichter'], TestRequest())
>>> addTemplate(myEdit)
>>> myEdit.update()
>>> print myEdit.render()
...
<html...
<input type="text" id="form-widgets-name"
name="form.widgets.name" class="MyCSS required textline-field"
value="Claudia Richter" />
...
Hidden fields
-------------
Another important part of a form is that we can generate hidden widgets. We can
do this in a form by defining a widget mode. We can do this by override the
setUpWidgets method.
>>> class HiddenFieldEditForm(form.EditForm):
...
... fields = field.Fields(IPerson)
... fields['name'].widgetFactory = MyFieldWidget
...
... def updateWidgets(self):
... super(HiddenFieldEditForm, self).updateWidgets()
... self.widgets['age'].mode = interfaces.HIDDEN_MODE
We can see that the widget gets rendered as hidden:
>>> hiddenEdit = HiddenFieldEditForm(root[u'srichter'], TestRequest())
>>> addTemplate(hiddenEdit)
>>> hiddenEdit.update()
>>> print hiddenEdit.render()
<html>...
<input type="text" id="form-widgets-name"
name="form.widgets.name" class="MyCSS required textline-field"
value="Claudia Richter" />
...
<input type="hidden" id="form-widgets-age"
name="form.widgets.age" class="hidden-widget"
value="29" />
...
Actions with Errors
-------------------
Even though the data might be validated correctly, it sometimes happens that
data turns out to be invalid while the action is executed. In those cases a
special action execution error can be raised that wraps the original error.
>>> class PersonAddForm(form.AddForm):
...
... fields = field.Fields(IPerson).select('id')
...
... @button.buttonAndHandler(u'Check')
... def handleCheck(self, action):
... data, errors = self.extractData()
... if data['id'] in self.getContent():
... raise interfaces.WidgetActionExecutionError(
... 'id', zope.interface.Invalid('Id already exists'))
In this case the action execution error is specific to a widget. The framework
will attach a proper error view to the widget and the widget manager:
>>> request = TestRequest(form={
... 'form.widgets.id': u'srichter',
... 'form.buttons.check': u'Check'}
... )
>>> addForm = PersonAddForm(root, request)
>>> addForm.update()
>>> addForm.widgets.errors
(<InvalidErrorViewSnippet for Invalid>,)
>>> addForm.widgets['id'].error
<InvalidErrorViewSnippet for Invalid>
>>> addForm.status
u'There were some errors.'
If the error is non-widget specific, then we can simply use the generic action
execution error:
>>> class PersonAddForm(form.AddForm):
...
... fields = field.Fields(IPerson).select('id')
...
... @button.buttonAndHandler(u'Check')
... def handleCheck(self, action):
... raise interfaces.ActionExecutionError(
... zope.interface.Invalid('Some problem occurred.'))
Let's have a look at the result:
>>> addForm = PersonAddForm(root, request)
>>> addForm.update()
>>> addForm.widgets.errors
(<InvalidErrorViewSnippet for Invalid>,)
>>> addForm.status
u'There were some errors.'
**Note**:
The action execution errors are connected to the form via an event
listener called ``handlerActionError``. This event listener listens for
``IActionErrorEvent`` events. If the event is called for an action associated
with a form, the listener does its work as seen above. If the action is not
coupled to a form, then event listener does nothing:
>>> from z3c.form import action
>>> cancel = action.Action(request, u'Cancel')
>>> event = action.ActionErrorOccurred(cancel, ValueError(3))
>>> form.handleActionError(event)
Integration tests
-----------------
Identify the different forms can be important if it comes to layout template
lookup. Ensure that we support the right interfaces for the different forms.
Form
~~~~
>>> from zope.interface.verify import verifyObject
>>> from z3c.form import interfaces
>>> obj = form.Form(None, None)
>>> verifyObject(interfaces.IForm, obj)
True
>>> interfaces.IForm.providedBy(obj)
True
>>> from z3c.form import interfaces
>>> interfaces.IDisplayForm.providedBy(obj)
False
>>> from z3c.form import interfaces
>>> interfaces.IEditForm.providedBy(obj)
False
>>> from z3c.form import interfaces
>>> interfaces.IAddForm.providedBy(obj)
False
DisplayForm
~~~~~~~~~~~
>>> from z3c.form import interfaces
>>> obj = form.DisplayForm(None, None)
>>> verifyObject(interfaces.IDisplayForm, obj)
True
>>> interfaces.IForm.providedBy(obj)
True
>>> from z3c.form import interfaces
>>> interfaces.IDisplayForm.providedBy(obj)
True
>>> from z3c.form import interfaces
>>> interfaces.IEditForm.providedBy(obj)
False
>>> from z3c.form import interfaces
>>> interfaces.IAddForm.providedBy(obj)
False
EditForm
~~~~~~~~
>>> from z3c.form import interfaces
>>> obj = form.EditForm(None, None)
>>> verifyObject(interfaces.IEditForm, obj)
True
>>> interfaces.IForm.providedBy(obj)
True
>>> from z3c.form import interfaces
>>> interfaces.IDisplayForm.providedBy(obj)
False
>>> from z3c.form import interfaces
>>> interfaces.IEditForm.providedBy(obj)
True
>>> from z3c.form import interfaces
>>> interfaces.IAddForm.providedBy(obj)
False
AddForm
~~~~~~~
>>> from z3c.form import interfaces
>>> obj = form.AddForm(None, None)
>>> verifyObject(interfaces.IAddForm, obj)
True
>>> interfaces.IForm.providedBy(obj)
True
>>> from z3c.form import interfaces
>>> interfaces.IDisplayForm.providedBy(obj)
False
>>> from z3c.form import interfaces
>>> interfaces.IEditForm.providedBy(obj)
False
>>> from z3c.form import interfaces
>>> interfaces.IAddForm.providedBy(obj)
True
===========
Group Forms
===========
Group forms allow you to split up a form into several logical units without
much overhead. To the parent form, groups should be only dealt with during
coding and be transparent on the data extraction level.
For the examples to work, we have to bring up most of the form framework:
>>> from z3c.form import testing
>>> testing.setupFormDefaults()
So let's first define a complex content component that warrants setting up
multiple groups:
>>> import zope.interface
>>> import zope.schema
>>> class IVehicleRegistration(zope.interface.Interface):
... firstName = zope.schema.TextLine(title=u'First Name')
... lastName = zope.schema.TextLine(title=u'Last Name')
...
... license = zope.schema.TextLine(title=u'License')
... address = zope.schema.TextLine(title=u'Address')
...
... model = zope.schema.TextLine(title=u'Model')
... make = zope.schema.TextLine(title=u'Make')
... year = zope.schema.Int(title=u'Year')
>>> class VehicleRegistration(object):
... zope.interface.implements(IVehicleRegistration)
...
... def __init__(self, **kw):
... for name, value in kw.items():
... setattr(self, name, value)
The schema above can be separated into basic, license, and car information,
where the latter two will be placed into groups. First we create the two
groups:
>>> from z3c.form import field, group
>>> class LicenseGroup(group.Group):
... label = u'License'
... fields = field.Fields(IVehicleRegistration).select(
... 'license', 'address')
>>> class CarGroup(group.Group):
... label = u'Car'
... fields = field.Fields(IVehicleRegistration).select(
... 'model', 'make', 'year')
Most of the group is setup like any other (sub)form. Additionally, you can
specify a label, which is a human-readable string that can be used for layout
purposes.
Let's now create an add form for the entire vehicle registration. In
comparison to a regular add form, you only need to add the ``GroupForm`` as
one of the base classes. The groups are specified in a simple tuple:
>>> import os
>>> from zope.app.pagetemplate import viewpagetemplatefile
>>> from z3c.form import form, tests
>>> class RegistrationAddForm(group.GroupForm, form.AddForm):
... fields = field.Fields(IVehicleRegistration).select(
... 'firstName', 'lastName')
... groups = (LicenseGroup, CarGroup)
...
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_groupedit.pt', os.path.dirname(tests.__file__))
...
... def create(self, data):
... return VehicleRegistration(**data)
...
... def add(self, object):
... self.getContent()['obj1'] = object
... return object
Note: The order of the base classes is very important here. The ``GroupForm``
class must be left of the ``AddForm`` class, because the ``GroupForm`` class
overrides some methods of the ``AddForm`` class.
Now we can instantiate the form:
>>> request = testing.TestRequest()
>>> add = RegistrationAddForm(None, request)
>>> add.update()
After the form is updated the tuple of group classes is converted to group
instances:
>>> add.groups
(<LicenseGroup object at ...>, <CarGroup object at ...>)
If we happen to update the add form again, the groups that have
already been converted to instances ares skipped.
>>> add.update()
>>> add.groups
(<LicenseGroup object at ...>, <CarGroup object at ...>)
We can now render the form:
>>> print add.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="form-widgets-firstName">First Name</label>
<input type="text" id="form-widgets-firstName"
name="form.widgets.firstName"
class="text-widget required textline-field"
value="" />
</div>
<div class="row">
<label for="form-widgets-lastName">Last Name</label>
<input type="text" id="form-widgets-lastName"
name="form.widgets.lastName"
class="text-widget required textline-field"
value="" />
</div>
<fieldgroup>
<legend>License</legend>
<div class="row">
<label for="form-widgets-license">License</label>
<input type="text" id="form-widgets-license"
name="form.widgets.license"
class="text-widget required textline-field"
value="" />
</div>
<div class="row">
<label for="form-widgets-address">Address</label>
<input type="text" id="form-widgets-address"
name="form.widgets.address"
class="text-widget required textline-field"
value="" />
</div>
</fieldgroup>
<fieldgroup>
<legend>Car</legend>
<div class="row">
<label for="form-widgets-model">Model</label>
<input type="text" id="form-widgets-model"
name="form.widgets.model"
class="text-widget required textline-field"
value="" />
</div>
<div class="row">
<label for="form-widgets-make">Make</label>
<input type="text" id="form-widgets-make"
name="form.widgets.make"
class="text-widget required textline-field"
value="" />
</div>
<div class="row">
<label for="form-widgets-year">Year</label>
<input type="text" id="form-widgets-year"
name="form.widgets.year"
class="text-widget required int-field"
value="" />
</div>
</fieldgroup>
<div class="action">
<input type="submit" id="form-buttons-add"
name="form.buttons.add" class="submit-widget button-field"
value="Add" />
</div>
</form>
</body>
</html>
Let's now submit the form, but forgetting to enter the address:
>>> request = testing.TestRequest(form={
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.lastName': u'Richter',
... 'form.widgets.license': u'MA 40387',
... 'form.widgets.model': u'BMW',
... 'form.widgets.make': u'325',
... 'form.widgets.year': u'2005',
... 'form.buttons.add': u'Add'
... })
>>> add = RegistrationAddForm(None, request)
>>> add.update()
>>> print add.render()
<html>
<body>
<i>There were some errors.</i>
<form action=".">
...
<fieldgroup>
<legend>License</legend>
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
...
</fieldgroup>
...
</form>
</body>
</html>
As you can see, the template is clever enough to just report the errors at the
top of the form, but still report the actual problem within the group. So what
happens, if errors happen inside and outside a group?
>>> request = testing.TestRequest(form={
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.license': u'MA 40387',
... 'form.widgets.model': u'BMW',
... 'form.widgets.make': u'325',
... 'form.widgets.year': u'2005',
... 'form.buttons.add': u'Add'
... })
>>> add = RegistrationAddForm(None, request)
>>> add.update()
>>> print add.render()
<html>
<body>
<i>There were some errors.</i>
<ul>
<li>
Last Name: <div class="error">Required input is missing.</div>
</li>
</ul>
<form action=".">
...
<fieldgroup>
<legend>License</legend>
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
...
</fieldgroup>
...
</form>
</body>
</html>
Let's now successfully complete the add form.
>>> from zope.app.container import btree
>>> context = btree.BTreeContainer()
>>> request = testing.TestRequest(form={
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.lastName': u'Richter',
... 'form.widgets.license': u'MA 40387',
... 'form.widgets.address': u'10 Main St, Maynard, MA',
... 'form.widgets.model': u'BMW',
... 'form.widgets.make': u'325',
... 'form.widgets.year': u'2005',
... 'form.buttons.add': u'Add'
... })
>>> add = RegistrationAddForm(context, request)
>>> add.update()
The object is now added to the container and all attributes should be set:
>>> reg = context['obj1']
>>> reg.firstName
u'Stephan'
>>> reg.lastName
u'Richter'
>>> reg.license
u'MA 40387'
>>> reg.address
u'10 Main St, Maynard, MA'
>>> reg.model
u'BMW'
>>> reg.make
u'325'
>>> reg.year
2005
Let's now have a look at an edit form for the vehicle registration:
>>> class RegistrationEditForm(group.GroupForm, form.EditForm):
... fields = field.Fields(IVehicleRegistration).select(
... 'firstName', 'lastName')
... groups = (LicenseGroup, CarGroup)
...
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_groupedit.pt', os.path.dirname(tests.__file__))
>>> request = testing.TestRequest()
>>> edit = RegistrationEditForm(reg, request)
>>> edit.update()
After updating the form, we can render the HTML:
>>> print edit.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="form-widgets-firstName">First Name</label>
<input type="text" id="form-widgets-firstName"
name="form.widgets.firstName"
class="text-widget required textline-field"
value="Stephan" />
</div>
<div class="row">
<label for="form-widgets-lastName">Last Name</label>
<input type="text" id="form-widgets-lastName"
name="form.widgets.lastName"
class="text-widget required textline-field"
value="Richter" />
</div>
<fieldgroup>
<legend>License</legend>
<div class="row">
<label for="form-widgets-license">License</label>
<input type="text" id="form-widgets-license"
name="form.widgets.license"
class="text-widget required textline-field"
value="MA 40387" />
</div>
<div class="row">
<label for="form-widgets-address">Address</label>
<input type="text" id="form-widgets-address"
name="form.widgets.address"
class="text-widget required textline-field"
value="10 Main St, Maynard, MA" />
</div>
</fieldgroup>
<fieldgroup>
<legend>Car</legend>
<div class="row">
<label for="form-widgets-model">Model</label>
<input type="text" id="form-widgets-model"
name="form.widgets.model"
class="text-widget required textline-field"
value="BMW" />
</div>
<div class="row">
<label for="form-widgets-make">Make</label>
<input type="text" id="form-widgets-make"
name="form.widgets.make"
class="text-widget required textline-field"
value="325" />
</div>
<div class="row">
<label for="form-widgets-year">Year</label>
<input type="text" id="form-widgets-year"
name="form.widgets.year"
class="text-widget required int-field"
value="2005" />
</div>
</fieldgroup>
<div class="action">
<input type="submit" id="form-buttons-apply"
name="form.buttons.apply" class="submit-widget button-field"
value="Apply" />
</div>
</form>
</body>
</html>
The behavior when an error occurs is identical to that of the add form:
>>> request = testing.TestRequest(form={
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.lastName': u'Richter',
... 'form.widgets.license': u'MA 40387',
... 'form.widgets.model': u'BMW',
... 'form.widgets.make': u'325',
... 'form.widgets.year': u'2005',
... 'form.buttons.apply': u'Apply'
... })
>>> edit = RegistrationEditForm(reg, request)
>>> edit.update()
>>> print edit.render()
<html>
<body>
<i>There were some errors.</i>
<form action=".">
...
<fieldgroup>
<legend>License</legend>
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
...
</fieldgroup>
...
</form>
</body>
</html>
When an edit form with groups is successfully committed, a detailed
object-modified event is sent out telling the system about the changes.
To see the error, let's create an event subscriber for object-modified events:
>>> eventlog = []
>>> import zope.lifecycleevent
>>> @zope.component.adapter(zope.lifecycleevent.ObjectModifiedEvent)
... def logEvent(event):
... eventlog.append(event)
>>> zope.component.provideHandler(logEvent)
Let's now complete the form successfully:
>>> request = testing.TestRequest(form={
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.lastName': u'Richter',
... 'form.widgets.license': u'MA 4038765',
... 'form.widgets.address': u'11 Main St, Maynard, MA',
... 'form.widgets.model': u'Ford',
... 'form.widgets.make': u'F150',
... 'form.widgets.year': u'2006',
... 'form.buttons.apply': u'Apply'
... })
>>> edit = RegistrationEditForm(reg, request)
>>> edit.update()
The success message will be shown on the form, ...
>>> print edit.render()
<html>
<body>
<i>Data successfully updated.</i>
...
</body>
</html>
and the data is correctly updated:
>>> reg.firstName
u'Stephan'
>>> reg.lastName
u'Richter'
>>> reg.license
u'MA 4038765'
>>> reg.address
u'11 Main St, Maynard, MA'
>>> reg.model
u'Ford'
>>> reg.make
u'F150'
>>> reg.year
2006
Let's look at the event:
>>> event = eventlog[-1]
>>> event
<zope.app.event.objectevent.ObjectModifiedEvent object at ...>
The event's description contains the changed Interface and the names of
all changed fields, even if they where in different groups:
>>> attrs = event.descriptions[0]
>>> attrs.interface
<InterfaceClass __builtin__.IVehicleRegistration>
>>> attrs.attributes
('license', 'address', 'model', 'make', 'year')
And that's it!
Groups with Different Content
-----------------------------
You can customize the content for a group by overriding a group's
``getContent`` method. This is a very easy way to get around not
having object widgets. For example, suppose we want to maintain the
vehicle owner's information in a separate class than the vehicle. We
might have an ``IVehicleOwner`` interface like so.
>>> class IVehicleOwner(zope.interface.Interface):
... firstName = zope.schema.TextLine(title=u'First Name')
... lastName = zope.schema.TextLine(title=u'Last Name')
Then out ``IVehicleRegistration`` interface would include an object
field for the owner instead of the ``firstName`` and ``lastName``
fields.
>>> class IVehicleRegistration(zope.interface.Interface):
... owner = zope.schema.Object(title=u'Owner', schema=IVehicleOwner)
...
... license = zope.schema.TextLine(title=u'License')
... address = zope.schema.TextLine(title=u'Address')
...
... model = zope.schema.TextLine(title=u'Model')
... make = zope.schema.TextLine(title=u'Make')
... year = zope.schema.Int(title=u'Year')
Now let's create simple implementations of these two interfaces.
>>> class VehicleOwner(object):
... zope.interface.implements(IVehicleOwner)
...
... def __init__(self, **kw):
... for name, value in kw.items():
... setattr(self, name, value)
>>> class VehicleRegistration(object):
... zope.interface.implements(IVehicleRegistration)
...
... def __init__(self, **kw):
... for name, value in kw.items():
... setattr(self, name, value)
Now we can create a group just for the owner with its own
``getContent`` method that simply returns the ``owner`` object field
of the ``VehicleRegistration`` instance.
>>> class OwnerGroup(group.Group):
... label = u'Owner'
... fields = field.Fields(IVehicleOwner, prefix='owner')
...
... def getContent(self):
... return self.context.owner
When we create an Edit form for example, we should omit the ``owner``
field which is taken care of with the group.
>>> class RegistrationEditForm(group.GroupForm, form.EditForm):
... fields = field.Fields(IVehicleRegistration).omit(
... 'owner')
... groups = (OwnerGroup,)
...
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_groupedit.pt', os.path.dirname(tests.__file__))
>>> reg = VehicleRegistration(
... license=u'MA 40387',
... address=u'10 Main St, Maynard, MA',
... model=u'BMW',
... make=u'325',
... year=u'2005',
... owner=VehicleOwner(firstName=u'Stephan',
... lastName=u'Richter'))
>>> request = testing.TestRequest()
>>> edit = RegistrationEditForm(reg, request)
>>> edit.update()
When we render the form, the group appears as we would expect but with
the ``owner`` prefix for the fields.
>>> print edit.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="form-widgets-license">License</label>
<input type="text" id="form-widgets-license"
name="form.widgets.license"
class="text-widget required textline-field"
value="MA 40387" />
</div>
<div class="row">
<label for="form-widgets-address">Address</label>
<input type="text" id="form-widgets-address"
name="form.widgets.address"
class="text-widget required textline-field"
value="10 Main St, Maynard, MA" />
</div>
<div class="row">
<label for="form-widgets-model">Model</label>
<input type="text" id="form-widgets-model"
name="form.widgets.model"
class="text-widget required textline-field"
value="BMW" />
</div>
<div class="row">
<label for="form-widgets-make">Make</label>
<input type="text" id="form-widgets-make"
name="form.widgets.make"
class="text-widget required textline-field"
value="325" />
</div>
<div class="row">
<label for="form-widgets-year">Year</label>
<input type="text" id="form-widgets-year"
name="form.widgets.year"
class="text-widget required int-field" value="2005" />
</div>
<fieldgroup>
<legend>Owner</legend>
<div class="row">
<label for="form-widgets-owner-firstName">First Name</label>
<input type="text" id="form-widgets-owner-firstName"
name="form.widgets.owner.firstName"
class="text-widget required textline-field"
value="Stephan" />
</div>
<div class="row">
<label for="form-widgets-owner-lastName">Last Name</label>
<input type="text" id="form-widgets-owner-lastName"
name="form.widgets.owner.lastName"
class="text-widget required textline-field"
value="Richter" />
</div>
</fieldgroup>
<div class="action">
<input type="submit" id="form-buttons-apply"
name="form.buttons.apply"
class="submit-widget button-field" value="Apply" />
</div>
</form>
</body>
</html>
Now let's try and edit the owner. For example, suppose that Stephan
Richter gave his BMW to Paul Carduner because he is such a nice guy.
>>> request = testing.TestRequest(form={
... 'form.widgets.owner.firstName': u'Paul',
... 'form.widgets.owner.lastName': u'Carduner',
... 'form.widgets.license': u'MA 4038765',
... 'form.widgets.address': u'Berkeley',
... 'form.widgets.model': u'BMW',
... 'form.widgets.make': u'325',
... 'form.widgets.year': u'2005',
... 'form.buttons.apply': u'Apply'
... })
>>> edit = RegistrationEditForm(reg, request)
>>> edit.update()
We'll see if everything worked on the form side.
>>> print edit.render()
<html>
<body>
<i>Data successfully updated.</i>
...
</body>
</html>
Now the owner object should have updated fields.
>>> reg.owner.firstName
u'Paul'
>>> reg.owner.lastName
u'Carduner'
>>> reg.license
u'MA 4038765'
>>> reg.address
u'Berkeley'
>>> reg.model
u'BMW'
>>> reg.make
u'325'
>>> reg.year
2005
And that's it!
=========
Sub-Forms
=========
Traditionally, the Zope community talks about sub-forms in a generic manner
without defining their purpose, restrictions and assumptions. When we
initially talked about sub-forms for this package, we quickly noticed that
there are several classes of sub-forms with different goals.
Of course, we need to setup our defaults for this demonstration as well:
>>> from z3c.form import testing
>>> testing.setupFormDefaults()
Class I: The form within the form
---------------------------------
This class of sub-forms provides a complete form within a form, including its
own actions. When an action of the sub-form is submitted, the parent form
usually does not interact with that action at all. The same is true for the
reverse; when an action of the parent form is submitted, the sub-form does not
react.
A classic example for this type of sub-form is uploading an image. The subform
allows uploading a file and once the file is uploaded the image is shown as
well as a "Delete"/"Clear" button. The sub-form will store the image in the
session and when the main form is submitted it looks in the session for the
image.
This scenario was well supported in ``zope.formlib`` and also does not require
special support in ``z3c.form``. Let me show you, how this can be done.
In this example, we would like to describe a car and its owner:
>>> import zope.interface
>>> import zope.schema
>>> class IOwner(zope.interface.Interface):
... name = zope.schema.TextLine(title=u'Name')
... license = zope.schema.TextLine(title=u'License')
>>> class ICar(zope.interface.Interface):
... model = zope.schema.TextLine(title=u'Model')
... make = zope.schema.TextLine(title=u'Make')
... owner = zope.schema.Object(title=u'Owner', schema=IOwner)
Let's now implement the two interfaces and create instances, so that we can
create edit forms for it:
>>> class Owner(object):
... zope.interface.implements(IOwner)
... def __init__(self, name, license):
... self.name = name
... self.license = license
>>> class Car(object):
... zope.interface.implements(ICar)
... def __init__(self, model, make, owner):
... self.model = model
... self.make = make
... self.owner = owner
>>> me = Owner(u'Stephan Richter', u'MA-1231FW97')
>>> mycar = Car(u'Nissan', u'Sentra', me)
We define the owner sub-form as we would any other form. The only difference
is the template, which should not render a form-tag:
>>> import os
>>> from zope.app.pagetemplate import viewpagetemplatefile
>>> from z3c.form import form, field, tests
>>> templatePath = os.path.dirname(tests.__file__)
>>> class OwnerForm(form.EditForm):
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_owneredit.pt', templatePath)
... fields = field.Fields(IOwner)
... prefix = 'owner'
Next we define the car form, which has the owner form as a sub-form. The car
form also needs a special template, since it needs to render the sub-form at
some point. For the simplicity of this example, I have duplicated a lot of
template code here, but you can use your favorite template techniques, such as
METAL macros, viewlets, or pagelets to make better reuse of some code.
>>> class CarForm(form.EditForm):
... fields = field.Fields(ICar).select('model', 'make')
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_caredit.pt', templatePath)
... prefix = 'car'
... def update(self):
... self.owner = OwnerForm(self.context.owner, self.request)
... self.owner.update()
... super(CarForm, self).update()
Let's now instantiate the form and render it:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> print carForm.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="car-widgets-model">Model</label>
<input type="text" id="car-widgets-model" name="car.widgets.model"
class="text-widget required textline-field" value="Nissan" />
</div>
<div class="row">
<label for="car-widgets-make">Make</label>
<input type="text" id="car-widgets-make" name="car.widgets.make"
class="text-widget required textline-field" value="Sentra" />
</div>
<fieldset>
<legend>Owner</legend>
<div class="row">
<label for="owner-widgets-name">Name</label>
<input type="text" id="owner-widgets-name" name="owner.widgets.name"
class="text-widget required textline-field"
value="Stephan Richter" />
</div>
<div class="row">
<label for="owner-widgets-license">License</label>
<input type="text" id="owner-widgets-license"
name="owner.widgets.license"
class="text-widget required textline-field"
value="MA-1231FW97" />
</div>
<div class="action">
<input type="submit" id="owner-buttons-apply"
name="owner.buttons.apply"
class="submit-widget button-field"
value="Apply" />
</div>
</fieldset>
<div class="action">
<input type="submit" id="car-buttons-apply"
name="car.buttons.apply"
class="submit-widget button-field"
value="Apply" />
</div>
</form>
</body>
</html>
I can now submit the owner form, which should not submit any car changes I
might have made in the form:
>>> request = TestRequest(form={
... 'car.widgets.model': u'BMW',
... 'car.widgets.make': u'325',
... 'owner.widgets.name': u'Stephan Richter',
... 'owner.widgets.license': u'MA-97097A87',
... 'owner.buttons.apply': u'Apply'
... })
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> mycar.model
u'Nissan'
>>> mycar.make
u'Sentra'
>>> me.name
u'Stephan Richter'
>>> me.license
u'MA-97097A87'
Also, the form should say that the data of the owner has changed:
>>> print carForm.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="car-widgets-model">Model</label>
<input type="text" id="car-widgets-model"
name="car.widgets.model"
class="text-widget required textline-field"
value="BMW" />
</div>
<div class="row">
<label for="car-widgets-make">Make</label>
<input type="text" id="car-widgets-make"
name="car.widgets.make"
class="text-widget required textline-field"
value="325" />
</div>
<fieldset>
<legend>Owner</legend>
<i>Data successfully updated.</i>
<div class="row">
<label for="owner-widgets-name">Name</label>
<input type="text" id="owner-widgets-name"
name="owner.widgets.name"
class="text-widget required textline-field"
value="Stephan Richter" />
</div>
<div class="row">
<label for="owner-widgets-license">License</label>
<input type="text" id="owner-widgets-license"
name="owner.widgets.license"
class="text-widget required textline-field"
value="MA-97097A87" />
</div>
<div class="action">
<input type="submit" id="owner-buttons-apply"
name="owner.buttons.apply"
class="submit-widget button-field"
value="Apply" />
</div>
</fieldset>
<div class="action">
<input type="submit" id="car-buttons-apply"
name="car.buttons.apply"
class="submit-widget button-field"
value="Apply" />
</div>
</form>
</body>
</html>
The same is true the other way around as well. Submitting the overall form
does not submit the owner form:
>>> request = TestRequest(form={
... 'car.widgets.model': u'BMW',
... 'car.widgets.make': u'325',
... 'car.buttons.apply': u'Apply',
... 'owner.widgets.name': u'Claudia Richter',
... 'owner.widgets.license': u'MA-123403S2',
... })
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> mycar.model
u'BMW'
>>> mycar.make
u'325'
>>> me.name
u'Stephan Richter'
>>> me.license
u'MA-97097A87'
Class II: The logical unit
--------------------------
In this class of sub-forms, a sub-form is often just a collection of widgets
without any actions. Instead, the sub-form must be able to react to the
actions of the parent form. A good example of those types of sub-forms is
actually the example I chose above.
So let's redevelop our example above in a way that the owner sub-form is just
a logical unit that shares the action with its parent form. Initially, the
example does not look very different, except that we use ``EditSubForm`` as a
base class:
>>> from z3c.form import subform
>>> class OwnerForm(subform.EditSubForm):
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_subedit.pt', templatePath)
... fields = field.Fields(IOwner)
... prefix = 'owner'
The main form also is pretty much the same, except that a subform takes three
constructor arguments, the last one being the parent form:
>>> class CarForm(form.EditForm):
... fields = field.Fields(ICar).select('model', 'make')
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_caredit.pt', templatePath)
... prefix = 'car'
...
... def update(self):
... super(CarForm, self).update()
... self.owner = OwnerForm(self.context.owner, self.request, self)
... self.owner.update()
Rendering the form works as before:
>>> request = TestRequest()
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> print carForm.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="car-widgets-model">Model</label>
<input type="text" id="car-widgets-model"
name="car.widgets.model"
class="text-widget required textline-field"
value="BMW" />
</div>
<div class="row">
<label for="car-widgets-make">Make</label>
<input type="text" id="car-widgets-make"
name="car.widgets.make"
class="text-widget required textline-field"
value="325" />
</div>
<fieldset>
<legend>Owner</legend>
<div class="row">
<label for="owner-widgets-name">Name</label>
<input type="text" id="owner-widgets-name"
name="owner.widgets.name"
class="text-widget required textline-field"
value="Stephan Richter" />
</div>
<div class="row">
<label for="owner-widgets-license">License</label>
<input type="text" id="owner-widgets-license"
name="owner.widgets.license"
class="text-widget required textline-field"
value="MA-97097A87" />
</div>
</fieldset>
<div class="action">
<input type="submit" id="car-buttons-apply"
name="car.buttons.apply"
class="submit-widget button-field"
value="Apply" />
</div>
</form>
</body>
</html>
The interesting part of this setup is that the "Apply" button calls the action
handlers for both, the main and the sub-form:
>>> request = TestRequest(form={
... 'car.widgets.model': u'Ford',
... 'car.widgets.make': u'F150',
... 'car.buttons.apply': u'Apply',
... 'owner.widgets.name': u'Claudia Richter',
... 'owner.widgets.license': u'MA-991723FDG',
... })
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> mycar.model
u'Ford'
>>> mycar.make
u'F150'
>>> me.name
u'Claudia Richter'
>>> me.license
u'MA-991723FDG'
Let's now have a look at cases where an error happens. If an error occurs in
the parent form, the sub-form is still submitted:
>>> request = TestRequest(form={
... 'car.widgets.model': u'Volvo\n',
... 'car.widgets.make': u'450',
... 'car.buttons.apply': u'Apply',
... 'owner.widgets.name': u'Stephan Richter',
... 'owner.widgets.license': u'MA-991723FDG',
... })
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> mycar.model
u'Ford'
>>> mycar.make
u'F150'
>>> me.name
u'Stephan Richter'
>>> me.license
u'MA-991723FDG'
Let's look at the rendered form:
>>> print carForm.render()
<html>
<body>
<i>There were some errors.</i>
<ul>
<li>
Model: <div class="error">Constraint not satisfied</div>
</li>
</ul>
<form action=".">
<div class="row">
<b><div class="error">Constraint not satisfied</div>
</b><label for="car-widgets-model">Model</label>
<input type="text" id="car-widgets-model"
name="car.widgets.model"
class="text-widget required textline-field"
value="Volvo " />
</div>
<div class="row">
<label for="car-widgets-make">Make</label>
<input type="text" id="car-widgets-make"
name="car.widgets.make"
class="text-widget required textline-field"
value="450" />
</div>
<fieldset>
<legend>Owner</legend>
<i>Data successfully updated.</i>
<div class="row">
<label for="owner-widgets-name">Name</label>
<input type="text" id="owner-widgets-name"
name="owner.widgets.name"
class="text-widget required textline-field"
value="Stephan Richter" />
</div>
<div class="row">
<label for="owner-widgets-license">License</label>
<input type="text" id="owner-widgets-license"
name="owner.widgets.license"
class="text-widget required textline-field"
value="MA-991723FDG" />
</div>
</fieldset>
<div class="action">
<input type="submit" id="car-buttons-apply"
name="car.buttons.apply" class="submit-widget button-field"
value="Apply" />
</div>
</form>
</body>
</html>
Now, we know, we know. This might not be the behavior that *you* want. But
remember how we started this document. We started with the recognition that
there are many classes and policies surrounding subforms. So while this
package provides some sensible default behavior, it is not intended to be
comprehensive.
Let's now create an error in the sub-form, ensuring that an error message
occurs:
>>> request = TestRequest(form={
... 'car.widgets.model': u'Volvo',
... 'car.widgets.make': u'450',
... 'car.buttons.apply': u'Apply',
... 'owner.widgets.name': u'Claudia\n Richter',
... 'owner.widgets.license': u'MA-991723F12',
... })
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> mycar.model
u'Volvo'
>>> mycar.make
u'450'
>>> me.name
u'Stephan Richter'
>>> me.license
u'MA-991723FDG'
>>> print carForm.render()
<html>
...
<fieldset>
<legend>Owner</legend>
<i>There were some errors.</i>
<ul>
<li>
Name: <div class="error">Constraint not satisfied</div>
</li>
</ul>
...
</fieldset>
...
</html>
If the data did not change, it is also locally reported:
>>> request = TestRequest(form={
... 'car.widgets.model': u'Ford',
... 'car.widgets.make': u'F150',
... 'car.buttons.apply': u'Apply',
... 'owner.widgets.name': u'Stephan Richter',
... 'owner.widgets.license': u'MA-991723FDG',
... })
>>> carForm = CarForm(mycar, request)
>>> carForm.update()
>>> print carForm.render()
<html>
...
<fieldset>
<legend>Owner</legend>
<i>No changes were applied.</i>
...
</fieldset>
...
</html>
Final Note: With ``zope.formlib`` and ``zope.app.form`` people usually wrote
complex object widgets to handle objects within forms. We never considered
this a good way of programming, since one loses control over the layout too
easily.
Context-free subforms
---------------------
Ok, that was easy. But what about writing a form including a subform without a
context? Let's show how we can write a form without any context using the
sample above. Note, this sample form does not include actions which store the
form input. You can store the values like in any other forms using the forms
widget method ``self.widgets.extract()`` which will return the form and
subform input values.
>>> from z3c.form.interfaces import IWidgets
>>> class OwnerAddForm(form.EditForm):
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_owneredit.pt', templatePath)
... fields = field.Fields(IOwner)
... prefix = 'owner'
...
... def updateWidgets(self):
... self.widgets = zope.component.getMultiAdapter(
... (self, self.request, self.getContent()), IWidgets)
... self.widgets.ignoreContext = True
... self.widgets.update()
Next we define the car form, which has the owner form as a sub-form.
>>> class CarAddForm(form.EditForm):
... fields = field.Fields(ICar).select('model', 'make')
... template = viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_caredit.pt', templatePath)
... prefix = 'car'
...
... def updateWidgets(self):
... self.widgets = zope.component.getMultiAdapter(
... (self, self.request, self.getContent()), IWidgets)
... self.widgets.ignoreContext = True
... self.widgets.update()
...
... def update(self):
... self.owner = OwnerAddForm(None, self.request)
... self.owner.update()
... super(CarAddForm, self).update()
Let's now instantiate the form and render it. but first set up a simple
container which we can use for the add form context:
>>> class Container(object):
... """Simple context simulating a container."""
>>> container = Container()
Set up a test request:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
And render the form. As you can see, the widgets get rendered without any
*real* context.
>>> carForm = CarAddForm(container, request)
>>> carForm.update()
>>> print carForm.render()
<html>
<body>
<form action=".">
<div class="row">
<label for="car-widgets-model">Model</label>
<input type="text" id="car-widgets-model"
name="car.widgets.model"
class="text-widget required textline-field"
value="" />
</div>
<div class="row">
<label for="car-widgets-make">Make</label>
<input type="text" id="car-widgets-make"
name="car.widgets.make"
class="text-widget required textline-field"
value="" />
</div>
<fieldset>
<legend>Owner</legend>
<div class="row">
<label for="owner-widgets-name">Name</label>
<input type="text" id="owner-widgets-name"
name="owner.widgets.name"
class="text-widget required textline-field"
value="" />
</div>
<div class="row">
<label for="owner-widgets-license">License</label>
<input type="text" id="owner-widgets-license"
name="owner.widgets.license"
class="text-widget required textline-field"
value="" />
</div>
<div class="action">
<input type="submit" id="owner-buttons-apply"
name="owner.buttons.apply" class="submit-widget button-field"
value="Apply" />
</div>
</fieldset>
<div class="action">
<input type="submit" id="car-buttons-apply"
name="car.buttons.apply" class="submit-widget button-field"
value="Apply" />
</div>
</form>
</body>
</html>
Let's show how we can extract the input values of the form and the subform.
First give them some input:
>>> request = TestRequest(form={
... 'car.widgets.model': u'Ford',
... 'car.widgets.make': u'F150',
... 'owner.widgets.name': u'Stephan Richter',
... 'owner.widgets.license': u'MA-991723FDG',
... })
>>> carForm = CarAddForm(container, request)
>>> carForm.update()
Now get the form values. This is normally done in a action handler:
>>> carForm.widgets.extract()
({'model': u'Ford', 'make': u'F150'}, ())
>>> carForm.owner.widgets.extract()
({'name': u'Stephan Richter', 'license': u'MA-991723FDG'}, ())
==============
Field Managers
==============
One of the features in ``zope.formlib`` that works really well is the syntax
used to define the contents of the form. The formlib uses form fields, to
describe how the form should be put together. Since we liked this way of
working, this package offers this feature as well in a very similar way.
A field manager organizes all fields to be displayed within a form. Each field
is associated with additional meta-data. The simplest way to create a field
manager is to specify the schema from which to extract all fields.
Thus, the first step is to create a schema:
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... id = zope.schema.Int(
... title=u'Id',
... readonly=True)
...
... name = zope.schema.TextLine(
... title=u'Name')
...
... country = zope.schema.Choice(
... title=u'Country',
... values=(u'Germany', u'Switzerland', u'USA'),
... required=False)
We can now create the field manager:
>>> from z3c.form import field
>>> manager = field.Fields(IPerson)
Like all managers in this package, it provides the enumerable mapping API:
>>> manager['id']
<Field 'id'>
>>> manager['unknown']
Traceback (most recent call last):
...
KeyError: 'unknown'
>>> manager.get('id')
<Field 'id'>
>>> manager.get('unknown', 'default')
'default'
>>> 'id' in manager
True
>>> 'unknown' in manager
False
>>> manager.keys()
['id', 'name', 'country']
>>> [key for key in manager]
['id', 'name', 'country']
>>> manager.values()
[<Field 'id'>, <Field 'name'>, <Field 'country'>]
>>> manager.items()
[('id', <Field 'id'>),
('name', <Field 'name'>),
('country', <Field 'country'>)]
>>> len(manager)
3
You can also select the fields that you would like to have:
>>> manager = manager.select('name', 'country')
>>> manager.keys()
['name', 'country']
Changing the order is simply a matter of changing the selection order:
>>> manager = manager.select('country', 'name')
>>> manager.keys()
['country', 'name']
Selecting a field becomes a little bit more tricky when field names
overlap. For example, let's say that a person can be adapted to a pet:
>>> class IPet(zope.interface.Interface):
... id = zope.schema.TextLine(
... title=u'Id')
...
... name = zope.schema.TextLine(
... title=u'Name')
The pet field(s) can only be added to the fields manager with a prefix:
>>> manager += field.Fields(IPet, prefix='pet')
>>> manager.keys()
['country', 'name', 'pet.id', 'pet.name']
When selecting fields, this prefix has to be used:
>>> manager = manager.select('name', 'pet.name')
>>> manager.keys()
['name', 'pet.name']
However, sometimes it is tedious to specify the prefix together with the
field; for example here:
>>> manager = field.Fields(IPerson).select('name')
>>> manager += field.Fields(IPet, prefix='pet').select('pet.name', 'pet.id')
>>> manager.keys()
['name', 'pet.name', 'pet.id']
It is easier to specify the prefix as an afterthought:
>>> manager = field.Fields(IPerson).select('name')
>>> manager += field.Fields(IPet, prefix='pet').select(
... 'name', 'id', prefix='pet')
>>> manager.keys()
['name', 'pet.name', 'pet.id']
Alternatively, you can specify the interface:
>>> manager = field.Fields(IPerson).select('name')
>>> manager += field.Fields(IPet, prefix='pet').select(
... 'name', 'id', interface=IPet)
>>> manager.keys()
['name', 'pet.name', 'pet.id']
Sometimes it is easier to simply omit a set of fields instead of selecting all
the ones you want:
>>> manager = field.Fields(IPerson)
>>> manager = manager.omit('id')
>>> manager.keys()
['name', 'country']
Again, you can solve name conflicts using the full prefixed name, ...
>>> manager = field.Fields(IPerson).omit('country')
>>> manager += field.Fields(IPet, prefix='pet')
>>> manager.omit('pet.id').keys()
['id', 'name', 'pet.name']
using the prefix keyword argument, ...
>>> manager = field.Fields(IPerson).omit('country')
>>> manager += field.Fields(IPet, prefix='pet')
>>> manager.omit('id', prefix='pet').keys()
['id', 'name', 'pet.name']
or, using the interface:
>>> manager = field.Fields(IPerson).omit('country')
>>> manager += field.Fields(IPet, prefix='pet')
>>> manager.omit('id', interface=IPet).keys()
['id', 'name', 'pet.name']
You can also add two field managers together:
>>> manager = field.Fields(IPerson).select('name', 'country')
>>> manager2 = field.Fields(IPerson).select('id')
>>> (manager + manager2).keys()
['name', 'country', 'id']
Adding anything else to a field manager is not well defined:
>>> manager + 1
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'Fields' and 'int'
You also cannot make any additions that would cause a name conflict:
>>> manager + manager
Traceback (most recent call last):
...
ValueError: ('Duplicate name', 'name')
When creating a new form derived from another, you often want to keep existing
fields and add new ones. In order to not change the super-form class, you need
to copy the field manager:
>>> manager.keys()
['name', 'country']
>>> manager.copy().keys()
['name', 'country']
More on the Constructor
-----------------------
The constructor does not only accept schemas to be passed in; one can also
just pass in schema fields:
>>> field.Fields(IPerson['name']).keys()
['name']
However, the schema field has to have a name:
>>> email = zope.schema.TextLine(title=u'E-Mail')
>>> field.Fields(email)
Traceback (most recent call last):
...
ValueError: Field has no name
Adding a name helps:
>>> email.__name__ = 'email'
>>> field.Fields(email).keys()
['email']
Or, you can just pass in other field managers, which is the feature that the add
mechanism uses:
>>> field.Fields(manager).keys()
['name', 'country']
Last, but not least, the constructor also accepts form fields, which are used
by ``select()`` and ``omit()``:
>>> field.Fields(manager['name'], manager2['id']).keys()
['name', 'id']
If the constructor does not recognize any of the types above, it raises a
``TypeError`` exception:
>>> field.Fields(object())
Traceback (most recent call last):
...
TypeError: ('Unrecognized argument type', <object object at ...>)
Additionally, you can specify several keyword arguments in the field manager
constructor that are used to set up the fields:
* ``omitReadOnly``
When set to ``True`` all read-only fields are omitted.
>>> field.Fields(IPerson, omitReadOnly=True).keys()
['name', 'country']
* ``keepReadOnly``
Sometimes you want to keep a particular read-only field around, even though
in general you want to omit them. In this case you can specify the fields to
keep:
>>> field.Fields(
... IPerson, omitReadOnly=True, keepReadOnly=('id',)).keys()
['id', 'name', 'country']
* ``prefix``
Sets the prefix of the fields. This argument is passed on to each field.
>>> manager = field.Fields(IPerson, prefix='myform.')
>>> manager['myform.name']
<Field 'myform.name'>
* ``interface``
Usually the interface is inferred from the field itself. The interface is
used to determine whether an adapter must be looked up for a given
context.
But sometimes fields are generated in isolation to an interface or the
interface of the field is not the one you want. In this case you can specify
the interface:
>>> class IMyPerson(IPerson):
... pass
>>> manager = field.Fields(email, interface=IMyPerson)
>>> manager['email'].interface
<InterfaceClass __builtin__.IMyPerson>
* ``mode``
The mode in which the widget will be rendered. By default there are two
available, "input" and "display". When mode is not specified, "input" is
chosen.
>>> from z3c.form import interfaces
>>> manager = field.Fields(IPerson, mode=interfaces.DISPLAY_MODE)
>>> manager['country'].mode
'display'
Fields Widget Manager
---------------------
When a form (or any other widget-using view) is updated, one of the tasks is
to create the widgets. Traditionally, generating the widgets involved looking
at the form fields (or similar) of a form and generating the widgets using the
information of those specifications. This solution is good for the common
(about 85%) use cases, since it makes writing new forms very simple and allows
a lot of control at a class-definition level.
It has, however, its limitations. It does not, for example, allow for
customization without rewriting a form. This can range from omitting fields on
a particular form to generically adding a new widget to the form, such as an
"object name" button on add forms. This package solves this issue by providing
a widget manager, which is responsible providing the widgets for a particular
view.
The default widget manager for forms is able to look at a form's field
definitions and create widgets for them. Thus, let's create a schema first:
>>> import zope.interface
>>> import zope.schema
>>> class LastNameTooShort(zope.schema.interfaces.ValidationError):
... """The last name is too short."""
>>> class IPerson(zope.interface.Interface):
... id = zope.schema.TextLine(
... title=u'ID',
... description=u"The person's ID.",
... readonly=True,
... required=True)
...
... lastName = zope.schema.TextLine(
... title=u'Last Name',
... description=u"The person's last name.",
... default=u'',
... required=True)
...
... firstName = zope.schema.TextLine(
... title=u'First Name',
... description=u"The person's first name.",
... default=u'-- unknown --',
... required=False)
...
... @zope.interface.invariant
... def twiceAsLong(person):
... if len(person.lastName) >= 2 * len(person.firstName):
... raise LastNameTooShort()
Next we need a form that specifies the fields to be added:
>>> from z3c.form import field
>>> class PersonForm(object):
... prefix = 'form.'
... fields = field.Fields(IPerson)
>>> personForm = PersonForm()
For more details on how to define fields within a form, see ``form.txt``. We
can now create the fields widget manager. Its discriminators are the form for
which the widgets are created, the request, and the context that is being
manipulated. In the simplest case the context is ``None`` and ignored, as it
is true for an add form.
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> context = object()
>>> manager = field.FieldWidgets(personForm, request, context)
>>> manager.ignoreContext = True
Widget Mapping
~~~~~~~~~~~~~~
The main responsibility of the manager is to provide the ``IEnumerableMapping``
interface and an ``update()`` method. Initially the mapping, going from widget
id to widget value, is empty:
>>> from zope.interface.common.mapping import IEnumerableMapping
>>> IEnumerableMapping.providedBy(manager)
True
>>> manager.keys()
[]
Only by "updating" the manager, will the widgets become available; before we can
use the update method, however, we have to register the ``IFieldWidget`` adapter
for the ``ITextLine`` field:
>>> from z3c.form import interfaces, widget
>>> @zope.component.adapter(zope.schema.TextLine, TestRequest)
... @zope.interface.implementer(interfaces.IFieldWidget)
... def TextFieldWidget(field, request):
... return widget.FieldWidget(field, widget.Widget(request))
>>> zope.component.provideAdapter(TextFieldWidget)
>>> from z3c.form import converter
>>> zope.component.provideAdapter(converter.FieldDataConverter)
>>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)
>>> manager.update()
Other than usual mappings in Python, the widget manager's widgets are always
in a particular order:
>>> manager.keys()
['id', 'lastName', 'firstName']
Let's make sure that all enumerable mapping functions work correctly:
>>> manager['lastName']
<Widget 'form.widgets.lastName'>
>>> manager['unknown']
Traceback (most recent call last):
...
KeyError: 'unknown'
>>> manager.get('lastName')
<Widget 'form.widgets.lastName'>
>>> manager.get('unknown', 'default')
'default'
>>> 'lastName' in manager
True
>>> 'unknown' in manager
False
>>> [key for key in manager]
['id', 'lastName', 'firstName']
>>> manager.values()
[<Widget 'form.widgets.id'>,
<Widget 'form.widgets.lastName'>,
<Widget 'form.widgets.firstName'>]
>>> manager.items()
[('id', <Widget 'form.widgets.id'>),
('lastName', <Widget 'form.widgets.lastName'>),
('firstName', <Widget 'form.widgets.firstName'>)]
>>> len(manager)
3
It is also possible to delete widgets from the manager:
>>> del manager['firstName']
>>> len(manager)
2
>>> manager.values()
[<Widget 'form.widgets.id'>, <Widget 'form.widgets.lastName'>]
>>> manager.keys()
['id', 'lastName']
>>> manager.items()
[('id', <Widget 'form.widgets.id'>),
('lastName', <Widget 'form.widgets.lastName'>)]
Note that deleting a non-existent widget causes a ``KeyError`` to be raised:
>>> del manager['firstName']
Traceback (most recent call last):
...
KeyError: 'firstName'
Properties of widgets within a manager
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a widget is added to the widget manager, it is located:
>>> lname = manager['lastName']
>>> lname.__name__
'lastName'
>>> lname.__parent__
<z3c.form.field.FieldWidgets object at ...>
All widgets created by this widget manager are context aware:
>>> interfaces.IContextAware.providedBy(lname)
True
>>> lname.context is context
True
Determination of the widget mode
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, all widgets will also assume the mode of the manager:
>>> manager['lastName'].mode
'input'
>>> manager.mode = interfaces.DISPLAY_MODE
>>> manager.update()
>>> manager['lastName'].mode
'display'
The exception is when some fields specifically desire a different mode. In the
first case, all "readonly" fields will be shown in display mode:
>>> manager.mode = interfaces.INPUT_MODE
>>> manager.update()
>>> manager['id'].mode
'display'
An exception is made when the flag, "ignoreReadonly" is set:
>>> manager.ignoreReadonly = True
>>> manager.update()
>>> manager['id'].mode
'input'
In the second case, the last name will inherit the mode from the widget
manager, while the first name will want to use a display widget:
>>> personForm.fields = field.Fields(IPerson).select('lastName')
>>> personForm.fields += field.Fields(
... IPerson, mode=interfaces.DISPLAY_MODE).select('firstName')
>>> manager.mode = interfaces.INPUT_MODE
>>> manager.update()
>>> manager['lastName'].mode
'input'
>>> manager['firstName'].mode
'display'
In a third case, the widget will be shown in display mode, if the attribute of
the context is not writable. Clearly this can never occur in add forms, since
there the context is ignored, but is an important use case in edit forms.
Thus, we need an implementation of the ``IPerson`` interface including some
security declarations:
>>> from zope.security import checker
>>> class Person(object):
... zope.interface.implements(IPerson)
...
... def __init__(self, firstName, lastName):
... self.id = firstName[0].lower() + lastName.lower()
... self.firstName = firstName
... self.lastName = lastName
>>> PersonChecker = checker.Checker(
... get_permissions = {'id': checker.CheckerPublic,
... 'firstName': checker.CheckerPublic,
... 'lastName': checker.CheckerPublic},
... set_permissions = {'firstName': 'test.Edit',
... 'lastName': checker.CheckerPublic}
... )
>>> srichter = checker.ProxyFactory(
... Person(u'Stephan', u'Richter'), PersonChecker)
In this case the last name is always editable, but for the first name the user
will need the edit ("test.Edit") permission.
We also need to register the data manager and setup a new security policy:
>>> from z3c.form import datamanager
>>> zope.component.provideAdapter(datamanager.AttributeField)
>>> from zope.security import management
>>> from z3c.form import testing
>>> management.endInteraction()
>>> newPolicy = testing.SimpleSecurityPolicy()
>>> oldpolicy = management.setSecurityPolicy(newPolicy)
>>> management.newInteraction()
Now we can create the widget manager:
>>> personForm = PersonForm()
>>> request = TestRequest()
>>> manager = field.FieldWidgets(personForm, request, srichter)
After updating the widget manager, the fields are available as widgets, the
first name being in display and the last name is input mode:
>>> manager.update()
>>> manager['id'].mode
'display'
>>> manager['firstName'].mode
'display'
>>> manager['lastName'].mode
'input'
However, explicitly overriding the mode in the field declaration overrides
this selection for you:
>>> personForm.fields['firstName'].mode = interfaces.INPUT_MODE
>>> manager.update()
>>> manager['id'].mode
'display'
>>> manager['firstName'].mode
'input'
>>> manager['lastName'].mode
'input'
Data extraction and validation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Besides managing widgets, the widget manager also controls the process of
extracting and validating extracted data. Let's start with the validation
first, which only validates the data as a whole, assuming each individual
value being already validated.
Before we can use the method, we have to register a "manager validator":
>>> from z3c.form import validator
>>> zope.component.provideAdapter(validator.InvariantsValidator)
>>> personForm.fields = field.Fields(IPerson)
>>> manager.update()
>>> manager.validate(
... {'firstName': u'Stephan', 'lastName': u'Richter'})
()
The result of this method is a tuple of errors that occurred during the
validation. An empty tuple means the validation succeeded. Let's now make the
validation fail:
>>> errors = manager.validate(
... {'firstName': u'Stephan', 'lastName': u'Richter-Richter'})
>>> [error.doc() for error in errors]
['The last name is too short.']
A special case occurs when the schema fields are not associated with an
interface:
>>> name = zope.schema.TextLine(__name__='name')
>>> class PersonNameForm(object):
... prefix = 'form.'
... fields = field.Fields(name)
>>> personNameForm = PersonNameForm()
>>> manager = field.FieldWidgets(personNameForm, request, context)
In this case, the widget manager's ``validate()`` method should simply ignore
the field and not try to look up any invariants:
>>> manager.validate({'name': u'Stephan'})
()
Let's now have a look at the widget manager's ``extract()``, which returns a
data dictionary and the collection of errors. Before we can validate, we have
to register a validator for the widget:
>>> zope.component.provideAdapter(validator.SimpleFieldValidator)
When all goes well, the data dictionary is complete and the error collection
empty:
>>> request = TestRequest(form={
... 'form.widgets.id': u'srichter',
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.lastName': u'Richter'})
>>> manager = field.FieldWidgets(personForm, request, context)
>>>
