An advanced form and widget framework for Zope 3
Project description
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.
- ``testing.txt`` [informative]
The ``testing`` module provides helper functions that make it easier to tet
form-based code in unit tests. It also provides components that simplify
testing in testbrowser and Selenium.
- ``object-caveat.txt`` [informative]
Explains the current problems of ObjectWidget.
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 are 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 z3c.form import ptcompat as viewpagetemplatefile
>>> from z3c.form import tests
>>> def addTemplate(form):
... form.template = viewpagetemplatefile.bind_template(
... viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_edit.pt', os.path.dirname(tests.__file__)), form)
>>> addTemplate(addForm)
Let's now render the page:
>>> print addForm.render()
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<div class="row">
<label for="form-widgets-id">ID</label>
<input type="text" id="form-widgets-id"
name="form.widgets.id"
class="text-widget required textline-field"
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 xmlns="http://www.w3.org/1999/xhtml">
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
</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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
</html>
The form also provides a label for rendering a required info. This required
info depends by default on the given requiredInfo label and if at least one
field is required:
>>> addForm.requiredInfo
u'<span class="required">*</span>&ndash; required'
If we set the labelRequired to None, we do not get a requiredInfo label:
>>> addForm.labelRequired = None
>>> addForm.requiredInfo is None
True
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 testing.render(addForm, './/xmlns:div[2][@class="row"]')
<div class="row">
<label for="form-widgets-name">Full Name</label>
<input class="text-widget required textline-field"
id="form-widgets-name" name="form.widgets.name" type="text" 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 testing.render(addForm, './/xmlns:div[@class="action"]')
<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 xmlns="http://www.w3.org/1999/xhtml">
<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 are 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 xmlns="http://www.w3.org/1999/xhtml">
<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 testing.render(editForm, './/xmlns:i')
<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...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 testing.render(editForm, './/xmlns:i')
<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 testing.render(editForm, './/xmlns:i')
<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 xmlns="http://www.w3.org/1999/xhtml">
<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 are 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 xmlns="http://www.w3.org/1999/xhtml">
<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 testing.render(myEdit, './/xmlns:input[@id="form-widgets-name"]')
<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 testing.render(hiddenEdit, './/xmlns:input[@id="form-widgets-age"]')
<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)
Applying Changes
----------------
When applying the data of a form to a content component, the function
``applyChanges()`` is called. It simply iterates through the fields of the
form and uses the data managers to store the values. The output of the
function is a list of changes:
>>> roger = Person(u'roger', u'Roger')
>>> roger
<Person u'Roger'>
>>> class BaseForm(form.Form):
... fields = field.Fields(IPerson).select('name')
>>> myForm = BaseForm(roger, TestRequest())
>>> form.applyChanges(myForm, roger, {'name': u'Roger Ineichen'})
{<InterfaceClass __builtin__.IPerson>: ['name']}
>>> roger
<Person u'Roger Ineichen'>
When a field is missing from the data, it is simply skipped:
>>> form.applyChanges(myForm, roger, {})
{}
If the new and old value are identical, storing the data is skipped as well:
>>> form.applyChanges(myForm, roger, {'name': u'Roger Ineichen'})
{}
In some cases the data converter for a field-widget pair returns the
``NOT_CHANGED`` value. In this case, the field is skipped as well:
>>> form.applyChanges(myForm, roger, {'name': interfaces.NOT_CHANGED})
{}
>>> roger
<Person u'Roger Ineichen'>
Refreshing actions
------------------
Sometimes, it's useful to update actions again after executing them,
because some conditions could have changed. For example, imagine
we have a sequence edit form that has a delete button. We don't
want to show delete button when the sequence is empty. The button
condition would handle this, but what if the sequence becomes empty
as a result of execution of the delete action that was available?
In that case we want to refresh our actions to new conditions to make
our delete button not visible anymore. The ``refreshActions`` form
variable is intended to handle this case.
Let's create a simple form with an action that clears our context
sequence.
>>> class SequenceForm(form.Form):
...
... @button.buttonAndHandler(u'Empty', condition=lambda form:bool(form.context))
... def handleEmpty(self, action):
... self.context[:] = []
... self.refreshActions = True
First, let's illustrate simple cases, when no button is pressed.
The button will be available when context is not empty.
>>> context = [1, 2, 3, 4]
>>> request = TestRequest()
>>> myForm = SequenceForm(context, request)
>>> myForm.update()
>>> addTemplate(myForm)
>>> print testing.render(myForm, './/xmlns:div[@class="action"]')
<div class="action">
<input type="submit" id="form-buttons-empty" name="form.buttons.empty"
class="submit-widget button-field" value="Empty" />
</div>
The button will not be available when the context is empty.
>>> context = []
>>> request = TestRequest()
>>> myForm = SequenceForm(context, request)
>>> myForm.update()
>>> addTemplate(myForm)
>>> print testing.render(myForm, './/xmlns:form')
<form action=".">
</form>
Now, the most interesting case when context is not empty, but becomes
empty as a result of pressing the "empty" button. We set the
``refreshActions`` flag in the action handler, so our actions should
be updated to new conditions.
>>> context = [1, 2, 3, 4, 5]
>>> request = TestRequest(form={
... 'form.buttons.empty': u'Empty'}
... )
>>> myForm = SequenceForm(context, request)
>>> myForm.update()
>>> addTemplate(myForm)
>>> print testing.render(myForm, './/xmlns:form')
<form action=".">
</form>
Integration tests
-----------------
Identifying the different forms can be important if it comes to layout
template lookup. Let's 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.TextLine(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 z3c.form.ptcompat 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(
... '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()
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<div class="row">
<label for="form-widgets-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>
<fieldset>
<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>
</fieldset>
<fieldset>
<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 textline-field"
value="" />
</div>
</fieldset>
<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 testing.render(add, './/xmlns:i')
<i>There were some errors.</i>
>>> print testing.render(add, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
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 testing.render(add, './/xmlns:i')
<i>There were some errors.</i>
>>> print testing.render(add, './/xmlns:ul[1]')
<ul>
<li>
Last Name:
<div class="error">Required input is missing.</div>
</li>
</ul>
<ul>
<li>
Address:
<div class="error">Required input is missing.</div>
</li>
</ul>
>>> print testing.render(add, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
Let's now successfully complete the add form.
>>> from zope.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
u'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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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>
<fieldset>
<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>
</fieldset>
<fieldset>
<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 textline-field"
value="2005" />
</div>
</fieldset>
<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 testing.render(edit, './/xmlns:i')
<i>There were some errors.</i>
>>> print testing.render(edit, './/xmlns:ul')
<ul>
<li>
Address:
<div class="error">Required input is missing.</div>
</li>
</ul>
>>> print testing.render(edit, './/xmlns:fieldset/xmlns:ul')
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
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 testing.render(edit, './/xmlns:i')
<i>Data successfully updated.</i>
and the data are 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
u'2006'
Let's look at the event:
>>> event = eventlog[-1]
>>> event
<zope...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')
Group form as instance
----------------------
It is also possible to use group instances in forms. Let's setup our previous
form and assing a group instance:
>>> class RegistrationEditForm(group.GroupForm, form.EditForm):
... fields = field.Fields(IVehicleRegistration).select(
... 'firstName', 'lastName')
...
... template = ViewPageTemplateFile(
... 'simple_groupedit.pt', os.path.dirname(tests.__file__))
>>> request = testing.TestRequest()
>>> edit = RegistrationEditForm(reg, request)
Instanciate the form and use a group class and a group instance:
>>> carGroupInstance = CarGroup(edit.context, request, edit)
>>> edit.groups = (LicenseGroup, carGroupInstance)
>>> edit.update()
>>> print edit.render()
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<div class="row">
<label for="form-widgets-firstName">First Name</label>
<input id="form-widgets-firstName"
name="form.widgets.firstName"
class="text-widget required textline-field"
value="Stephan" type="text" />
</div>
<div class="row">
<label for="form-widgets-lastName">Last Name</label>
<input id="form-widgets-lastName"
name="form.widgets.lastName"
class="text-widget required textline-field"
value="Richter" type="text" />
</div>
<fieldset>
<legend>License</legend>
<div class="row">
<label for="form-widgets-license">License</label>
<input id="form-widgets-license"
name="form.widgets.license"
class="text-widget required textline-field"
value="MA 4038765" type="text" />
</div>
<div class="row">
<label for="form-widgets-address">Address</label>
<input id="form-widgets-address"
name="form.widgets.address"
class="text-widget required textline-field"
value="11 Main St, Maynard, MA" type="text" />
</div>
</fieldset>
<fieldset>
<legend>Car</legend>
<div class="row">
<label for="form-widgets-model">Model</label>
<input id="form-widgets-model" name="form.widgets.model"
class="text-widget required textline-field"
value="Ford" type="text" />
</div>
<div class="row">
<label for="form-widgets-make">Make</label>
<input id="form-widgets-make" name="form.widgets.make"
class="text-widget required textline-field"
value="F150" type="text" />
</div>
<div class="row">
<label for="form-widgets-year">Year</label>
<input id="form-widgets-year" name="form.widgets.year"
class="text-widget required textline-field"
value="2006" type="text" />
</div>
</fieldset>
<div class="action">
<input id="form-buttons-apply" name="form.buttons.apply"
class="submit-widget button-field" value="Apply"
type="submit" />
</div>
</form>
</body>
</html>
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.TextLine(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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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 textline-field" value="2005" />
</div>
<fieldset>
<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>
</fieldset>
<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 testing.render(edit, './/xmlns:i')
<i>Data successfully updated.</i>
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
u'2005'
Nested Groups
-------------
The group can contains groups. Let's adapt the previous RegistrationEditForm:
>>> class OwnerGroup(group.Group):
... label = u'Owner'
... fields = field.Fields(IVehicleOwner, prefix='owner')
...
... def getContent(self):
... return self.context.owner
>>> class VehicleRegistrationGroup(group.Group):
... label = u'Registration'
... fields = field.Fields(IVehicleRegistration).omit(
... 'owner')
... groups = (OwnerGroup,)
...
... template = ViewPageTemplateFile(
... 'simple_groupedit.pt', os.path.dirname(tests.__file__))
>>> class RegistrationEditForm(group.GroupForm, form.EditForm):
... groups = (VehicleRegistrationGroup,)
...
... template = ViewPageTemplateFile(
... 'simple_nested_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()
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 testing.render(edit, './/xmlns:i')
<i>Data successfully updated.</i>
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
u'2005'
So what happens, if errors happen inside a nested group? Let's use an empty
invllaid object for the test missing input errors:
>>> reg = VehicleRegistration(owner=VehicleOwner())
>>> request = testing.TestRequest(form={
... 'form.widgets.owner.firstName': u'',
... 'form.widgets.owner.lastName': u'',
... 'form.widgets.license': u'',
... 'form.widgets.address': u'',
... 'form.widgets.model': u'',
... 'form.widgets.make': u'',
... 'form.widgets.year': u'',
... 'form.buttons.apply': u'Apply'
... })
>>> edit = RegistrationEditForm(reg, request)
>>> edit.update()
>>> data, errors = edit.extractData()
>>> print testing.render(edit, './/xmlns:i')
<i >There were some errors.</i>
>>> print testing.render(edit, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
License:
<div class="error">Required input is missing.</div>
</li>
<li>
Address:
<div class="error">Required input is missing.</div>
</li>
<li>
Model:
<div class="error">Required input is missing.</div>
</li>
<li>
Make:
<div class="error">Required input is missing.</div>
</li>
<li>
Year:
<div class="error">Required input is missing.</div>
</li>
</ul>
<ul>
<li>
First Name:
<div class="error">Required input is missing.</div>
</li>
<li>
Last Name:
<div class="error">Required input is missing.</div>
</li>
</ul>
Group instance in nested group
------------------------------
Let's also test if the Group class can handle group objects as instances:
>>> 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)
>>> vrg = VehicleRegistrationGroup(edit.context, request, edit)
>>> ownerGroup = OwnerGroup(edit.context, request, edit)
Now build the group instance object chain:
>>> vrg.groups = (ownerGroup,)
>>> edit.groups = (vrg,)
Also use refreshActions whihc is not needed but will make coverage this
additional line of code in the update method:
>>> edit.refreshActions = True
Update and render:
>>> edit.update()
>>> print edit.render()
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<fieldset>
<legend>Registration</legend>
<div class="row">
<label for="form-widgets-license">License</label>
<input id="form-widgets-license"
name="form.widgets.license"
class="text-widget required textline-field"
value="MA 40387" type="text" />
</div>
<div class="row">
<label for="form-widgets-address">Address</label>
<input id="form-widgets-address"
name="form.widgets.address"
class="text-widget required textline-field"
value="10 Main St, Maynard, MA" type="text" />
</div>
<div class="row">
<label for="form-widgets-model">Model</label>
<input id="form-widgets-model" name="form.widgets.model"
class="text-widget required textline-field"
value="BMW" type="text" />
</div>
<div class="row">
<label for="form-widgets-make">Make</label>
<input id="form-widgets-make" name="form.widgets.make"
class="text-widget required textline-field"
value="325" type="text" />
</div>
<div class="row">
<label for="form-widgets-year">Year</label>
<input id="form-widgets-year" name="form.widgets.year"
class="text-widget required textline-field"
value="2005" type="text" />
</div>
<fieldset>
<legend>Owner</legend>
<div class="row">
<label for="form-widgets-owner-firstName">First Name</label>
<input id="form-widgets-owner-firstName"
name="form.widgets.owner.firstName"
class="text-widget required textline-field"
value="Stephan" type="text" />
</div>
<div class="row">
<label for="form-widgets-owner-lastName">Last Name</label>
<input id="form-widgets-owner-lastName"
name="form.widgets.owner.lastName"
class="text-widget required textline-field"
value="Richter" type="text" />
</div>
</fieldset>
</fieldset>
<div class="action">
<input id="form-buttons-apply" name="form.buttons.apply"
class="submit-widget button-field" value="Apply"
type="submit" />
</div>
</form>
</body>
</html>
Now test the error handling if just one missing value is given in a group:
>>> request = testing.TestRequest(form={
... 'form.widgets.owner.firstName': u'Paul',
... 'form.widgets.owner.lastName': u'',
... '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)
>>> vrg = VehicleRegistrationGroup(edit.context, request, edit)
>>> ownerGroup = OwnerGroup(edit.context, request, edit)
>>> vrg.groups = (ownerGroup,)
>>> edit.groups = (vrg,)
>>> edit.update()
>>> data, errors = edit.extractData()
>>> print testing.render(edit, './/xmlns:i')
<i >There were some errors.</i>
>>> print testing.render(edit, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
Last Name:
<div class="error">Required input is missing.</div>
</li>
</ul>
Just check whether we fully support the interface:
>>> from z3c.form import interfaces
>>> from zope.interface.verify import verifyClass
>>> verifyClass(interfaces.IGroup, group.Group)
True
=========
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 z3c.form.ptcompat import ViewPageTemplateFile
>>> from z3c.form import form, field, tests
>>> templatePath = os.path.dirname(tests.__file__)
>>> class OwnerForm(form.EditForm):
... template = 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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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 xmlns="http://www.w3.org/1999/xhtml">
<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(
... '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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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 xmlns="http://www.w3.org/1999/xhtml">
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
<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(
... '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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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'
* ``ignoreContext``
While the ``ignoreContext`` flag is usually set on the form, it is sometimes
desirable to set the flag for a particular field.
>>> manager = field.Fields(IPerson)
>>> manager['country'].ignoreContext
>>> manager = field.Fields(IPerson, ignoreContext=True)
>>> manager['country'].ignoreContext
True
>>> manager = field.Fields(IPerson, ignoreContext=False)
>>> manager['country'].ignoreContext
False
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']
As you can see, if we call update twice, we still get the same amount and
order of keys:
>>> manager.update()
>>> 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'
Required fields
---------------
There is a flag for required fields. This flag get set if at least one field
is required. This let us render a required info legend in forms if required
fields get used.
>>> manager.hasRequiredFields
True
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)
>>> manager.ignoreContext = True
>>> manager.update()
>>> from zope.testing.doctestunit import pprint
>>> pprint(manager.extract())
({'firstName': u'Stephan', 'lastName': u'Richter'}, ())
Since all errors are immediately converted to error view snippets, we have to
provide the adapter from a validation error to an error view snippet first:
>>> from z3c.form import error
>>> zope.component.provideAdapter(error.ErrorViewSnippet)
Let's now cause a widget-level error by not submitting the required last
name:
>>> request = TestRequest(form={
... 'form.widgets.firstName': u'Stephan', 'form.widgets.id': u'srichter'})
>>> manager = field.FieldWidgets(personForm, request, context)
>>> manager.ignoreContext = True
>>> manager.update()
>>> manager.extract()
({'firstName': u'Stephan'}, (<ErrorViewSnippet for RequiredMissing>,))
Finally, let's ensure that invariant failures are also caught:
>>> request = TestRequest(form={
... 'form.widgets.id': u'srichter',
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.lastName': u'Richter-Richter'})
>>> manager = field.FieldWidgets(personForm, request, context)
>>> manager.ignoreContext = True
>>> manager.update()
>>> data, errors = manager.extract()
>>> errors[0].error.doc()
'The last name is too short.'
Note that the errors coming from invariants are all error view snippets as
well, just as it is the case for field-specific validation errors. And that's
really all there is!
By default, the ``extract()`` method not only returns the errors that it
catches, but also sets them on individual widgets and on the manager:
>>> manager.errors
(<ErrorViewSnippet for LastNameTooShort>,)
This behavior can be turned off. To demonstrate, let's make a new request that
causes a widget-level error:
>>> request = TestRequest(form={
... 'form.widgets.firstName': u'Stephan', 'form.widgets.id': u'srichter'})
>>> manager = field.FieldWidgets(personForm, request, context)
>>> manager.ignoreContext = True
>>> manager.update()
We have to set the setErrors property to False before calling extract,
we still get the same result from the method call, ...
>>> manager.setErrors = False
>>> manager.extract()
({'firstName': u'Stephan'}, (<ErrorViewSnippet for RequiredMissing>,))
but there are no side effects on the manager and the widgets:
>>> manager.errors
()
>>> manager['lastName'].error is None
True
Customization of Ignoring the Context
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Note that you can also manually control ignoring the context per field.
>>> class CustomPersonForm(object):
... prefix = 'form.'
... fields = field.Fields(IPerson).select('id')
... fields += field.Fields(IPerson, ignoreContext=True).select(
... 'firstName', 'lastName')
>>> customPersonForm = CustomPersonForm()
Let's now create a manager and update it:
>>> customManager = field.FieldWidgets(customPersonForm, request, context)
>>> customManager.update()
>>> customManager['id'].ignoreContext
False
>>> customManager['firstName'].ignoreContext
True
>>> customManager['lastName'].ignoreContext
True
Fields -- Custom Widget Factories
---------------------------------
It is possible to declare custom widgets for fields within the field's
declaration.
Let's have a look at the default form first. Initially, the standard
registered widgets are used:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.update()
>>> manager['firstName']
<Widget 'form.widgets.firstName'>
Now we would like to have our own custom input widget:
>>> class CustomInputWidget(widget.Widget):
... pass
>>> def CustomInputWidgetFactory(field, request):
... return widget.FieldWidget(field, CustomInputWidget(request))
It can be simply assigned as follows:
>>> personForm.fields['firstName'].widgetFactory = CustomInputWidgetFactory
>>> personForm.fields['lastName'].widgetFactory = CustomInputWidgetFactory
Now this widget should be used instead of the registered default one:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.update()
>>> manager['firstName']
<CustomInputWidget 'form.widgets.firstName'>
In the background the widget factory assignment really just registered the
default factory in the ``WidgetFactories`` object, which manages the
custom widgets for all modes. Now all modes show this input widget:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.mode = interfaces.DISPLAY_MODE
>>> manager.update()
>>> manager['firstName']
<CustomInputWidget 'form.widgets.firstName'>
However, we can also register a specific widget for the display mode:
>>> class CustomDisplayWidget(widget.Widget):
... pass
>>> def CustomDisplayWidgetFactory(field, request):
... return widget.FieldWidget(field, CustomDisplayWidget(request))
>>> personForm.fields['firstName']\
... .widgetFactory[interfaces.DISPLAY_MODE] = CustomDisplayWidgetFactory
>>> personForm.fields['lastName']\
... .widgetFactory[interfaces.DISPLAY_MODE] = CustomDisplayWidgetFactory
Now the display mode should produce the custom display widget, ...
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.mode = interfaces.DISPLAY_MODE
>>> manager.update()
>>> manager['firstName']
<CustomDisplayWidget 'form.widgets.firstName'>
>>> manager['lastName']
<CustomDisplayWidget 'form.widgets.lastName'>
... while the input mode still shows the default custom input widget
on the ``lastName`` field but not on the ``firstName`` field since we
don't have the ``test.Edit`` permission:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.mode = interfaces.INPUT_MODE
>>> manager.update()
>>> manager['firstName']
<CustomDisplayWidget 'form.widgets.firstName'>
>>> manager['lastName']
<CustomInputWidget 'form.widgets.lastName'>
The widgets factories component,
>>> factories = personForm.fields['firstName'].widgetFactory
>>> factories
{'display': <function CustomDisplayWidgetFactory at ...>}
is pretty much a standard dictionary that also manages a default value:
>>> factories.default
<function CustomInputWidgetFactory at ...>
When getting a value for a key, if the key is not found, the default is
returned:
>>> factories.keys()
['display']
>>> factories[interfaces.DISPLAY_MODE]
<function CustomDisplayWidgetFactory at ...>
>>> factories[interfaces.INPUT_MODE]
<function CustomInputWidgetFactory at ...>
>>> factories.get(interfaces.DISPLAY_MODE)
<function CustomDisplayWidgetFactory at ...>
>>> factories.get(interfaces.INPUT_MODE)
<function CustomInputWidgetFactory at ...>
If no default is specified,
>>> factories.default = None
then the dictionary behaves as usual:
>>> factories[interfaces.DISPLAY_MODE]
<function CustomDisplayWidgetFactory at ...>
>>> factories[interfaces.INPUT_MODE]
Traceback (most recent call last):
...
KeyError: 'input'
>>> factories.get(interfaces.DISPLAY_MODE)
<function CustomDisplayWidgetFactory at ...>
>>> factories.get(interfaces.INPUT_MODE)
=======
Buttons
=======
Buttons are a method to declare actions for a form. Like fields describe
widgets within a form, buttons describe actions. The symmetry goes even
further; like fields, buttons are schema fields within schema. When the form
is instantiated and updated, the buttons are converted to actions.
>>> from z3c.form import button
Schema Defined Buttons
----------------------
Let's now create a schema that describes the buttons of a form. Having button
schemas allows one to more easily reuse button declarations and to group them
logically. ``Button`` objects are just a simple extension to ``Field``
objects, so they behave identical within a schema:
>>> import zope.interface
>>> class IButtons(zope.interface.Interface):
... apply = button.Button(title=u'Apply')
... cancel = button.Button(title=u'Cancel')
In reality, only the title and name is relevant. Let's now create a form that
provides those buttons.
>>> from z3c.form import interfaces
>>> class Form(object):
... zope.interface.implements(
... interfaces.IButtonForm, interfaces.IHandlerForm)
... buttons = button.Buttons(IButtons)
... prefix = 'form'
...
... @button.handler(IButtons['apply'])
... def apply(self, action):
... print 'successfully applied'
...
... @button.handler(IButtons['cancel'])
... def cancel(self, action):
... self.request.response.redirect('index.html')
Let's now create an action manager for the button manager in the form. To do
that we first need a request and a form instance:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> form = Form()
We also have to register a button action factory for the buttons:
>>> zope.component.provideAdapter(
... button.ButtonAction, provides=interfaces.IButtonAction)
Action managers are instantiated using the form, request, and
context/content. A special button-action-manager implementation is available
in the ``button`` package:
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
Once the action manager is updated, the buttons should be available as
actions:
>>> actions.keys()
['apply', 'cancel']
>>> actions['apply']
<ButtonAction 'form.buttons.apply' u'Apply'>
It is possible to customize how a button is transformed into an action
by registering an adapter for the request and the button that provides
``IButtonAction``.
>>> import zope.component
>>> from zope.publisher.interfaces.browser import IBrowserRequest
>>> class CustomButtonAction(button.ButtonAction):
... """Custom Button Action Class."""
>>> zope.component.provideAdapter(
... CustomButtonAction, provides=interfaces.IButtonAction)
Now if we rerun update we will get this other ButtonAction
implementation. Note, there are two strategies what now could happen. We can
remove the existing action and get the new adapter based action or we can
reuse the existing action. Since the ButtonActions class offers an API for
remove existing actions, we reuse the existing action because it very uncommon
to replace existing action during an for update call with an adapter. If
someone really will add an action adapter during process time via directly
provided interface, he is also responsible for remove existing actions.
As you can see we still will get the old button action if we only call update:
>>> actions.update()
>>> actions.keys()
['apply', 'cancel']
>>> actions['apply']
<ButtonAction 'form.buttons.apply' u'Apply'>
This means we have to remove the previous action before we call update:
>>> del actions['apply']
>>> actions.update()
Make sure we do not append a button twice to the key and value lists by calling
update twice:
>>> actions.keys()
['apply', 'cancel']
>>> actions['apply']
<CustomButtonAction 'form.buttons.apply' u'Apply'>
Alternatively, customize an individual button by setting its
actionFactory attribute.
>>> def customButtonActionFactory(request, field):
... print "This button factory creates a button only once."
... button = CustomButtonAction(request, field)
... button.css = "happy"
... return button
>>> form.buttons['apply'].actionFactory = customButtonActionFactory
Again, remove the old button action befor we call update:
>>> del actions['apply']
>>> actions.update()
This button factory creates a button only once.
>>> actions.update()
>>> actions['apply'].css
'happy'
Since we only create a button once from an adapter or a factory, we can change
the button attributes without to lose changes:
>>> actions['apply'].css = 'very happy'
>>> actions['apply'].css
'very happy'
>>> actions.update()
>>> actions['apply'].css
'very happy'
But let's not digress too much and get rid of this customization
>>> form.buttons['apply'].actionFactory = None
>>> actions.update()
Button actions are locations:
>>> apply = actions['apply']
>>> apply.__name__
'apply'
>>> apply.__parent__
<ButtonActions None>
A button action is also a submit widget. The attributes translate as follows:
>>> interfaces.ISubmitWidget.providedBy(apply)
True
>>> apply.value == apply.title
True
>>> apply.id == apply.name.replace('.', '-')
True
Next we want to display our button actions. To be able to do this, we have to
register a template for the submit widget:
>>> from z3c.form import testing, widget
>>> templatePath = testing.getPath('submit_input.pt')
>>> factory = widget.WidgetTemplateFactory(templatePath, 'text/html')
>>> from zope.pagetemplate.interfaces import IPageTemplate
>>> zope.component.provideAdapter(factory,
... (zope.interface.Interface, TestRequest, None, None,
... interfaces.ISubmitWidget),
... IPageTemplate, name='input')
A widget template has many discriminators: context, request, view, field, and
widget. We can now render each action:
>>> print actions['apply'].render()
<input type="submit" id="form-buttons-apply"
name="form.buttons.apply" class="submit-widget button-field"
value="Apply" />
So displaying is nice, but how do button handlers get executed? The action
manager provides attributes and method to check whether actions were
executed. Initially there are no executed actions:
>>> list(actions.executedActions)
[]
So in this case executing the actions does not do anything:
>>> actions.execute()
But if the request contains the information that the button was pressed, the
execution works:
>>> request = TestRequest(form={'form.buttons.apply': 'Apply'})
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
Aehm, something should have happened. But in order for the system to look at
the handlers declared in the form, a special action handler has to be
registered with the system:
>>> zope.component.provideAdapter(button.ButtonActionHandler)
And voila, the execution works:
>>> actions.execute()
successfully applied
Finally, if there is no handler for a button, then the button click is
silently ignored:
>>> form.handlers = button.Handlers()
>>> actions.execute()
While this might seem awkward at first, this is an intended feature. Sometimes
there are several sub-forms that listen to a particular button and one form or
another might simply not care about the button at all and not provide a
handler.
In-Form Button Declarations
---------------------------
Some readers might find it cumbersome to declare a full schema just to create
some buttons. A faster method is to write simple arguments to the button
manager:
>>> class Form(object):
... zope.interface.implements(
... interfaces.IButtonForm, interfaces.IHandlerForm)
... buttons = button.Buttons(
... button.Button('apply', title=u'Apply'))
... prefix = 'form.'
...
... @button.handler(buttons['apply'])
... def apply(self, action):
... print 'successfully applied'
The first argument of the ``Button`` class constructor is the name of the
button. Optionally, this can also be one of the following keyword arguments:
>>> button.Button(name='apply').__name__
'apply'
>>> button.Button(__name__='apply').__name__
'apply'
If no name is specified, the button will not have a name immediately, ...
>>> button.Button(title=u'Apply').__name__
''
because if the button is created within an interface, the name is assigned
later:
>>> class IActions(zope.interface.Interface):
... apply = button.Button(title=u'Apply')
>>> IActions['apply'].__name__
'apply'
However, once the button is added to a button manager, a name will be
assigned:
>>> btns = button.Buttons(button.Button(title=u'Apply'))
>>> btns['apply'].__name__
'apply'
>>> btns = button.Buttons(button.Button(title=u'Apply and more'))
>>> btns['4170706c7920616e64206d6f7265'].__name__
'4170706c7920616e64206d6f7265'
This declaration behaves identical to the one before:
>>> form = Form()
>>> request = TestRequest()
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
When sending in the right information, the actions are executed:
>>> request = TestRequest(form={'form.buttons.apply': 'Apply'})
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
successfully applied
An even simpler method -- resembling closest the API provided by formlib -- is
to create the button and handler at the same time:
>>> class Form(object):
... zope.interface.implements(
... interfaces.IButtonForm, interfaces.IHandlerForm)
... prefix = 'form.'
...
... @button.buttonAndHandler(u'Apply')
... def apply(self, action):
... print 'successfully applied'
In this case the ``buttonAndHandler`` decorator creates a button and a handler
for it. By default the name is computed from the title of the button, which is
required. All (keyword) arguments are forwarded to the button
constructor. Let's now render the form:
>>> request = TestRequest(form={'form.buttons.apply': 'Apply'})
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
successfully applied
If the title is a more complex string, then the name of the button becomes a
hex-encoded string:
>>> class Form(object):
...
... @button.buttonAndHandler(u'Apply and Next')
... def apply(self, action):
... print 'successfully applied'
>>> Form.buttons.keys()
['4170706c7920616e64204e657874']
Of course, you can use the ``__name__`` argument to specify a name
yourself. The decorator, however, also allows the keyword ``name``:
>>> class Form(object):
...
... @button.buttonAndHandler(u'Apply and Next', name='applyNext')
... def apply(self, action):
... print 'successfully applied'
>>> Form.buttons.keys()
['applyNext']
This helper function also supports a keyword argument ``provides``, which
allows the developer to specify a sequence of interfaces that the generated
button should directly provide. Those provided interfaces can be used for a
multitude of things, including handler discrimination and UI layout:
>>> class IMyButton(zope.interface.Interface):
... pass
>>> class Form(object):
...
... @button.buttonAndHandler(u'Apply', provides=(IMyButton,))
... def apply(self, action):
... print 'successfully applied'
>>> IMyButton.providedBy(Form.buttons['apply'])
True
Button Conditions
-----------------
Sometimes it is desirable to only show a button when a certain condition is
fulfilled. The ``Button`` field supports conditions via a simple argument. The
``condition`` argument must be a callable taking the form as argument and
returning a truth-value. If the condition is not fulfilled, the button will not
be converted to an action:
>>> class Form(object):
... prefix='form'
... showApply = True
...
... @button.buttonAndHandler(
... u'Apply', condition=lambda form: form.showApply)
... def apply(self, action):
... print 'successfully applied'
In this case a form variable specifies the availability. Initially the button
is available as action:
>>> myform = Form()
>>> actions = button.ButtonActions(myform, TestRequest(), None)
>>> actions.update()
>>> actions.keys()
['apply']
If we set the show-apply attribute to false, the action will not be available.
>>> myform.showApply = False
>>> actions = button.ButtonActions(myform, TestRequest(), None)
>>> actions.update()
>>> actions.keys()
[]
This feature is very helpful in multi-forms and wizards.
Customizing the Title
---------------------
As for widgets, it is often desirable to change attributes of the button
actions without altering any original code. Again we will be using attribute
value adapters to complete the task. Originally, our title is as follows:
>>> myform = Form()
>>> actions = button.ButtonActions(myform, TestRequest(), None)
>>> actions.update()
>>> actions['apply'].title
u'Apply'
Let's now create a custom label for the action:
>>> ApplyLabel = button.StaticButtonActionAttribute(
... u'Apply now', button=myform.buttons['apply'])
>>> zope.component.provideAdapter(ApplyLabel, name='title')
Once the button action manager is updated, the new title is chosen:
>>> actions.update()
>>> actions['apply'].title
u'Apply now'
The Button Manager
------------------
The button manager contains several additional API methods that make the
management of buttons easy.
First, you are able to add button managers:
>>> bm1 = button.Buttons(IButtons)
>>> bm2 = button.Buttons(button.Button('help', title=u'Help'))
>>> bm1 + bm2
<z3c.form.button.Buttons object at ...>
>>> list(bm1 + bm2)
['apply', 'cancel', 'help']
The result of the addition is another button manager. Also note that the order
of the buttons is preserved throughout the addition. Adding anything else is
not well-defined:
>>> bm1 + 1
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'Buttons' and 'int'
Second, you can select the buttons in a particular order:
>>> bm = bm1 + bm2
>>> list(bm)
['apply', 'cancel', 'help']
>>> list(bm.select('help', 'apply', 'cancel'))
['help', 'apply', 'cancel']
The ``select()`` method can also be used to eliminate another button:
>>> list(bm.select('help', 'apply'))
['help', 'apply']
Of course, in the example above we eliminated one and reorganized the buttons.
Third, you can omit one or more buttons:
>>> list(bm.omit('cancel'))
['apply', 'help']
Finally, while the constructor is very flexible, you cannot just pass in
anything:
>>> button.Buttons(1, 2)
Traceback (most recent call last):
...
TypeError: ('Unrecognized argument type', 1)
When creating a new form derived from another, you often want to keep existing
buttons and add new ones. In order not to change the super-form class, you need
to copy the button manager:
>>> bm.keys()
['apply', 'cancel', 'help']
>>> bm.copy().keys()
['apply', 'cancel', 'help']
The Handlers Object
-------------------
All handlers of a form are collected in the ``handlers`` attribute, which is a
``Handlers`` instance:
>>> isinstance(form.handlers, button.Handlers)
True
>>> form.handlers
<Handlers [<Handler for <Button 'apply' u'Apply'>>]>
Internally the object uses an adapter registry to manage the handlers for
buttons. If a handler is registered for a button, it simply behaves as an
instance-adapter.
The object itself is pretty simple. You can get a handler as follows:
>>> apply = form.buttons['apply']
>>> form.handlers.getHandler(apply)
<Handler for <Button 'apply' u'Apply'>>
But you can also register handlers for groups of buttons, either by interface
or class:
>>> class SpecialButton(button.Button):
... pass
>>> def handleSpecialButton(form, action):
... return 'Special button action'
>>> form.handlers.addHandler(
... SpecialButton, button.Handler(SpecialButton, handleSpecialButton))
>>> form.handlers
<Handlers
[<Handler for <Button 'apply' u'Apply'>>,
<Handler for <class 'SpecialButton'>>]>
Now all special buttons should use that handler:
>>> button1 = SpecialButton(name='button1', title=u'Button 1')
>>> button2 = SpecialButton(name='button2', title=u'Button 2')
>>> form.handlers.getHandler(button1)(form, None)
'Special button action'
>>> form.handlers.getHandler(button2)(form, None)
'Special button action'
However, registering a more specific handler for button 1 will override the
general handler:
>>> def handleButton1(form, action):
... return 'Button 1 action'
>>> form.handlers.addHandler(
... button1, button.Handler(button1, handleButton1))
>>> form.handlers.getHandler(button1)(form, None)
'Button 1 action'
>>> form.handlers.getHandler(button2)(form, None)
'Special button action'
You can also add handlers objects:
>>> handlers2 = button.Handlers()
>>> button3 = SpecialButton(name='button3', title=u'Button 3')
>>> handlers2.addHandler(
... button3, button.Handler(button3, None))
>>> form.handlers + handlers2
<Handlers
[<Handler for <Button 'apply' u'Apply'>>,
<Handler for <class 'SpecialButton'>>,
<Handler for <SpecialButton 'button1' u'Button 1'>>,
<Handler for <SpecialButton 'button3' u'Button 3'>>]>
However, adding other components is not supported:
>>> form.handlers + 1
Traceback (most recent call last):
...
NotImplementedError
The handlers also provide a method to copy the handlers to a new instance:
>>> copy = form.handlers.copy()
>>> isinstance(copy, button.Handlers)
True
>>> copy is form.handlers
False
This is commonly needed when one wants to extend the handlers of a super-form.
Image Buttons
-------------
A special type of button is the image button. Instead of creating a "submit"-
or "button"-type input, an "image" button is created. An image button is a
simple extension of a button, requiring an `image` argument to the constructor:
>>> imgSubmit = button.ImageButton(
... name='submit',
... title=u'Submit',
... image=u'submit.png')
>>> imgSubmit
<ImageButton 'submit' u'submit.png'>
Some browsers do not submit the value of the input, but only the coordinates
of the image where the mouse click occurred. Thus we also need a special
button action:
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> imgSubmitAction = button.ImageButtonAction(request, imgSubmit)
>>> imgSubmitAction
<ImageButtonAction 'submit' u'Submit'>
Initially, we did not click on the image:
>>> imgSubmitAction.isExecuted()
False
Now the button is clicked:
>>> request = TestRequest(form={'submit.x': '3', 'submit.y': '4'})
>>> imgSubmitAction = button.ImageButtonAction(request, imgSubmit)
>>> imgSubmitAction.isExecuted()
True
The "image" type of the "input"-element also requires there to be a `src`
attribute, which is the URL to the image to be used. The attribute is also
supported by the Python API. However, in order for the attribute to work, the
image must be available as a resource, so let's do that now:
# Traversing setup
>>> from zope.traversing import testing
>>> testing.setUp()
# Resource namespace
>>> import zope.component
>>> from zope.traversing.interfaces import ITraversable
>>> from zope.traversing.namespace import resource
>>> zope.component.provideAdapter(
... resource, (None,), ITraversable, name="resource")
>>> zope.component.provideAdapter(
... resource, (None, None), ITraversable, name="resource")
# Register the "submit.png" resource
>>> from zope.app.publisher.browser.resource import Resource
>>> testing.browserResource('submit.png', Resource)
Now the attribute can be called:
>>> imgSubmitAction.src
u'http://127.0.0.1/@@/submit.png'
==========
Directives
==========
Widget template directive
-------------------------
Show how we can use the widget template directive. Register the meta
configuration for the directive.
>>> import sys
>>> from zope.configuration import xmlconfig
>>> import z3c.form
>>> context = xmlconfig.file('meta.zcml', z3c.form)
We need a custom widget template
>>> import os, tempfile
>>> temp_dir = tempfile.mkdtemp()
>>> file = os.path.join(temp_dir, 'widget.pt')
>>> open(file, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <input type="text" id="" name="" value="" size=""
... tal:attributes="id view/id;
... name view/name;
... size view/size;
... value view/value;" />
... </html>
... ''')
and a interface
>>> import zope.interface
>>> from z3c.form import interfaces
>>> class IMyWidget(interfaces.IWidget):
... """My widget interface."""
and a widget class:
>>> from z3c.form.testing import TestRequest
>>> from z3c.form.browser import text
>>> class MyWidget(text.TextWidget):
... zope.interface.implements(IMyWidget)
>>> request = TestRequest()
>>> myWidget = MyWidget(request)
Make them available under the fake package ``custom``:
>>> sys.modules['custom'] = type(
... 'Module', (),
... {'IMyWidget': IMyWidget})()
and register them as a widget template within the ``z3c:widgetTemplate``
directive:
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:widgetTemplate
... template="%s"
... widget="custom.IMyWidget"
... />
... </configure>
... """ % file, context=context)
Let's get the template
>>> import zope.component
>>> from z3c.template.interfaces import IPageTemplate
>>> template = zope.component.queryMultiAdapter((None, request, None, None,
... myWidget), interface=IPageTemplate, name='input')
and check it:
>>> from z3c.form.ptcompat import ViewPageTemplateFile
>>> isinstance(template, ViewPageTemplateFile)
True
Let's use the template within the widget.
>>> print template(myWidget)
<input type="text" value="" />
We normally render the widget which returns the registered template.
>>> print myWidget.render()
<input type="text" value="" />
If the template does not exist, then the widget directive should fail
immediately:
>>> unknownFile = os.path.join(temp_dir, 'unknown.pt')
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:widgetTemplate
... template="%s"
... widget="custom.IMyWidget"
... />
... </configure>
... """ % unknownFile, context=context)
Traceback (most recent call last):
...
ZopeXMLConfigurationError: File "<string>", line 4.2-7.8
ConfigurationError: ('No such file', '...unknown.pt')
Object Widget template directive
--------------------------------
Show how we can use the objectwidget template directive.
The big difference between the 'simple' Widget template and the Object Widget
directive is that the Object Widget template takes the field's schema into
account. That makes it easy to register different widget templates for different
sub-schemas. You can use this together with SubformAdapter to get a totally
custom subwidget.
We need a custom widget template
>>> file = os.path.join(temp_dir, 'widget.pt')
>>> open(file, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <div class="object-widget" tal:attributes="class view/klass">
... yeah, this can get comlex
... </div>
... </html>
... ''')
and a interface
>>> class IMyObjectWidget(interfaces.IObjectWidget):
... """My objectwidget interface."""
and a widget class:
>>> from z3c.form.browser import object
>>> class MyObjectWidget(object.ObjectWidget):
... zope.interface.implements(IMyObjectWidget)
>>> request = TestRequest()
>>> myObjectWidget = MyObjectWidget(request)
>>> from z3c.form.testing import IMySubObject
>>> import zope.schema
>>> field = zope.schema.Object(
... __name__='subobject',
... title=u'my object widget',
... schema=IMySubObject)
>>> myObjectWidget.field = field
Make them available under the fake package ``custom``:
>>> sys.modules['custom'] = type(
... 'Module', (),
... {'IMyObjectWidget': IMyObjectWidget})()
and register them as a widget template within the ``z3c:objectWidgetTemplate``
directive:
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:objectWidgetTemplate
... template="%s"
... widget="custom.IMyObjectWidget"
... />
... </configure>
... """ % file, context=context)
Let's get the template
>>> template = zope.component.queryMultiAdapter((None, request, None, None,
... myObjectWidget, None), interface=IPageTemplate, name='input')
and check it:
>>> isinstance(template, ViewPageTemplateFile)
True
Let's use the template within the widget.
>>> print template(myObjectWidget)
<div class="object-widget">yeah, this can get comlex</div>
We normally render the widget which returns the registered template.
>>> print myObjectWidget.render()
<div class="object-widget">yeah, this can get comlex</div>
If the template does not exist, then the widget directive should fail
immediately:
>>> unknownFile = os.path.join(temp_dir, 'unknown.pt')
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:objectWidgetTemplate
... template="%s"
... widget="custom.IMyObjectWidget"
... />
... </configure>
... """ % unknownFile, context=context)
Traceback (most recent call last):
...
ZopeXMLConfigurationError: File "<string>", line 4.2-7.8
ConfigurationError: ('No such file', '...unknown.pt')
Register a specific template for a schema:
We need a custom widget template
>>> file = os.path.join(temp_dir, 'widgetspec.pt')
>>> open(file, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <div class="object-widget" tal:attributes="class view/klass">
... this one is specific
... </div>
... </html>
... ''')
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:objectWidgetTemplate
... template="%s"
... widget="custom.IMyObjectWidget"
... schema="z3c.form.testing.IMySubObject"
... />
... </configure>
... """ % file, context=context)
Let's get the template
>>> template = zope.component.queryMultiAdapter((None, request, None, None,
... myObjectWidget, None), interface=IPageTemplate, name='input')
and check it:
>>> print myObjectWidget.render()
<div class="object-widget">this one is specific</div>
Cleanup
-------
Now we need to clean up the custom module.
>>> del sys.modules['custom']
Also let's not leave temporary files lying around
>>> import shutil
>>> shutil.rmtree(temp_dir)
==========
Validators
==========
Validators are components that validate submitted data. This is certainly not
a new concept, but in the previous form frameworks validation was hidden in
many places:
* Field/Widget Validation
The schema field consists of a ``validate()`` method. Validation is
automatically invoked when converting a unicode string to a field value
using ``fromUnicode()``. This makes it very hard to customize the field
validation. No hooks were provided to exert dditional restriction at the
presentation level.
* Schema/Form Validation
This type of validation was not supported at all initially. ``zope.formlib``
fixed this problem by validating against schema invariants. While this was a
first good step, it still made it hard to customize validators, since it
required touching the base implementations of the forms.
* Action Validation
``zope.formlib`` supports the notion of action validatos. Actions have a
success and failure handler. If the validation succeeds, the success handler
is called, otherwise the failure handler is chosen. We believe that this
design was ill-conceived, especially the default, which required the data to
completely validate in order for the action to successful. There are many
actions that do not even care about the data in the form, such as "Help",
"Cancel" and "Reset" buttons. Thus validation should be part of the data
retrieval process and not the action.
For me, the primary goals of the validator framework are as follows:
* Assert additional restrictions on the data at the presentation
level.
There are several use cases for this. Sometimes clients desire additional
restrictions on data for their particular version of the software. It is not
always desireable to adjust the model for this client, since the framework
knows how to handle the less restrictive case anyways. In another case,
additional restrictions might be applied to a particular form due to limited
restrictions.
* Make validation pluggable.
Like most other components of this package, it should be possible to control
the validation adapters at a fine grained level.
* Widgets: context, request, view, field[1], widget
* Widget Managers: context, request, view, schema[2], manager
[1].. This is optional, since widgets must not necessarily have fields.
[2].. This is optional, since widget managers must not necessarily have
manage field widgets and thus know about schemas.
* Provide good defaults that behave sensibly.
Good defaults are, like in anywhere in this pacakge, very important. We have
chosen to implement the ``zope.formlib`` behavior as the default, since it
worked very well -- with exception of action validation, of course.
For this package, we have decided to support validators at the widget and
widget manager level. By default the framework only supports field widgets,
since the validation of field-absent widgets is generally not
well-defined. Thus, we first need to create a schema.
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... login = zope.schema.TextLine(
... title=u'Login',
... min_length=1,
... max_length=10)
...
... email = zope.schema.TextLine(
... title=u'E-mail')
...
... @zope.interface.invariant
... def isLoginPartOfEmail(person):
... if not person.email.startswith(person.login):
... raise zope.interface.Invalid("The login not part of email.")
Widget Validators
-----------------
Widget validators only validate the data of one particular widget. The
validated value is always assumed to be an internal value and not a widget
value.
By default, the system uses the simple field validator, which simply uses the
``validate()`` method of the field. For instantiation, all validators have the
following signature for its discriminators: context, request, view, field, and
widget
>>> from z3c.form import validator
>>> simple = validator.SimpleFieldValidator(
... None, None, None, IPerson['login'], None)
A validator has a single method ``validate()``. When the validation is
successful, ``None`` is returned:
>>> simple.validate(u'srichter')
A validation error is raised, when the validation fails:
>>> simple.validate(u'StephanCaveman3')
Traceback (most recent call last):
...
TooLong: (u'StephanCaveman3', 10)
Let's now create a validator that also requires at least 1 numerical character
in the login name:
>>> import re
>>> class LoginValidator(validator.SimpleFieldValidator):
...
... def validate(self, value):
... super(LoginValidator, self).validate(value)
... if re.search('[0-9]', value) is None:
... raise zope.interface.Invalid('No numerical character found.')
Let's now try our new validator:
>>> login = LoginValidator(None, None, None, IPerson['login'], None)
>>> login.validate(u'srichter1')
>>> login.validate(u'srichter')
Traceback (most recent call last):
...
Invalid: No numerical character found.
We can now register the validator with the component architecture, ...
>>> import zope.component
>>> zope.component.provideAdapter(LoginValidator)
and look up the adapter using the usual way:
>>> from z3c.form import interfaces
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['login'], None),
... interfaces.IValidator)
<LoginValidator for IPerson['login']>
Unfortunately, the adapter is now registered for all fields, so that the
E-mail field also has this restriction (which is okay in this case, but not
generally):
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['email'], None),
... interfaces.IValidator)
<LoginValidator for IPerson['email']>
The validator module provides a helper function to set the discriminators for
a validator, which can include instances:
>>> validator.WidgetValidatorDiscriminators(
... LoginValidator, field=IPerson['login'])
Let's now clean up the component architecture and register the login validator
again:
>>> from zope.testing import cleanup
>>> cleanup.cleanUp()
>>> zope.component.provideAdapter(LoginValidator)
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['login'], None),
... interfaces.IValidator)
<LoginValidator for IPerson['login']>
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['email'], None),
... interfaces.IValidator)
Widget Validators and File-Uploads
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
File-Uploads behave a bit different than the other form
elements. Whether the user did not choose a file to upload
``interfaces.NOT_CHANGED`` is set as value. But the validator knows
how to handle this.
The example has two bytes fields where File-Uploads are possible, one
field is required the other one not:
>>> class IPhoto(zope.interface.Interface):
... data = zope.schema.Bytes(
... title=u'Photo')
...
... thumb = zope.schema.Bytes(
... title=u'Thumbnail',
... required=False)
There are several possible cases to differentiate between:
No widget
+++++++++
If there is no widget or the widget does not provide
``interfaces.IContextAware``, no value is looked up from the
context. So the not required field validates successfully but the
required one has an required missing error, as the default value of
the field is looked up on the field:
>>> simple_thumb = validator.SimpleFieldValidator(
... None, None, None, IPhoto['thumb'], None)
>>> simple_thumb.validate(interfaces.NOT_CHANGED)
>>> simple_data = validator.SimpleFieldValidator(
... None, None, None, IPhoto['data'], None)
>>> simple_data.validate(interfaces.NOT_CHANGED)
Traceback (most recent call last):
RequiredMissing
Widget which ignores context
++++++++++++++++++++++++++++
If the context is ignored in the widget - as in the add form - the
behavior is the same as if there was no widget:
>>> import z3c.form.widget
>>> widget = z3c.form.widget.Widget(None)
>>> zope.interface.alsoProvides(widget, interfaces.IContextAware)
>>> widget.ignoreContext = True
>>> simple_thumb = validator.SimpleFieldValidator(
... None, None, None, IPhoto['thumb'], widget)
>>> simple_thumb.validate(interfaces.NOT_CHANGED)
>>> simple_data = validator.SimpleFieldValidator(
... None, None, None, IPhoto['data'], widget)
>>> simple_data.validate(interfaces.NOT_CHANGED)
Traceback (most recent call last):
RequiredMissing
Look up value from default adapter
++++++++++++++++++++++++++++++++++
When the value is ``interfaces.NOT_CHANGED`` the validator tries to
look up the default value using a ``interfaces.IValue``
adapter. Whether the adapter is found, its value is used as default,
so the validation of the required field is successful here:
>>> data_default = z3c.form.widget.StaticWidgetAttribute(
... 'data', context=None, request=None, view=None,
... field=IPhoto['data'], widget=widget)
>>> zope.component.provideAdapter(data_default, name='default')
>>> simple_data.validate(interfaces.NOT_CHANGED)
Look up value from context
++++++++++++++++++++++++++
If there is a context aware widget which does not ignore its context,
the value is looked up on the context using a data manager:
>>> class Photo(object):
... zope.interface.implements(IPhoto)
...
... data = None
... thumb = None
>>> photo = Photo()
>>> widget.ignoreContext = False
>>> zope.component.provideAdapter(z3c.form.datamanager.AttributeField)
>>> simple_thumb = validator.SimpleFieldValidator(
... photo, None, None, IPhoto['thumb'], widget)
>>> simple_thumb.validate(interfaces.NOT_CHANGED)
If the value is not set on the context it is a required missing as
neither context nor input have a valid value:
>>> simple_data = validator.SimpleFieldValidator(
... photo, None, None, IPhoto['data'], widget)
>>> simple_data.validate(interfaces.NOT_CHANGED)
Traceback (most recent call last):
RequiredMissing
After setting the value validation is successful:
>>> photo.data = 'data'
>>> simple_data.validate(interfaces.NOT_CHANGED)
Clean-up
++++++++
>>> gsm = zope.component.getGlobalSiteManager()
>>> gsm.unregisterAdapter(z3c.form.datamanager.AttributeField)
True
>>> gsm.unregisterAdapter(data_default, name='default')
True
Widget Manager Validators
-------------------------
The widget manager validator, while similar in spirit, works somewhat
different. The discriminators of the widget manager validator are: context,
request, view, schema, and manager.
A simple default implementation is provided that checks the invariants of the
schemas:
>>> invariants = validator.InvariantsValidator(
... None, None, None, IPerson, None)
Widget manager validators have the option to validate a data dictionary,
>>> invariants.validate(
... {'login': u'srichter', 'email': u'srichter@foo.com'})
()
or an object implementing the schema:
>>> class Person(object):
... zope.interface.implements(IPerson)
... login = u'srichter'
... email = u'srichter@foo.com'
>>> stephan = Person()
>>> invariants.validateObject(stephan)
()
Since multiple errors can occur during the validation process, all errors are
collected in a tuple, which is returned. If the tuple is empty, the validation
was successful. Let's now generate a failure:
>>> errors = invariants.validate(
... {'login': u'srichter', 'email': u'strichter@foo.com'})
>>> for e in errors:
... print e.__class__.__name__ + ':', e
Invalid: The login not part of email.
Let's now have a look at writing a custom validator. In this case, we want to
ensure that the E-mail address is at most twice as long as the login:
>>> class CustomValidator(validator.InvariantsValidator):
... def validateObject(self, obj):
... errors = super(CustomValidator, self).validateObject(obj)
... if len(obj.email) > 2 * len(obj.login):
... errors += (zope.interface.Invalid('Email too long.'),)
... return errors
Since the ``validate()`` method of ``InvatiantsValidator`` simply uses
``validateObject()`` it is enough to only override ``validateObject()``. Now
we can use the validator:
>>> custom = CustomValidator(
... None, None, None, IPerson, None)
>>> custom.validate(
... {'login': u'srichter', 'email': u'srichter@foo.com'})
()
>>> errors = custom.validate(
... {'login': u'srichter', 'email': u'srichter@foobar.com'})
>>> for e in errors:
... print e.__class__.__name__ + ':', e
Invalid: Email too long.
To register the custom validator only for this schema, we have to use the
discriminator generator again.
>>> from z3c.form import util
>>> validator.WidgetsValidatorDiscriminators(
... CustomValidator, schema=util.getSpecification(IPerson, force=True))
Note: Of course we could have used the ``zope.component.adapts()`` function
from within the class, but I think it is too tedious, since you have to
specify all discriminators and not only the specific ones you are
interested in.
After registering the validator,
>>> zope.component.provideAdapter(CustomValidator)
it becomes the validator for this schema:
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson, None), interfaces.IManagerValidator)
<CustomValidator for IPerson>
>>> class ICar(zope.interface.Interface):
... pass
>>> zope.component.queryMultiAdapter(
... (None, None, None, ICar, None), interfaces.IManagerValidator)
The Data Wrapper
----------------
The ``Data`` class provides a wrapper to present a dictionary as a class
instance. This is used to check for invariants, which always expect an
object. While the common use cases of the data wrapper are well tested in the
code above, there are some corner cases that need to be addressed.
So let's start by creating a data object:
>>> context = object()
>>> data = validator.Data(IPerson, {'login': 'srichter', 'other': 1}, context)
When we try to access a name that is not in the schema, we get an attribute
error:
>>> data.address
Traceback (most recent call last):
...
AttributeError: address
>>> data.other
Traceback (most recent call last):
...
AttributeError: other
If the field found is a method, then a runtime error is raised:
>>> class IExtendedPerson(IPerson):
... def compute():
... """Compute something."""
>>> data = validator.Data(IExtendedPerson, {'compute': 1}, context)
>>> data.compute
Traceback (most recent call last):
...
RuntimeError: ('Data value is not a schema field', 'compute')
Finally, the context is available as attribute directly:
>>> data.__context__ is context
True
It is used by the validators (especially invariant validators) to provide a
context of validation, for example to look up a vocabulary or access the
parent of an object. Note that the context will be different between add and
edit forms.
Validation of interface variants when not all fields are displayed in form
--------------------------------------------------------------------------
We need to register the data manager to access the data on the context object:
>>> from z3c.form import datamanager
>>> zope.component.provideAdapter(datamanager.AttributeField)
Sometimes you might leave out fields in the form which need to compute the
invariant. An exception should be raised. The data wrapper is used to test
the invariants and looks up values on the context object that are left out in
the form.
>>> invariants = validator.InvariantsValidator(
... stephan, None, None, IPerson, None)
>>> errors = invariants.validate({'email': 'foo@bar.com'})
>>> errors[0].__class__.__name__
'Invalid'
>>> errors[0].args[0]
'The login not part of email.'
=======
Widgets
=======
Widgets are small UI components that accept and process the textual user
input. The only responsibility of a widget is to represent a value to the
user, allow it to be modified and then return a new value. Good examples of
widgets include the Qt widgets and HTML widgets. The widget is not responsible
for converting its value to the desired internal value or validate the
incoming data. These responsibilities are passed data converters and
validators, respectively.
There are several problems that can be identified in the original Zope 3 widget
implementation located at ``zope.app.form``.
(1) Field Dependence -- Widgets are always views of fields. While this might
be a correct choice for a high-level API, it is fundamentally wrong. It
disallows us to use widgets without defining fields. This also couples
certain pieces of information too tightly to the field, especially, value
retrieval from and storage to the context, validation and raw data
conversion.
(2) Form Dependence -- While widgets do not have to be located within a form,
they are usually tightly coupled to it. It is very difficult to use
widgets outside the context of a form.
(3) Traversability -- Widgets cannot be traversed, which means that they
cannot interact easily using Javascript. This is not a fundamental
problem, but simply a lack of the current design to recognize that small
UI components must also be traversable and thus have a URI.
(4) Customizability -- A consequence of issue (1) is that widgets are not
customizable enough. Implementing real-world projects has shown that
widgets often want a very fine-grained ability to customize values. A
prime example is the label. Because the label of a widget is retrieved
from the field title, it is impossible to provide an alternative label for
a widget. While the label could be changed from the form, this would
require rewriting the entire form to change a label. Instead, we often
endde up writing cusom schemas.
(5) Flexibility -- Oftentimes it is desired to have one widget, but multiple
styles of representation. For example, in one scenario the widget uses a
plain HTML widget and in another a fancy JavaScript widget is used. The
current implementation makes it very hard to provide alternative styles
for a widget.
Creating and Using Simple Widgets
---------------------------------
When using the widget API by itself, the simplest way to use it is to just
instantiate it using the request:
>>> from z3c.form.testing import TestRequest
>>> from z3c.form import widget
>>> request = TestRequest()
>>> age = widget.Widget(request)
In this case we instantiated a generic widget. A full set of simple
browser-based widgets can be found in the ``browser/`` package. Since no
helper components are around to fill the attributes of the widget, we have to
do it by hand:
>>> age.name = 'age'
>>> age.label = u'Age'
>>> age.value = '39'
The most important attributes are the "name" and the "value". The name is used
to identify the widget within the form. The value is either the value to be
manipulated or the default value. The value must be provided in the form the
widget needs it. It is the responsibility of a data converter to convert
between the widget value and the desired internal value.
Before we can render the widget, we have to register a template for the
widget. The first step is to define the template:
>>> import tempfile
>>> textWidgetTemplate = tempfile.mktemp('text.pt')
>>> open(textWidgetTemplate, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <input type="text" name="" value=""
... tal:attributes="name view/name; value view/value;" />
... </html>
... ''')
Next, we have to create a template factory for the widget:
>>> from z3c.form.widget import WidgetTemplateFactory
>>> factory = WidgetTemplateFactory(
... textWidgetTemplate, widget=widget.Widget)
The first argument, which is also required, is the path to the template
file. An optional ``content_type`` keyword argument allows the developer to
specify the output content type, usually "text/html". Then there are five
keyword arguments that specify the discriminators of the template:
* ``context`` -- This is the context in which the widget is displayed. In a
simple widget like the one we have now, the context is ``None``.
* ``request`` -- This discriminator allows you to specify the type of request
for which the widget will be available. In our case this would be a browser
request. Note that browser requests can be further broken into layer, so you
could also specify a layer interface here.
* ``view`` -- This is the view from which the widget is used. The simple
widget at hand, does not have a view associated with it though.
* ``field`` -- This is the field for which the widget provides a
representation. Again, this simple widget does not use a field, so it is
``None``.
* ``widget`` -- This is the widget itself. With this discriminator you can
specify for which type of widget you are providing a template.
We can now register the template factory. The name of the factory is the mode
of the widget. By default, there are two widget modes: "input" and
"display". However, since the mode is just a string, one can develop other
kinds of modes as needed for a project. The default mode is "input":
>>> from z3c.form import interfaces
>>> age.mode is interfaces.INPUT_MODE
True
>>> import zope.component
>>> zope.component.provideAdapter(factory, name=interfaces.INPUT_MODE)
Once everything is set up, the widget is updated and then rendered:
>>> age.update()
>>> print age.render()
<input type="text" name="age" value="39" />
If a value is found in the request, it takes precedence, since the user
entered the value:
>>> age.request = TestRequest(form={'age': '25'})
>>> age.update()
>>> print age.render()
<input type="text" name="age" value="25" />
However, there is an option to turn off all request data:
>>> age.value = '39'
>>> age.ignoreRequest = True
>>> age.update()
>>> print age.render()
<input type="text" name="age" value="39" />
Creating and Using Field Widgets
--------------------------------
An extended form of the widget allows fields to control several of the
widget's properties. Let's create a field first:
>>> ageField = zope.schema.Int(
... __name__ = 'age',
... title = u'Age',
... min = 0,
... max = 130)
We can now use our simple widget and create a field widget from it:
>>> ageWidget = widget.FieldWidget(ageField, age)
Such a widget provides ``IFieldWidget``:
>>> interfaces.IFieldWidget.providedBy(ageWidget)
True
Of course, this is more commonly done using an adapter. Commonly those
adapters look like this:
>>> @zope.component.adapter(zope.schema.Int, TestRequest)
... @zope.interface.implementer(interfaces.IFieldWidget)
... def IntWidget(field, request):
... return widget.FieldWidget(field, widget.Widget(request))
>>> zope.component.provideAdapter(IntWidget)
>>> ageWidget = zope.component.getMultiAdapter((ageField, request),
... interfaces.IFieldWidget)
Now we just have to update and render the widget:
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" />
There is no initial value for the widget, since there is no value in the
request and the field does not provide a default. Let's now give our field a
default value and see what happens:
>>> ageField.default = 30
>>> ageWidget.update()
Traceback (most recent call last):
...
TypeError: ('Could not adapt', <Widget 'age'>,
<InterfaceClass z3c.form.interfaces.IDataConverter>)
In order for the widget to be able to take the field's default value and use
it to provide an initial value the widget, we need to provide a data converter
that defines how to convert from the field value to the widget value.
>>> from z3c.form import converter
>>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)
>>> zope.component.provideAdapter(converter.FieldDataConverter)
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="30" />
Again, the request value is honored above everything else:
>>> ageWidget.request = TestRequest(form={'age': '25'})
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="25" />
Creating and Using Context Widgets
----------------------------------
When widgets represent an attribute value of an object, then this object must
be set as the context of the widget:
>>> class Person(object):
... age = 45
>>> ageWidget.context = Person()
>>> zope.interface.alsoProvides(ageWidget, interfaces.IContextAware)
The result is that the context value takes over precendence over the default
value:
>>> ageWidget.request = TestRequest()
>>> ageWidget.update()
Traceback (most recent call last):
...
ComponentLookupError: ((...), <InterfaceClass ...IDataManager>, u'')
This call fails because the widget does not know how to extract the value from
the context. Registering a data manager for the widget does the trick:
>>> from z3c.form import datamanager
>>> zope.component.provideAdapter(datamanager.AttributeField)
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="45" />
The context can be explicitely ignored, making the widget display the default
value again:
>>> ageWidget.ignoreContext = True
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="30" />
Again, the request value is honored above everything else:
>>> ageWidget.request = TestRequest(form={'age': '25'})
>>> ageWidget.ignoreContext = False
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="25" />
But what happens if the object we are working on is security proxied? In
particular, what happens, if the access to the attribute is denied. To see
what happens, we have to create a proxied person:
>>> from zope.security import checker
>>> PersonChecker = checker.Checker({'age': 'Access'}, {'age': 'Edit'})
>>> ageWidget.request = TestRequest()
>>> ageWidget.context = checker.ProxyFactory(Person(), PersonChecker)
After changing the security policy, ...
>>> from zope.security import management
>>> from z3c.form import testing
>>> management.endInteraction()
>>> newPolicy = testing.SimpleSecurityPolicy()
>>> oldPolicy = management.setSecurityPolicy(newPolicy)
>>> management.newInteraction()
it is not possible anymore to update the widget:
>>> ageWidget.update()
Traceback (most recent call last):
...
Unauthorized: (<Person object at ...>, 'age', 'Access')
If no security declaration has been made at all, we get a
``ForbiddenAttribute`` error:
>>> ageWidget.context = checker.ProxyFactory(Person(), checker.Checker({}))
>>> ageWidget.update()
Traceback (most recent call last):
...
ForbiddenAttribute: ('age', <Person object at ...>)
Let's clean up the setup:
>>> management.endInteraction()
>>> newPolicy = management.setSecurityPolicy(oldPolicy)
>>> management.newInteraction()
>>> ageWidget.context = Person()
Dynamically Changing Attribute Values
-------------------------------------
Once widgets are used within a framework, it is very tedious to write Python
code to adjust certain attributes, even though hooks exist. The easiest way to
change those attribute values is actually to provide an adapter that provides
the custom value.
We can create a custom label for the age widget:
>>> AgeLabel = widget.StaticWidgetAttribute(
... u'Current Age',
... context=None, request=None, view=None, field=ageField, widget=None)
Clearly, this code does not require us to touch the orginal form and widget
code, given that we have enough control over the selection. In the example
above, all the selection discriminators are listed for demonstration
purposes. Of course, the label in this case can be created as follows:
>>> AgeLabel = widget.StaticWidgetAttribute(u'Current Age', field=ageField)
Much better, isn't it? Initially the label is the title of the field:
>>> ageWidget.label
u'Age'
Let's now simply register the label as a named adapter; the name is the name
of the attribute to change:
>>> zope.component.provideAdapter(AgeLabel, name='label')
Asking the widget for the label now will return the newly registered label:
>>> ageWidget.update()
>>> ageWidget.label
u'Current Age'
Of course, simply setting the label or changing the label extraction via a
sub-class are other options you might want to consider. Furthermore, you
could also create a computed attribute value or implement your own component.
Overriding other attributes, such as ``required``, is done in the same
way. If any widget provides new attributes, they are also overridable this
way. For example, the selection widget defines a label for the option that no
value was selected. We often want to override this, because the German
translation sucks or the wording is often too generic. Widget implementation
should add names of overridable attributes to their "_adapterValueAttributes"
internal attribute.
Let's try to override the ``required`` attribute. By default the widget is required,
because the field is required as well:
>>> ageWidget.required
True
Let's provide a static widget attribute adapter with name "required":
>>> AgeNotRequired = widget.StaticWidgetAttribute(False, field=ageField)
>>> zope.component.provideAdapter(AgeNotRequired, name="required")
Now, let's check if it works:
>>> ageWidget.update()
>>> ageWidget.required
False
Overriding the default value is somewhat special due to the complexity of
obtaining the value. So let's register one now:
>>> AgeDefault = widget.StaticWidgetAttribute(50, field=ageField)
>>> zope.component.provideAdapter(AgeDefault, name="default")
Let's now instantiate, update and render the widget to see the default value:
>>> ageWidget = zope.component.getMultiAdapter((ageField, request),
... interfaces.IFieldWidget)
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="50" />
Sequence Widget
---------------
A common use case in user interfaces is to ask the user to select one or more
items from a set of options/choices. The ``widget`` module provides a basic
widget implementation to support this use case.
The options available for selections are known as terms. Initially, there are
no terms:
>>> request = TestRequest()
>>> seqWidget = widget.SequenceWidget(request)
>>> seqWidget.name = 'seq'
>>> seqWidget.terms is None
True
There are two ways terms can be added, either manually or via an
adapter. Those term objects must provide ``ITerms``. There is no simple
default implementation, so we have to provide one ourselves:
>>> from zope.schema import vocabulary
>>> class Terms(vocabulary.SimpleVocabulary):
... zope.interface.implements(interfaces.ITerms)
... def getValue(self, token):
... return self.getTermByToken(token).value
>>> terms = Terms(
... [Terms.createTerm(1, 'v1', u'Value 1'),
... Terms.createTerm(2, 'v2', u'Value 2'),
... Terms.createTerm(3, 'v3', u'Value 3')])
>>> seqWidget.terms = terms
Once the ``terms`` attribute is set, updating the widgets does not change the
terms:
>>> seqWidget.update()
>>> [term.value for term in seqWidget.terms]
[1, 2, 3]
The value of a sequence widget is a tuple/list of term tokens. When extracting
values from the request, the values must be valid tokens, otherwise the
default value is returned:
>>> seqWidget.request = TestRequest(form={'seq': ['v1']})
>>> seqWidget.extract()
['v1']
>>> seqWidget.request = TestRequest(form={'seq': ['v4']})
>>> seqWidget.extract()
<NO_VALUE>
>>> seqWidget.request = TestRequest(form={'seq-empty-marker': '1'})
>>> seqWidget.extract()
[]
Note that we also support single values being returned outside a sequence. The
extracted value is then wrapped by a tuple. This feature is useful when
integrating with third-party client frameworks that do not know about the Zope
naming conventions.
>>> seqWidget.request = TestRequest(form={'seq': 'v1'})
>>> seqWidget.extract()
('v1',)
If the no-value token has been selected, it is returned without further
verification:
>>> seqWidget.request = TestRequest(form={'seq': [seqWidget.noValueToken]})
>>> seqWidget.extract()
['--NOVALUE--']
Since the value of the widget is a tuple of tokens, when displaying the
values, they have to be converted to the title of the term:
>>> seqWidget.value = ('v1', 'v2')
>>> seqWidget.displayValue
[u'Value 1', u'Value 2']
When input forms are directly switched to display forms within the same
request, it can happen that the value contains the "--NOVALUE--" token
entry. This entry should be silently ignored:
>>> seqWidget.value = (seqWidget.noValueToken,)
>>> seqWidget.displayValue
[]
To demonstrate how the terms is automatically chosen by a widget, we should
instantiate a field widget. Let's do this with a choice field:
>>> seqField = zope.schema.Choice(
... title=u'Sequence Field',
... vocabulary=terms)
Let's now create the field widget:
>>> seqWidget = widget.FieldWidget(seqField, widget.SequenceWidget(request))
>>> seqWidget.terms
The terms should be available as soon as the widget is updated:
>>> seqWidget.update()
Traceback (most recent call last):
...
ComponentLookupError: ((...), <InterfaceClass ...ITerms>, u'')
This failed, because we did not register an adapter for the terms yet. After
the adapter is registered, everything should work as expected:
>>> from z3c.form import term
>>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
>>> zope.component.provideAdapter(term.ChoiceTerms)
>>> seqWidget.update()
>>> seqWidget.terms
<z3c.form.term.ChoiceTermsVocabulary object at ...>
So that's it. Everything else is the same from then on.
Multi Widget
------------
A common use case in user interfaces is to ask the user to define one or more
items. The ``widget`` module provides a basic widget implementation to support
this use case.
The `MultiWidget` allows to store none, one or more values for a sequence
field. Don't get cinfused by the term sequence. Ther sequence used in
`SequenceWidget` means that the widget can choose from a sequence of values
which is a really a collection. The `MultiWidget` can collect values for build
and store a sequence of values like used in `ITuple` or `IList` field.
>>> request = TestRequest()
>>> multiWidget = widget.MultiWidget(request)
>>> multiWidget.name = 'multi.name'
>>> multiWidget.id = 'multi-id'
>>> multiWidget.value
[]
Let's define a field for our multi widget:
>>> multiField = zope.schema.List(
... value_type=zope.schema.Int(default=42))
>>> multiWidget.field = multiField
The value of a multi widget is always list. When extracting values from the
request, the values must be a list of valid values based on the value_type
field used from the used sequence field. The widget also uses a counter which
is required for processing the input from a request. The counter is a marker
for build the right amount of enumerated widgets.
If we provide no request we will get no value:
>>> multiWidget.extract()
<NO_VALUE>
If we provide an empty counter we will get no value:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'0'})
>>> multiWidget.extract()
<NO_VALUE>
If we provide real values within the request, we will get it back:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
... 'multi.name.0':u'42',
... 'multi.name.1':u'43'})
>>> multiWidget.extract()
[u'42', u'43']
If we provide a bad value we will get the bad value within the extract method.
Our widget update process will validate this bad value later:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'1',
... 'multi.name.0':u'bad'})
>>> multiWidget.extract()
[u'bad']
Storing a widget value forces to update the (sub) widgets. this forces also to
validate the (sub) widget values. For showing this we nee dot register a
validator:
>>> from z3c.form.validator import SimpleFieldValidator
>>> zope.component.provideAdapter(SimpleFieldValidator)
Since the value of the widget is a list of (widget) value items, when
displaying the values, they can be used as they are:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
... 'multi.name.0':u'42',
... 'multi.name.1':u'43'})
>>> multiWidget.value = multiWidget.extract()
>>> multiWidget.value
[u'42', u'43']
Each widget normaly gets first processed by it's update method call after
intialization. This update call forces to call extract, which first will get
the right amount of (sub) widgets by the given counter value. Based on that
counter value the right amount of widgets will get created. Each widget will
return it's own value and this collected values get returned by the extract
method. The multi widget update method will then store this values if any given
as multi widget value argument. If extract doesn't return a value the multi
widget update method will use it's default value. If we store a given value
from the extract as multi widget value, this will force to setup the multi
widget widgets based on the given values and apply the right value for them.
After that the mutli widget is ready for rendering. The good thing about that
pattern is that it is possible to set a value before or after the update method
is called. At any time if we change the mutli widget value the (sub) widgets
get updated within the new relevant value.
>>> multiRequest = TestRequest(form={'multi.name.count':'2',
... 'multi.name.0':u'42',
... 'multi.name.1':u'43'})
>>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(
... multiRequest))
>>> multiWidget.name = 'multi.name'
>>> multiWidget.value
[]
>>> multiWidget.update()
>>> multiWidget.widgets[0].value
u'42'
>>> multiWidget.widgets[1].value
u'43'
>>> multiWidget.value
[u'42', u'43']
MultiWidget also declares the ``allowAdding`` and ``allowRemoving``
attributes that can be used in browser presentation to control add/remove
button availability. To ease working with common cases, the
``updateAllowAddRemove`` method provided that will set those attributes
in respect to field's min_length and max_length, if the field provides
zope.schema.interfaces.IMinMaxLen interface.
Let's define a field with min and max length constraints and create
a widget for it.
>>> multiField = zope.schema.List(
... value_type=zope.schema.Int(),
... min_length=2,
... max_length=5)
>>> request = TestRequest()
>>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(request))
Now, let's check if the function will do the right thing depending on
the value:
No value:
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(True, False)
Minimum length:
>>> multiWidget.value = [u'3', u'5']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(True, False)
Some allowed length:
>>> multiWidget.value = [u'3', u'5', u'8', u'6']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(True, True)
Maximum length:
>>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(False, True)
Over maximum length:
>>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42', u'45']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(False, True)
I know a guy who once switched widget mode in the middle. All simple widgets
are easy to hack, but multiWidget needs to update all subwidgets:
>>> [w.mode for w in multiWidget.widgets]
['input', 'input', 'input', 'input', 'input', 'input']
Switch the multiWidget mode:
>>> multiWidget.mode = interfaces.DISPLAY_MODE
Yes, all subwigets switch mode:
>>> [w.mode for w in multiWidget.widgets]
['display', 'display', 'display', 'display', 'display', 'display']
Widget Events
-------------
Widget-system interaction can be very rich and wants to be extended in
unexpected ways. Thus there exists a generic widget event that can be used by
other code.
>>> event = widget.WidgetEvent(ageWidget)
>>> event
<WidgetEvent <Widget 'age'>>
These events provide the ``IWidgetEvent`` interface:
>>> interfaces.IWidgetEvent.providedBy(event)
True
There exists a special event that can be send out after a widget has been
updated, ...
>>> afterUpdate = widget.AfterWidgetUpdateEvent(ageWidget)
>>> afterUpdate
<AfterWidgetUpdateEvent <Widget 'age'>>
which provides another special interface:
>>> interfaces.IAfterWidgetUpdateEvent.providedBy(afterUpdate)
True
This event should be used by widget-managing components and is not created and
sent out internally by the widget's ``update()`` method. The event was
designed to provide an additional hook between updating the widget and
rendering it.
Cleanup
-------
Let's not leave temporary files lying around
>>> import os
>>> os.remove(textWidgetTemplate)
===============
Action Managers
===============
Action managers are components that manage all actions that can be taken
within a view, usually a form. They are also responsible for executing actions
when asked to do so.
Creating an action manager
--------------------------
An action manager is a form-related adapter that has the following
discriminator: form, request, and content. While there is a base
implementation for an action manager, the ``action`` module does not provide a
full implementation.
So we first have to build a simple implementation based on the ``Actions``
manager base class which allows us to add actions. Note that the following
implementation is for demonstration purposes. If you want to see a real action
manager implementation, then have a look at ``ButtonActions``. Let's now
implement our simple action manager:
>>> from z3c.form import action
>>> class SimpleActions(action.Actions):
... """Simple sample."""
...
... def append(self, name, action):
... """See z3c.form.interfaces.IActions."""
... if not name in self:
... self._data_keys.append(name)
... self._data_values.append(action)
... self._data[name] = action
Before we can initialise the action manager, we have to create instances for
our three discriminators, just enough to get it working:
>>> import zope.interface
>>> from z3c.form import interfaces
>>> class Form(object):
... zope.interface.implements(interfaces.IForm)
>>> form = Form()
>>> class Content(object):
... zope.interface.implements(zope.interface.Interface)
>>> content = Content()
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
We are now ready to create the action manager, which is a simple
triple-adapter:
>>> manager = SimpleActions(form, request, content)
>>> manager
<SimpleActions None>
As we can see in the manager representation above, the name of the manager is
``None``, since we have not specified one:
>>> manager.__name__ = 'example'
>>> manager
<SimpleActions 'example'>
Managing and Accessing Actions
------------------------------
Initially there are no actions in the manager:
>>> manager.keys()
[]
Our simple implementation of has an additional ``append()`` method, which we
will use to add actions:
>>> apply = action.Action(request, u'Apply')
>>> manager.append(apply.name, apply)
The action is added immediately:
>>> manager.keys()
['apply']
However, you should not rely on it being added, and always update the manager
once all actions were defined:
>>> manager.update()
Note: If the title of the action is a more complex unicode string and no name
is specified for the action, then a hexadecimal name is created from the
title:
>>> action.Action(request, u'Apply Now!').name
'4170706c79204e6f7721'
Since the action manager is an enumerable mapping, ...
>>> from zope.interface.common.mapping import IEnumerableMapping
>>> IEnumerableMapping.providedBy(manager)
True
there are several API methods available:
>>> manager['apply']
<Action 'apply' u'Apply'>
>>> manager['foo']
Traceback (most recent call last):
...
KeyError: 'foo'
>>> manager.get('apply')
<Action 'apply' u'Apply'>
>>> manager.get('foo', 'default')
'default'
>>> 'apply' in manager
True
>>> 'foo' in manager
False
>>> manager.values()
[<Action 'apply' u'Apply'>]
>>> manager.items()
[('apply', <Action 'apply' u'Apply'>)]
>>> len(manager)
1
Executing actions
-----------------
When an action is executed, an execution adapter is looked up. If there is no
adapter, nothing happens. So let's create a request that submits the apply
button:
>>> request = TestRequest(form={'apply': 'Apply'})
>>> manager = SimpleActions(form, request, content)
We also want to have two buttons in this case, so that we can ensure that only
one is executed:
>>> apply = action.Action(request, u'Apply')
>>> manager.append(apply.name, apply)
>>> cancel = action.Action(request, u'Cancel')
>>> manager.append(cancel.name, cancel)
>>> manager.update()
Now that the manager is updated, we can ask it for the "executed" actions:
>>> manager.executedActions
[<Action 'apply' u'Apply'>]
Executing the actions does nothing, because there are no handlers yet:
>>> manager.execute()
Let's now register an action handler that listens to the "Apply" action. An
action handler has four discriminators: form, request, content, and
action. All those objects are available to the handler under those names. When
using the base action handler from the ``action`` module, ``__call__()`` is
the only method that needs to be implemented:
>>> from z3c.form import util
>>> class SimpleActionHandler(action.ActionHandlerBase):
... zope.component.adapts(
... None, TestRequest, None, util.getSpecification(apply))
... def __call__(self):
... print 'successfully applied'
>>> zope.component.provideAdapter(SimpleActionHandler)
As you can see, we registered the action specifically for the apply
action. Now, executing the actions calls this handler:
>>> manager.execute()
successfully applied
Of course it only works for the "Apply" action and not ""Cancel":
>>> request = TestRequest(form={'cancel': 'Cancel'})
>>> manager.request = apply.request = cancel.request = request
>>> manager.execute()
Further, when a handler is successfully executed, an event is sent out, so
let's register an event handler:
>>> eventlog = []
>>> @zope.component.adapter(interfaces.IActionEvent)
... def handleEvent(event):
... eventlog.append(event)
>>> zope.component.provideHandler(handleEvent)
Let's now execute the "Apply" action again:
>>> request = TestRequest(form={'apply': 'Apply'})
>>> manager.request = apply.request = cancel.request = request
>>> manager.execute()
successfully applied
>>> eventlog[-1]
<ActionSuccessful for <Action 'apply' u'Apply'>>
Action handlers, however, can also raise action errors. These action errors
are caught and an event is created notifying the system of the problem. The
error is not further propagated. Other errors are not handled by the system to
avoid hiding real failures of the code.
Let's see how action errors can be used by implementing a handler for the
cancel action:
>>> class ErrorActionHandler(action.ActionHandlerBase):
... zope.component.adapts(
... None, TestRequest, None, util.getSpecification(cancel))
... def __call__(self):
... raise interfaces.ActionExecutionError(
... zope.interface.Invalid('Something went wrong'))
>>> zope.component.provideAdapter(ErrorActionHandler)
As you can see, the action execution error wraps some other execption, in this
case a simple invalid error.
Executing the "Cancel" action now produces the action error event:
>>> request = TestRequest(form={'cancel': 'Cancel'})
>>> manager.request = apply.request = cancel.request = request
>>> manager.execute()
>>> eventlog[-1]
<ActionErrorOccurred for <Action 'cancel' u'Cancel'>>
>>> eventlog[-1].error
<ActionExecutionError wrapping ...Invalid...>
========================
Attribute Value Adapters
========================
In advanced, highly customized projects it is often the case that a property
wants to be overridden for a particular customer in a particular case. A prime
example is the label of a widget. Until this implementation of a form
framework was written, widgets only could get their label from the field they
were representing. Thus, wanting to change the label of a widget meant
implementing a custom schema and re-registering the form in question for the
custom schema. It is needless to say that this was very annoying.
For this form framework, we are providing multiple levels of customization.
The user has the choice to change the value of an attribute through attribute
assignment or adapter lookup. The chronological order of an attribute value
assignment is as follows:
1. During initialization or right thereafter, the attribute value can be set
by direct attribute assignment, i.e. ``obj.attr = value``
2. While updating the object, an adapter is looked up for the attribute. If an
adapter is found, the attribute value will be overridden. Of course, if the
object does not have an ``update()`` method, one can choose another
location to do the adapter lookup.
3. After updating, the developer again has the choice to override the attribute
allowing granularity above and beyond the adapter.
The purpose of this module is to implement the availability of an attribute
value using an adapter.
>>> from z3c.form import value
The module provides helper functions and classes, to create those adapters
with as little code as possible.
Static Value Adapter
--------------------
To demonstrate the static value adapter, let's go back to our widget label
example. Let's create a couple of simple widgets and forms first:
>>> class TextWidget(object):
... label = u'Text'
>>> tw = TextWidget()
>>> class CheckboxWidget(object):
... label = u'Checkbox'
>>> cbw = CheckboxWidget()
>>> class Form1(object):
... pass
>>> form1 = Form1()
>>> class Form2(object):
... pass
>>> form2 = Form2()
We can now create a generic widget property adapter:
>>> WidgetAttribute = value.StaticValueCreator(
... discriminators = ('widget', 'view')
... )
Creating the widget attribute object, using the helper function above, allows
us to define the discriminators (or the granulatrity) that can be used to
control a widget attribute by an adapter. In our case this is the widget
itself and the form/view in which the widget is displayed. In other words, it
will be possible to register a widget attribute value specifically for a
particular widget, a particular form, or a combination thereof.
Let's now create a label attribute adapter for the text widget, since our
customer does not like the default label:
>>> TextLabel = WidgetAttribute(u'My Text', widget=TextWidget)
The first argument of any static attribute value is the value itself, in our
case the string "My Text". The following keyword arguments are the
discriminators specified in the property factory. Since we only specify the
widget, the label will be available to all widgets. But first we have to
register the adapter:
>>> import zope.component
>>> zope.component.provideAdapter(TextLabel, name='label')
The name of the adapter is the attribute name of the widget. Let's now see how
we can get the label:
>>> from z3c.form import interfaces
>>> staticValue = zope.component.getMultiAdapter(
... (tw, form1), interfaces.IValue, name='label')
>>> staticValue
<StaticValue u'My Text'>
The resulting value object has one public method ``get()``, which returns the
actual value:
>>> staticValue.get()
u'My Text'
As we said before, the value should be available to all forms, ...
>>> zope.component.getMultiAdapter(
... (tw, form2), interfaces.IValue, name='label')
<StaticValue u'My Text'>
... but only to the ``TextWidget``:
>>> zope.component.getMultiAdapter(
... (cbw, form2), interfaces.IValue, name='label')
Traceback (most recent call last):
...
ComponentLookupError: ((<CheckboxWidget...>, <Form2...>),
<InterfaceClass ...IValue>, 'label')
By the way, the attribute adapter factory notices, if you specify a
discriminator that was not specified:
>>> WidgetAttribute(u'My Text', form=Form2)
Traceback (most recent call last):
...
ValueError: One or more keyword arguments did not match the discriminators.
>>> WidgetAttribute.discriminators
('widget', 'view')
Computed Value Adapter
----------------------
A second implementation of the value adapter in the evaluated value, where one
can specify a function that computes the value to be returned. The only
argument to the function is the value adapter instance itself, which then
contains all the discriminators as specified when creating the generic widget
attribute factory. Let's take the same use case as before, but generating the
value as follows:
>>> def getLabelValue(adapter):
... return adapter.widget.label + ' (1)'
Now we create the value adapter for it:
>>> WidgetAttribute = value.ComputedValueCreator(
... discriminators = ('widget', 'view')
... )
>>> TextLabel = WidgetAttribute(getLabelValue, widget=TextWidget)
After registering the adapter, ...
>>> zope.component.provideAdapter(TextLabel, name='label')
we now get the answers:
>>> from z3c.form import interfaces
>>> zope.component.getMultiAdapter(
... (tw, form1), interfaces.IValue, name='label')
<ComputedValue u'Text (1)'>
__Note__: The two implementations of the attribute value adapters are not
meant to be canonical features that must always be used. The API is
kept simple to allow you to quickly implement your own value
adapter.
Automatic Interface Assignment
------------------------------
Oftentimes it is desirable to register an attribute value adapter for an
instance. A good example is a field, so let's create a small schema:
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... firstName = zope.schema.TextLine(title=u'First Name')
... lastName = zope.schema.TextLine(title=u'Last Name')
The customer now requires that the title -- which is the basis of the widget
label for field widgets -- of the last name should be "Surname". Until now the
option was to write a new schema changing the title. With this attribute value
module, as introduced thus far, we would need to provide a special interface
for the last name field, since registering a label adapter for all text fields
would also change the first name.
Before demonstrating the solution to this problem, let's first create a field
attribute value:
>>> FieldAttribute = value.StaticValueCreator(
... discriminators = ('field',)
... )
We can now create the last name title, changing only the title of the
``lastName`` field. Instead of passing in an interface of class as the field
discriminator, we pass in the field instance:
>>> LastNameTitle = FieldAttribute(u'Surname', field=IPerson['lastName'])
The attribute value factory will automatically detect instances, create an
interface on the fly, directly provide it on the field and makes it the
discriminator interface for the adapter registratioon.
So after registering the adapter, ...
>>> zope.component.provideAdapter(LastNameTitle, name='title')
the adapter is only available to the last name field and not the first name:
>>> zope.component.queryMultiAdapter(
... (IPerson['lastName'],), interfaces.IValue, name='title')
<StaticValue u'Surname'>
>>> zope.component.queryMultiAdapter(
... (IPerson['firstName'],), interfaces.IValue, name='title')
=============
Data Managers
=============
For the longest time the way widgets retrieved and stored their values on the
actual content/model was done by binding the field to a context and then
setting and getting the attribute from it. This has several distinct design
shortcomings:
1. The field has too much responsibility by knowing about its implementations.
2. There is no way of redefining the method used to store and access data
other than rewriting fields.
3. Finding the right content/model to modify is an implicit policy: Find an
adapter for the field's schema and then set the value there.
While implementing some real-world projects, we noticed that this approach is
too limiting and we often could not use the form framework when we wanted or
had to jump through many hoops to make it work for us. For example, if we want
to display a form to collect data that does not correspond to a set of content
components, we were forced to not only write a schema for the form, but also
implement that schema as a class. but all we wanted was a dictionary. For
edit-form like tasks we often also had an initial dictionary, which we just
wanted modified.
Data managers abstract the getting and setting of the data. A data manager is
responsible for setting one piece of data in a particular context.
>>> from z3c.form import datamanager
Attribute Field Manager
-----------------------
The most common case, of course, is the management of class attributes through
fields. In this case, the data manager needs to know about the context and the
field it is managing the data for.
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... name = zope.schema.TextLine(
... title=u'Name',
... default=u'<no name>')
... phone = zope.schema.TextLine(
... title=u'Phone')
>>> class Person(object):
... zope.interface.implements(IPerson)
... name = u''
... def __init__(self, name):
... self.name = name
>>> stephan = Person(u'Stephan Richter')
We can now instantiate the data manager for Stephan's name:
>>> nameDm = datamanager.AttributeField(stephan, IPerson['name'])
The data manager consists of a few simple methods to accomplish its
purpose. Getting the value is done using the ``get()`` or ``query()`` method:
>>> nameDm.get()
u'Stephan Richter'
>>> nameDm.query()
u'Stephan Richter'
The value can be set using ``set()``:
>>> nameDm.set(u'Stephan "Caveman" Richter')
>>> nameDm.get()
u'Stephan "Caveman" Richter'
>>> stephan.name
u'Stephan "Caveman" Richter'
If an attribute is not available, ``get()`` fails and ``query()`` returns a
default value:
>>> phoneDm = datamanager.AttributeField(stephan, IPerson['phone'])
>>> phoneDm.get()
Traceback (most recent call last):
...
AttributeError: 'Person' object has no attribute 'phone'
>>> phoneDm.query()
<NO_VALUE>
>>> phoneDm.query('nothing')
'nothing'
A final feature that is supported by the data manager is the check whether a
value can be accessed and written. When the context is not security proxied,
both, accessing and writing, is allowed:
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
True
To demonstrate the behavior for a security-proxied component, we first have to
provide security declarations for our person:
>>> from zope.security.management import endInteraction
>>> from zope.security.management import newInteraction
>>> from zope.security.management import setSecurityPolicy
>>> import z3c.form.testing
>>> endInteraction()
>>> newPolicy = z3c.form.testing.SimpleSecurityPolicy()
>>> newPolicy.allowedPermissions = ('View', 'Edit')
>>> oldpolicy = setSecurityPolicy(newPolicy)
>>> newInteraction()
>>> from zope.security.checker import Checker
>>> from zope.security.checker import defineChecker
>>> personChecker = Checker({'name':'View', 'name':'Edit'})
>>> defineChecker(Person, personChecker)
We now need to wrap stephan into a proxy:
>>> protectedStephan = zope.security.checker.ProxyFactory(stephan)
Since we are not logged in as anyone, we cannot acces or write the value:
>>> nameDm = datamanager.AttributeField(protectedStephan, IPerson['name'])
>>> nameDm.canAccess()
False
>>> nameDm.canWrite()
False
Clearly, this also means that ``get()`` and ``set()`` are also shut off:
>>> nameDm.get()
Traceback (most recent call last):
...
Unauthorized: (<Person object at ...>, 'name', 'Edit')
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
ForbiddenAttribute: ('name', <Person object at ...>)
Now we have to setup the security system and "log in" as a user:
>>> newPolicy.allowedPermissions = ('View', 'Edit')
>>> newPolicy.loggedIn = True
The created principal, with which we are logged in now, can only access the
attribute:
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
False
Thus only the ``get()`` method is allowed:
>>> nameDm.get()
u'Stephan "Caveman" Richter'
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
ForbiddenAttribute: ('name', <Person object at ...>)
If field's schema is not directly provided by the context, the datamanager
will attempt to find an adapter. Let's give the person an address for example:
>>> class IAddress(zope.interface.Interface):
... city = zope.schema.TextLine(title=u'City')
>>> class Address(object):
... zope.component.adapts(IPerson)
... zope.interface.implements(IAddress)
... def __init__(self, person):
... self.person = person
... @apply
... def city():
... def get(self):
... return getattr(self.person, '_city', None)
... def set(self, value):
... self.person._city = value
... return property(get, set)
>>> zope.component.provideAdapter(Address)
Now we can create a data manager for the city attribute:
>>> cityDm = datamanager.AttributeField(stephan, IAddress['city'])
We can access and write to the city attribute:
>>> cityDm.canAccess()
True
>>> cityDm.canWrite()
True
Initially there is no value, but of course we can create one:
>>> cityDm.get()
>>> cityDm.set(u'Maynard')
>>> cityDm.get()
u'Maynard'
The value can be accessed through the adapter itself as well:
>>> IAddress(stephan).city
u'Maynard'
While we think that implicitly looking up an adapter is not the cleanest
solution, it allows us to mimic the behavior of ``zope.formlib``. We think
that we will eventually provide alternative ways to accomplish the same in a
more explicit way.
If we try to set a value that is read-only, a type error is raised:
>>> readOnlyName = zope.schema.TextLine(
... __name__='name',
... readonly=True)
>>> nameDm = datamanager.AttributeField(stephan, readOnlyName)
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
TypeError: Can't set values on read-only fields
(name=name, class=__builtin__.Person)
Finally, we instantiate the data manager with a ``zope.schema``
field. And we can access the different methods like before.
>>> nameDm = datamanager.AttributeField(
... stephan, zope.schema.TextLine(__name__ = 'name'))
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
True
>>> nameDm.get()
u'Stephan "Caveman" Richter'
>>> nameDm.query()
u'Stephan "Caveman" Richter'
>>> nameDm.set(u'Stephan Richter')
>>> nameDm.get()
u'Stephan Richter'
Dictionary Field Manager
------------------------
Another implementation of the data manager interface is provided by the
dictionary field manager, which does not expect an instance with attributes as
its context, but a dictionary. It still uses a field to determine the key to
modify.
>>> personDict = {}
>>> nameDm = datamanager.DictionaryField(personDict, IPerson['name'])
The datamanager can really only deal with dictionaries and no other types:
>>> datamanager.DictionaryField([], IPerson['name'])
Traceback (most recent call last):
...
ValueError: Data are not a dictionary: <type 'list'>
Let's now access the name:
>>> nameDm.get()
Traceback (most recent call last):
...
KeyError: 'name'
>>> nameDm.query()
<NO_VALUE>
Initially we get the default value (as specified in the field), since the
person dictionariy has no entry. If no default value has been specified in the
field, the missing value is returned.
Now we set a value and it should be available:
>>> nameDm.set(u'Roger Ineichen')
>>> nameDm.get()
u'Roger Ineichen'
>>> personDict
{'name': u'Roger Ineichen'}
Since this dictionary is not security proxied, any field can be accessed and
written to:
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
True
As with the attribute data manager, readonly fields cannot be set:
>>> nameDm = datamanager.DictionaryField(personDict, readOnlyName)
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
TypeError: Can't set values on read-only fields name=name
Cleanup
-------
We clean up the changes we made in these examples:
>>> endInteraction()
>>> ignore = setSecurityPolicy(oldpolicy)
==============
Data Converter
==============
The data converter is the component that converts an internal data value as
described by a field to an external value as required by a widget and vice
versa. The goal of the converter is to avoid field and widget proliferation
solely to handle different types of values. The purpose of fields is to
describe internal data types and structures and that of widgets to provide one
particular mean of input.
The only two discriminators for the converter are the field and the widget.
Let's look at the ``Int`` field to ``TextWidget`` converter as an example:
>>> import zope.schema
>>> age = zope.schema.Int(
... __name__='age',
... title=u'Age',
... min=0)
>>> from z3c.form.testing import TestRequest
>>> from z3c.form import widget
>>> text = widget.Widget(TestRequest())
>>> from z3c.form import converter
>>> conv = converter.FieldDataConverter(age, text)
The field data converter is a generic data converter that can be used for all
fields that implement ``IFromUnicode``. If, for example, a ``Date`` field
-- which does not provide ``IFromUnicode`` -- is passed in, then a type error
is raised:
>>> converter.FieldDataConverter(zope.schema.Date(), text)
Traceback (most recent call last):
...
TypeError: Field of type ``Date`` must provide ``IFromUnicode``.
A named field will tell it's name:
>>> converter.FieldDataConverter(zope.schema.Date(__name__="foobar"), text)
Traceback (most recent call last):
...
TypeError: Field ``foobar`` of type ``Date`` must provide ``IFromUnicode``.
However, the ``FieldDataConverter`` is registered for ``IField``, since many
fields (like ``Decimal``) for which we want to create custom converters
provide ``IFromUnicode`` more specifically than their characterizing interface
(like ``IDecimal``).
The converter can now convert any integer to a the value the test widget deals
with, which is an ASCII string:
>>> conv.toWidgetValue(34)
u'34'
When the missing value is passed in, an empty string should be returned:
>>> conv.toWidgetValue(age.missing_value)
u''
Of course, values can also be converted from a widget value to field value:
>>> conv.toFieldValue('34')
34
An empty string means simply that the value is missing and the missing value
of the field is returned:
>>> age.missing_value = -1
>>> conv.toFieldValue('')
-1
Of course, trying to convert a non-integer string representation fails in a
conversion error:
>>> conv.toFieldValue('3.4')
Traceback (most recent call last):
...
ValueError: invalid literal for int(): 3.4
Also, the conversion to the field value also validates the data; in this case
negative values are not allowed:
>>> conv.toFieldValue('-34')
Traceback (most recent call last):
...
TooSmall: (-34, 0)
That's pretty much the entire API. When dealing with converters within the
component architecture, everything is a little bit simpler. So let's register
the converter:
>>> import zope.component
>>> zope.component.provideAdapter(converter.FieldDataConverter)
Once we ensure that our widget is a text widget, we can lookup the adapter:
>>> import zope.interface
>>> from z3c.form import interfaces
>>> zope.interface.alsoProvides(text, interfaces.ITextWidget)
>>> zope.component.getMultiAdapter((age, text), interfaces.IDataConverter)
<FieldDataConverter converts from Int to Widget>
For field-widgets there is a helper adapter that makes the lookup even
simpler:
>>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)
After converting our simple widget to a field widget,
>>> fieldtext = widget.FieldWidget(age, text)
we can now lookup the data converter adapter just by the field widget itself:
>>> interfaces.IDataConverter(fieldtext)
<FieldDataConverter converts from Int to Widget>
Number Data Converters
----------------------
As hinted on above, the package provides a specific data converter for each of
the three main numerical types: ``int``, ``float``, ``Decimal``. Specifically,
those data converters support full localization of the number formatting.
>>> age = zope.schema.Int()
>>> intdc = converter.IntegerDataConverter(age, text)
>>> intdc
<IntegerDataConverter converts from Int to Widget>
Since the age is so small, the formatting is trivial:
>>> intdc.toWidgetValue(34)
u'34'
But if we increase the number, the grouping seprator will be used:
>>> intdc.toWidgetValue(3400)
u'3,400'
An empty string is returned, if the missing value is passed in:
>>> intdc.toWidgetValue(None)
u''
Of course, parsing these outputs again, works as well:
>>> intdc.toFieldValue(u'34')
34
But if we increase the number, the grouping seprator will be used:
>>> intdc.toFieldValue(u'3,400')
3400
Luckily our parser is somewhat forgiving, and even allows for missing group
characters:
>>> intdc.toFieldValue(u'3400')
3400
If an empty string is passed in, the missing value of the field is returned:
>>> intdc.toFieldValue(u'')
Finally, if the input does not match at all, then a validation error is
returned:
>>> intdc.toFieldValue(u'fff')
Traceback (most recent call last):
...
FormatterValidationError:
(u'The entered value is not a valid integer literal.', u'fff')
The formatter validation error derives from the regular validation error, but
allows you to specify the message that is output when asked for the
documentation:
>>> err = converter.FormatterValidationError(u'Something went wrong.', None)
>>> err.doc()
u'Something went wrong.'
Let's now look at the float data converter.
>>> rating = zope.schema.Float()
>>> floatdc = converter.FloatDataConverter(rating, text)
>>> floatdc
<FloatDataConverter converts from Float to Widget>
Again, you can format and parse values:
>>> floatdc.toWidgetValue(7.43)
u'7.43'
>>> floatdc.toWidgetValue(10239.43)
u'10,239.43'
>>> floatdc.toFieldValue(u'7.43')
7.4299999999999997
>>> floatdc.toFieldValue(u'10,239.43')
10239.43
The error message, however, is customized to the floating point:
>>> floatdc.toFieldValue(u'fff')
Traceback (most recent call last):
...
FormatterValidationError:
(u'The entered value is not a valid decimal literal.', u'fff')
The decimal converter works like the other two before.
>>> money = zope.schema.Decimal()
>>> decimaldc = converter.DecimalDataConverter(money, text)
>>> decimaldc
<DecimalDataConverter converts from Decimal to Widget>
Formatting and parsing should work just fine:
>>> import decimal
>>> decimaldc.toWidgetValue(decimal.Decimal('7.43'))
u'7.43'
>>> decimaldc.toWidgetValue(decimal.Decimal('10239.43'))
u'10,239.43'
>>> decimaldc.toFieldValue(u'7.43')
Decimal("7.43")
>>> decimaldc.toFieldValue(u'10,239.43')
Decimal("10239.43")
Again, the error message, is customized to the floating point:
>>> floatdc.toFieldValue(u'fff')
Traceback (most recent call last):
...
FormatterValidationError:
(u'The entered value is not a valid decimal literal.', u'fff')
Date Data Converter
-------------------
Since the ``Date`` field does not provide ``IFromUnicode``, we have to provide
a custom data converter. This default one is not very sophisticated and is
inteded for use with the text widget:
>>> date = zope.schema.Date()
>>> ddc = converter.DateDataConverter(date, text)
>>> ddc
<DateDataConverter converts from Date to Widget>
Dates are simply converted to ISO format:
>>> import datetime
>>> bday = datetime.date(1980, 1, 25)
>>> ddc.toWidgetValue(bday)
u'80/01/25'
If the date is the missing value, an empty string is returned:
>>> ddc.toWidgetValue(None)
u''
The converter only knows how to convert this particular format back to a
datetime value:
>>> ddc.toFieldValue(u'80/01/25')
datetime.date(1980, 1, 25)
By default the converter converts missing input to missin_input value:
>>> ddc.toFieldValue(u'') is None
True
If the passed in string cannot be parsed, a formatter validation error is
raised:
>>> ddc.toFieldValue(u'8.6.07')
Traceback (most recent call last):
...
FormatterValidationError: ("The datetime string did not match the pattern
u'yy/MM/dd'.", u'8.6.07')
Time Data Converter
-------------------
Since the ``Time`` field does not provide ``IFromUnicode``, we have to provide
a custom data converter. This default one is not very sophisticated and is
inteded for use with the text widget:
>>> time = zope.schema.Time()
>>> tdc = converter.TimeDataConverter(time, text)
>>> tdc
<TimeDataConverter converts from Time to Widget>
Dates are simply converted to ISO format:
>>> noon = datetime.time(12, 0, 0)
>>> tdc.toWidgetValue(noon)
u'12:00'
The converter only knows how to convert this particular format back to a
datetime value:
>>> tdc.toFieldValue(u'12:00')
datetime.time(12, 0)
By default the converter converts missing input to missin_input value:
>>> tdc.toFieldValue(u'') is None
True
Datetime Data Converter
-----------------------
Since the ``Datetime`` field does not provide ``IFromUnicode``, we have to
provide a custom data converter. This default one is not very sophisticated
and is inteded for use with the text widget:
>>> dtField = zope.schema.Datetime()
>>> dtdc = converter.DatetimeDataConverter(dtField, text)
>>> dtdc
<DatetimeDataConverter converts from Datetime to Widget>
Dates are simply converted to ISO format:
>>> bdayNoon = datetime.datetime(1980, 1, 25, 12, 0, 0)
>>> dtdc.toWidgetValue(bdayNoon)
u'80/01/25 12:00'
The converter only knows how to convert this particular format back to a
datetime value:
>>> dtdc.toFieldValue(u'80/01/25 12:00')
datetime.datetime(1980, 1, 25, 12, 0)
By default the converter converts missing input to missin_input value:
>>> dtdc.toFieldValue(u'') is None
True
Timedelta Data Converter
------------------------
Since the ``Timedelta`` field does not provide ``IFromUnicode``, we have to
provide a custom data converter. This default one is not very sophisticated
and is inteded for use with the text widget:
>>> timedelta = zope.schema.Timedelta()
>>> tddc = converter.TimedeltaDataConverter(timedelta, text)
>>> tddc
<TimedeltaDataConverter converts from Timedelta to Widget>
Dates are simply converted to ISO format:
>>> allOnes = datetime.timedelta(1, 3600+60+1)
>>> tddc.toWidgetValue(allOnes)
u'1 day, 1:01:01'
The converter only knows how to convert this particular format back to a
datetime value:
>>> tddc.toFieldValue(u'1 day, 1:01:01')
datetime.timedelta(1, 3661)
By default the converter converts missing input to missin_input value:
>>> tddc.toFieldValue(u'') is None
True
File Upload Data Converter
--------------------------
Since the ``Bytes`` field can contain a ``FileUpload`` object, we have to make
sure we can convert ``FileUpload`` objects to bytes too.
>>> import z3c.form.browser.file
>>> fileWidget = z3c.form.browser.file.FileWidget(TestRequest())
>>> bytes = zope.schema.Bytes()
>>> fudc = converter.FileUploadDataConverter(bytes, fileWidget)
>>> fudc
<FileUploadDataConverter converts from Bytes to FileWidget>
The file upload widget usually provides a file object. But sometimes is also
provides a string:
>>> simple = 'foobar'
>>> fudc.toFieldValue(simple)
'foobar'
The converter can also convert ``FileUpload`` objects. So we need to setup a
fields storage stub ...
>>> class FieldStorageStub:
... def __init__(self, file):
... self.file = file
... self.headers = {}
... self.filename = 'foo.bar'
and a ``FileUpload`` component:
>>> import cStringIO
>>> from zope.publisher.browser import FileUpload
>>> myfile = cStringIO.StringIO('File upload contents.')
>>> aFieldStorage = FieldStorageStub(myfile)
>>> myUpload = FileUpload(aFieldStorage)
Let's try to convert the input now:
>>> fudc.toFieldValue(myUpload)
'File upload contents.'
By default the converter converts missing input to the ``NOT_CHANGED`` value:
>>> fudc.toFieldValue('')
<NOT_CHANGED>
This allows machinery later to ignore the field without sending all the data
around.
If we get an empty filename in a ``FileUpload`` obejct, we also get the
``missing_value``. But this really means that there was an error somewhere in
the upload, since you are normaly not able to upload a file without a filename:
>>> class EmptyFilenameFieldStorageStub:
... def __init__(self, file):
... self.file = file
... self.headers = {}
... self.filename = ''
>>> myfile = cStringIO.StringIO('')
>>> aFieldStorage = EmptyFilenameFieldStorageStub(myfile)
>>> myUpload = FileUpload(aFieldStorage)
>>> bytes = zope.schema.Bytes()
>>> fudc = converter.FileUploadDataConverter(bytes, fileWidget)
>>> fudc.toFieldValue(myUpload) is None
True
There is also a ``ValueError`` if we don't get a seekable file from the
``FieldStorage`` during the upload:
>>> myfile = ''
>>> aFieldStorage = FieldStorageStub(myfile)
>>> myUpload = FileUpload(aFieldStorage)
>>> bytes = zope.schema.Bytes()
>>> fudc = converter.FileUploadDataConverter(bytes, fileWidget)
>>> fudc.toFieldValue(myUpload) is None
Traceback (most recent call last):
...
ValueError: (u'Bytes data are not a file object', ...AttributeError...)
When converting to the widget value, not conversion should be done, since
bytes are not convertable in that sense.
>>> fudc.toWidgetValue('bytes')
'bytes'
When the file upload widget is not used and a text-based widget is desired,
then the regular field data converter will be chosen. Using a text widget,
however, must be setup manually in the form with code like this::
fields['bytesField'].widgetFactory = TextWidget
Sequence Data Converter
-----------------------
For widgets and fields that work with choices of a sequence, a special data
converter is required that works with terms. A prime example is a choice
field. Before we can use the converter, we have to register some adapters:
>>> from z3c.form import term
>>> import zc.sourcefactory.browser.source
>>> import zc.sourcefactory.browser.token
>>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
>>> zope.component.provideAdapter(term.ChoiceTermsSource)
>>> zope.component.provideAdapter(term.ChoiceTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.source.FactoredTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.token.fromInteger)
The choice fields can be used together with vocabularies and sources.
Using vocabulary
~~~~~~~~~~~~~~~~
Let's now create a choice field (using a vocabulary) and a widget:
>>> from zope.schema.vocabulary import SimpleVocabulary
>>> gender = zope.schema.Choice(
... vocabulary = SimpleVocabulary([
... SimpleVocabulary.createTerm(0, 'm', u'male'),
... SimpleVocabulary.createTerm(1, 'f', u'female'),
... ]) )
>>> from z3c.form import widget
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = gender
We now use the field and widget to instantiate the converter:
>>> sdv = converter.SequenceDataConverter(gender, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> sdv.toWidgetValue(0)
['m']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> sdv.toFieldValue(['m'])
0
Sometimes a field is not required. In those cases, the internal value is the
missing value of the field. The converter interprets that as no value being
selected:
>>> gender.missing_value = 'missing'
>>> sdv.toWidgetValue(gender.missing_value)
[]
If the internal value is not a valid item in the terms, it is treated as
missing:
>>> sdv.toWidgetValue(object())
[]
If "no value" has been specified in the widget, the missing value
of the field is returned:
>>> sdv.toFieldValue([u'--NOVALUE--'])
'missing'
An empty list will also cause the missing value to be returned:
>>> sdv.toFieldValue([])
'missing'
Using source
~~~~~~~~~~~~
Let's now create a choice field (using a source) and a widget:
>>> from zc.sourcefactory.basic import BasicSourceFactory
>>> class GenderSourceFactory(BasicSourceFactory):
... _mapping = {0: u'male', 1: u'female'}
... def getValues(self):
... return self._mapping.keys()
... def getTitle(self, value):
... return self._mapping[value]
>>> gender_source = zope.schema.Choice(
... source = GenderSourceFactory())
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = gender_source
We now use the field and widget to instantiate the converter:
>>> sdv = converter.SequenceDataConverter(gender, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> sdv.toWidgetValue(0)
['0']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> sdv.toFieldValue(['0'])
0
Sometimes a field is not required. In those cases, the internalvalue is the
missing value of the field. The converter interprets that as no value being
selected:
>>> gender.missing_value = 'missing'
>>> sdv.toWidgetValue(gender.missing_value)
[]
If "no value" has been specified in the widget, the missing value
of the field is returned:
>>> sdv.toFieldValue([u'--NOVALUE--'])
'missing'
An empty list will also cause the missing value to be returned:
>>> sdv.toFieldValue([])
'missing'
Collection Sequence Data Converter
----------------------------------
For widgets and fields that work with a sequence of choices, another data
converter is required that works with terms. A prime example is a list
field. Before we can use the converter, we have to register the terms adapters:
>>> from z3c.form import term
>>> zope.component.provideAdapter(term.CollectionTerms)
>>> zope.component.provideAdapter(term.CollectionTermsVocabulary)
>>> zope.component.provideAdapter(term.CollectionTermsSource)
Collections can also use either vocabularies or sources.
Using vocabulary
~~~~~~~~~~~~~~~~
Let's now create a list field (using the previously defined field using
a vocabulary) and a widget:
>>> genders = zope.schema.List(value_type=gender)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
We now use the field and widget to instantiate the converter:
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> csdv.toWidgetValue([0])
['m']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> csdv.toFieldValue(['m'])
[0]
Of course, a collection field can also have multiple values:
>>> csdv.toWidgetValue([0, 1])
['m', 'f']
>>> csdv.toFieldValue(['m', 'f'])
[0, 1]
If any of the values are not a valid choice, they are simply ignored:
>>> csdv.toWidgetValue([0, 3])
['m']
Sometimes a field is not required. In those cases, the internal value is the
missing value of the field. The converter interprets that as no values being
given:
>>> genders.missing_value is None
True
>>> csdv.toWidgetValue(genders.missing_value)
[]
For some field, like the ``Set``, the collection type is a tuple. Sigh. In
these cases we use the last entry in the tuple as the type to use:
>>> genders = zope.schema.Set(value_type=gender)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
>>> csdv.toWidgetValue(set([0]))
['m']
>>> csdv.toFieldValue(['m'])
set([0])
Getting Terms
+++++++++++++
As an optimization of this converter, the converter actually does not look up
the terms itself but uses the widget's ``terms`` attribute. If the terms are
not yet retrieved, the converter will ask the widget to do so when in need.
So let's see how this works when getting the widget value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
>>> csdv.toWidgetValue([0])
['m']
>>> seqWidget.terms
<z3c.form.term.CollectionTermsVocabulary object ...>
The same is true when getting the field value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
>>> csdv.toFieldValue(['m'])
set([0])
>>> seqWidget.terms
<z3c.form.term.CollectionTermsVocabulary object ...>
Corner case: Just in case the field has a sequence as ``_type``:
>>> class myField(zope.schema.List):
... _type = (list, tuple)
>>> genders = myField(value_type=gender)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
We now use the field and widget to instantiate the converter:
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
The converter uses the latter type (tuple) to convert:
>>> csdv.toFieldValue(['m'])
(0,)
Using source
~~~~~~~~~~~~
Let's now create a list field (using the previously defined field using
a source) and a widget:
>>> genders_source = zope.schema.List(value_type=gender_source)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
We now use the field and widget to instantiate the converter:
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> csdv.toWidgetValue([0])
['0']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> csdv.toFieldValue(['0'])
[0]
For some field, like the ``Set``, the collection type is a tuple. Sigh. In
these cases we use the last entry in the tuple as the type to use:
>>> genders_source = zope.schema.Set(value_type=gender_source)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
>>> csdv.toWidgetValue(set([0]))
['0']
>>> csdv.toFieldValue(['0'])
set([0])
Getting Terms
+++++++++++++
As an optimization of this converter, the converter actually does not look up
the terms itself but uses the widget's ``terms`` attribute. If the terms are
not yet retrieved, the converter will ask the widget to do so when in need.
So let's see how this works when getting the widget value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
>>> csdv.toWidgetValue([0])
['0']
>>> seqWidget.terms
<z3c.form.term.CollectionTermsSource object ...>
The same is true when getting the field value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
>>> csdv.toFieldValue(['0'])
set([0])
>>> seqWidget.terms
<z3c.form.term.CollectionTermsSource object ...>
Boolean to Single Checkbox Data Converter
-----------------------------------------
The conversion from any field to the single checkbox widget value is a special
case, because it has to be defined what selecting the value means. In the case
of the boolean field, "selected" means ``True`` and if unselected, ``False``
is returned:
>>> boolField = zope.schema.Bool()
>>> bscbx = converter.BoolSingleCheckboxDataConverter(boolField, seqWidget)
>>> bscbx
<BoolSingleCheckboxDataConverter converts from Bool to SequenceWidget>
Let's now convert boolean field to widget values:
>>> bscbx.toWidgetValue(True)
['selected']
>>> bscbx.toWidgetValue(False)
[]
Converting back is equally simple:
>>> bscbx.toFieldValue(['selected'])
True
>>> bscbx.toFieldValue([])
False
Note that this widget has no concept of missing value, since it can only
represent two states by desgin.
Text Lines Data Converter
-------------------------
For sequence widgets and fields that work with a sequence of `TextLine` value
fields, a simple data converter is required. Let's create a list of text lines
field and a widget:
>>> languages = zope.schema.List(
... value_type=zope.schema.TextLine(),
... default=[],
... missing_value=None,
... )
>>> from z3c.form.browser import textlines
>>> tlWidget = textlines.TextLinesWidget(TestRequest())
>>> tlWidget.field = languages
We now use the field and widget to instantiate the converter:
>>> tlc = converter.TextLinesConverter(languages, tlWidget)
We can now convert a real value to a widget value:
>>> tlc.toWidgetValue([u'de', u'fr', u'en'])
u'de\nfr\nen'
The result is always a string, since text lines widgets only deal with textarea
as input field. Of course, we can convert the widget value back to an internal
value:
>>> tlc.toFieldValue('de\nfr\nen')
[u'de', u'fr', u'en']
Each line should be one item:
>>> tlc.toFieldValue('this morning\ntomorrow evening\nyesterday')
[u'this morning', u'tomorrow evening', u'yesterday']
An empty string will also cause the missing value to be returned:
>>> tlc.toFieldValue('') is None
True
It also should work for schema fields that define their type as tuple,
for instance zope.schema.Int declares its type as (int, long).
>>> ids = zope.schema.List(
... value_type=zope.schema.Int(),
... )
Let's illustrate the problem:
>>> zope.schema.Int._type
(<type 'int'>, <type 'long'>)
The converter will use the first one.
>>> tlWidget.field = ids
>>> tlc = converter.TextLinesConverter(ids, tlWidget)
Of course, it still can convert to the widget value:
>>> tlc.toWidgetValue([1,2,3])
u'1\n2\n3'
And back:
>>> tlc.toFieldValue(u'1\n2\n3\n')
[1, 2, 3]
An empty string will also cause the missing value to be returned:
>>> tlc.toFieldValue('') is None
True
Converting Missing value to Widget value returns '':
>>> tlc.toWidgetValue(tlc.field.missing_value)
u''
Just in case the field has sequence as its ``_type``:
>>> class myField(zope.schema.List):
... _type = (list, tuple)
>>> ids = myField(
... value_type=zope.schema.Int(),
... )
The converter will use the latter one.
>>> tlWidget.field = ids
>>> tlc = converter.TextLinesConverter(ids, tlWidget)
Of course, it still can convert to the widget value:
>>> tlc.toWidgetValue([1,2,3])
u'1\n2\n3'
And back:
>>> tlc.toFieldValue(u'1\n2\n3\n')
(1, 2, 3)
Multi Data Converter
--------------------
For multi widgets and fields that work with a sequence of other basic types, a
separate data converter is required. Let's create a list of integers field and
a widget:
>>> numbers = zope.schema.List(
... value_type=zope.schema.Int(),
... default=[],
... missing_value=None,
... )
>>> from z3c.form.browser import multi
>>> multiWidget = multi.MultiWidget(TestRequest())
>>> multiWidget.field = numbers
Before we can convert, we have to regsiter a widget for the integer field:
>>> from z3c.form.browser import text
>>> zope.component.provideAdapter(
... text.TextFieldWidget,
... (zope.schema.Int, TestRequest))
We now use the field and widget to instantiate the converter:
>>> conv = converter.MultiConverter(numbers, multiWidget)
We can now convert a list of integers to the multi-widget internal
representation:
>>> conv.toWidgetValue([1, 2, 3])
[u'1', u'2', u'3']
If the value is the missing value, an empty list is returned:
>>> conv.toWidgetValue(None)
[]
Now, let's look at the reverse:
>>> conv.toFieldValue([u'1', u'2', u'3'])
[1, 2, 3]
If the list is empty, the missing value is returned:
>>> conv.toFieldValue([]) is None
True
=====
Terms
=====
Terms are used to provide choices for sequence widgets or any other construct
needing them. Since Zope 3 already has sources and vocabularies, the base
terms class simply builds on them.
Vocabularies
------------
Thus, let's create a vocabulary first:
>>> from zope.schema import vocabulary
>>> ratings = vocabulary.SimpleVocabulary([
... vocabulary.SimpleVocabulary.createTerm(0, '0', u'bad'),
... vocabulary.SimpleVocabulary.createTerm(1, '1', u'okay'),
... vocabulary.SimpleVocabulary.createTerm(2, '2', u'good')
... ])
Terms
~~~~~
Now we can create the terms object:
>>> from z3c.form import term
>>> terms = term.Terms()
>>> terms.terms = ratings
Getting a term from a given value is simple:
>>> terms.getTerm(0).title
u'bad'
>>> terms.getTerm(3)
Traceback (most recent call last):
...
LookupError: 3
When converting values from their Web representation back to the internal
representation, we have to be able to look up a term by its token:
>>> terms.getTermByToken('0').title
u'bad'
>>> terms.getTerm('3')
Traceback (most recent call last):
...
LookupError: 3
However, often we just want the value so asking for the value that is
represented by a token saves usually one line of code:
>>> terms.getValue('0')
0
>>> terms.getValue('3')
Traceback (most recent call last):
...
LookupError: 3
You can also iterate through all terms:
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Or ask how many terms you have in the first place:
>>> len(terms)
3
Finally the API allows you to check whether a particular value is available in
the terms:
>>> 0 in terms
True
>>> 3 in terms
False
Now, there are several terms implementations that were designed for particular
fields. Within the framework, terms are used as adapters with the follwoing
discriminators: context, request, form, field, vocabulary/source and widget.
Choice field
~~~~~~~~~~~~
The first terms implementation is for ``Choice`` fields. Choice fields
unfortunately can have a vocabulary and a source which behave differently.
Let's have a look a the vocabulary first:
>>> import zope.component
>>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
>>> import z3c.form.testing
>>> request = z3c.form.testing.TestRequest()
>>> import z3c.form.widget
>>> widget = z3c.form.widget.Widget(request)
>>> import zope.schema
>>> ratingField = zope.schema.Choice(
... title=u'Rating',
... vocabulary=ratings)
>>> terms = term.ChoiceTerms(
... None, request, None, ratingField, widget)
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Sometimes choice fields only specify a vocabulary name and the actual
vocabulary is looked up at run time.
>>> ratingField2 = zope.schema.Choice(
... title=u'Rating',
... vocabulary='Ratings')
Initially we get an error because the "Ratings" vocabulary is not defined:
>>> terms = term.ChoiceTerms(
... None, request, None, ratingField2, widget)
Traceback (most recent call last):
...
VocabularyRegistryError: unknown vocabulary: 'Ratings'
Let's now register the vocabulary under this name:
>>> def RatingsVocabulary(obj):
... return ratings
>>> from zope.schema import vocabulary
>>> vr = vocabulary.getVocabularyRegistry()
>>> vr.register('Ratings', RatingsVocabulary)
We should now be able to get all terms as before:
>>> terms = term.ChoiceTerms(
... None, request, None, ratingField, widget)
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Bool fields
+++++++++++
A similar terms implementation exists for a ``Bool`` field:
>>> truthField = zope.schema.Bool()
>>> terms = term.BoolTerms(None, None, None, truthField, None)
>>> [entry.title for entry in terms]
[u'yes', u'no']
In case you don't like the choice of 'yes' and 'no' for the labels, we
can subclass the ``BoolTerms`` class to control the display labels.
>>> class MyBoolTerms(term.BoolTerms):
... trueLabel = u'True'
... falseLabel = u'False'
>>> terms = MyBoolTerms(None, None, None, truthField, None)
>>> [entry.title for entry in terms]
[u'True', u'False']
Collections
+++++++++++
Finally, there are a terms adapters for all collections. But we have to
register some adapters before using it:
>>> from z3c.form import term
>>> zope.component.provideAdapter(term.CollectionTerms)
>>> zope.component.provideAdapter(term.CollectionTermsVocabulary)
>>> zope.component.provideAdapter(term.CollectionTermsSource)
>>> ratingsField = zope.schema.List(
... title=u'Ratings',
... value_type=ratingField)
>>> terms = term.CollectionTerms(
... None, request, None, ratingsField, widget)
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Sources
-------
Basic sources
~~~~~~~~~~~~~
Basic sources need no context to compute their value. Let's create a
source first:
>>> from zc.sourcefactory.basic import BasicSourceFactory
>>> class RatingSourceFactory(BasicSourceFactory):
... _mapping = {10: u'ugly', 20: u'nice', 30: u'great'}
... def getValues(self):
... return self._mapping.keys()
... def getTitle(self, value):
... return self._mapping[value]
As we did not include the configure.zcml of zc.sourcefactory we have
to register some required adapters manually. We also need the
ChoiceTermsSource adapter:
>>> import zope.component
>>> import zc.sourcefactory.browser.source
>>> import zc.sourcefactory.browser.token
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.source.FactoredTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.token.fromInteger)
>>> zope.component.provideAdapter(term.ChoiceTermsSource)
Choice fields
+++++++++++++
Sources can be used with ``Choice`` fields like vocabularies. First
we create a field based on the source:
>>> sourceRatingField = zope.schema.Choice(
... title=u'Sourced Rating',
... source=RatingSourceFactory())
We connect the field to a widget to see the ITerms adapter for sources
at work:
>>> terms = term.ChoiceTerms(
... None, request, None, sourceRatingField, widget)
Iterating over the terms adapter returnes the term objects:
>>> [entry for entry in terms]
[<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>]
>>> len(terms)
3
>>> [entry.token for entry in terms]
['10', '20', '30']
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
Using a token it is possible to look up the term and the value:
>>> terms.getTermByToken('20').title
u'nice'
>>> terms.getValue('30')
30
With can test if a value is in the source:
>>> 30 in terms
True
>>> 25 in terms
False
Collections
+++++++++++
Finally, there are terms adapters for all collections:
>>> sourceRatingsField = zope.schema.List(
... title=u'Sourced Ratings',
... value_type=sourceRatingField)
>>> terms = term.CollectionTerms(
... None, request, None, sourceRatingsField, widget)
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
Contextual sources
~~~~~~~~~~~~~~~~~~
Contextual sources depend on the context they are called on. Let's
create a context and a contextual source:
>>> from zc.sourcefactory.contextual import BasicContextualSourceFactory
>>> class RatingContext(object):
... base_value = 10
>>> class ContextualRatingSourceFactory(BasicContextualSourceFactory):
... _mapping = {10: u'ugly', 20: u'nice', 30: u'great'}
... def getValues(self, context):
... return [context.base_value + x for x in self._mapping.keys()]
... def getTitle(self, context, value):
... return self._mapping[value - context.base_value]
As we did not include the configure.zcml of zc.sourcefactory we have
to register some required adapters manually. We also need the
ChoiceTermsSource adapter:
>>> import zope.component
>>> import zc.sourcefactory.browser.source
>>> import zc.sourcefactory.browser.token
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.source.FactoredContextualTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.token.fromInteger)
>>> zope.component.provideAdapter(term.ChoiceTermsSource)
Choice fields
+++++++++++++
Contextual sources can be used with ``Choice`` fields like
vocabularies. First we create a field based on the source:
>>> contextualSourceRatingField = zope.schema.Choice(
... title=u'Context Sourced Rating',
... source=ContextualRatingSourceFactory())
We create an context object and connect the field to a widget to see
the ITerms adapter for sources at work:
>>> rating_context = RatingContext()
>>> rating_context.base_value = 100
>>> terms = term.ChoiceTerms(
... rating_context, request, None, contextualSourceRatingField, widget)
Iterating over the terms adapter returnes the term objects:
>>> [entry for entry in terms]
[<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>]
>>> len(terms)
3
>>> [entry.token for entry in terms]
['110', '120', '130']
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
Using a token, it is possible to look up the term and the value:
>>> terms.getTermByToken('120').title
u'nice'
>>> terms.getValue('130')
130
With can test if a value is in the source:
>>> 130 in terms
True
>>> 125 in terms
False
Collections
+++++++++++
Finally, there are terms adapters for all collections:
>>> contextualSourceRatingsField = zope.schema.List(
... title=u'Contextual Sourced Ratings',
... value_type=contextualSourceRatingField)
>>> terms = term.CollectionTerms(
... rating_context, request, None, contextualSourceRatingsField, widget)
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
=============================
Utility Functions and Classes
=============================
This file documents the utility functions and classes that are otherwise not
tested.
>>> from z3c.form import util
``creeateId(name)`` Function
----------------------------
This function converts an arbitrary unicode string into a valid Python
identifier. If the name is a valid identifier, then it is just returned, but
all upper case letters are lowered:
>>> util.createId(u'Change')
'change'
>>> util.createId(u'Change_2')
'change_2'
If a name is not a valid identifier, a hex code of the string is created:
>>> util.createId(u'Change 3')
'4368616e67652033'
The function can also handle non-ASCII characters:
>>> util.createId(u'&#196;ndern')
'c383c2846e6465726e'
``createCSSId(name)`` Function
------------------------------
This function takes any unicode name and coverts it into an id that
can be easily referenced by CSS selectors. Characters that are in the
ascii alphabet, are numbers, or are '-' or '_' will be left the same.
All other characters will be converted to ordinal numbers:
>>> util.createCSSId(u'NormalId')
'NormalId'
>>> util.createCSSId(u'&#1593;&#1614;&#1585;&#1614;')
'c398c2b9c399c28ec398c2b1c399c28e'
>>> util.createCSSId(u'This has spaces')
'This20has20spaces'
``getWidgetById(form, id)`` Function
------------------------------------
Given a form and a widget id, this function extracts the widget for you. First
we need to create a properly developed form:
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... name = zope.schema.TextLine(title=u'Name')
>>> from z3c.form import form, field
>>> class AddPerson(form.AddForm):
... fields = field.Fields(IPerson)
>>> from z3c.form import testing
>>> testing.setupFormDefaults()
>>> addPerson = AddPerson(None, testing.TestRequest())
>>> addPerson.update()
We can now ask for the widget:
>>> util.getWidgetById(addPerson, 'form-widgets-name')
<TextWidget 'form.widgets.name'>
The widget id can be split into a prefix and a widget name. The id must always
start with the correct prefix, otherwise a value error is raised:
>>> util.getWidgetById(addPerson, 'myform-widgets-name')
Traceback (most recent call last):
...
ValueError: Name 'myform.widgets.name' must start with prefix 'form.widgets.'
If the widget is not found but the prefix is correct, ``None`` is returned:
>>> util.getWidgetById(addPerson, 'form-widgets-myname') is None
True
``extractFileName(form, id, cleanup=True, allowEmptyPostfix=False)`` Function
-----------------------------------------------------------------------------
Test the filename extraction method:
>>> class IDocument(zope.interface.Interface):
... data = zope.schema.Bytes(title=u'Data')
Define a widgets stub and a upload widget stub class and setup them as a
faked form:
>>> class FileUploadWidgetStub(object):
... def __init__(self):
... self.filename = None
>>> class WidgetsStub(object):
... def __init__(self):
... self.data = FileUploadWidgetStub()
... self.prefix = 'widgets.'
... def get(self, name, default):
... return self.data
>>> class FileUploadFormStub(form.AddForm):
... def __init__(self):
... self.widgets = WidgetsStub()
...
... def setFakeFileName(self, filename):
... self.widgets.data.filename = filename
Now we can setup the stub form. Note this form is just a fake it's not a real
implementation. We just provide a form like class which simulates the
FileUpload object in the a widget. See z3c.form.browser.file.txt for a real
file upload test uscase:
>>> uploadForm = FileUploadFormStub()
>>> uploadForm.setFakeFileName('foo.txt')
And extract the filename
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
'foo.txt'
Test a unicode filename:
>>> uploadForm.setFakeFileName(u'foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test a windows IE uploaded filename:
>>> uploadForm.setFakeFileName(u'D:\\some\\folder\\foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test another filename:
>>> uploadForm.setFakeFileName(u'D:/some/folder/foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test another filename:
>>> uploadForm.setFakeFileName(u'/tmp/folder/foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test special characters in filename, e.g. dots:
>>> uploadForm.setFakeFileName(u'/tmp/foo.bar.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.bar.txt'
Test some other special characters in filename:
>>> uploadForm.setFakeFileName(u'/tmp/foo-bar.v.0.1.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo-bar.v.0.1.txt'
Test special characters in file path of filename:
>>> uploadForm.setFakeFileName(u'/tmp-v.1.0/foo-bar.v.0.1.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo-bar.v.0.1.txt'
Test optional keyword arguments. But remember it's hard for Zope to guess the
content type for filenames without extensions:
>>> uploadForm.setFakeFileName(u'minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True,
... allowEmptyPostfix=True)
u'minimal'
>>> uploadForm.setFakeFileName(u'/tmp/minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True,
... allowEmptyPostfix=True)
u'minimal'
>>> uploadForm.setFakeFileName(u'D:\\some\\folder\\minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True,
... allowEmptyPostfix=True)
u'minimal'
There will be a ValueError if we get a empty filename by default:
>>> uploadForm.setFakeFileName(u'/tmp/minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
Traceback (most recent call last):
...
ValueError: Missing filename extension.
We also can skip removing a path from a upload. Note only IE will upload a
path in a upload ``<input type="file" ...>`` field:
>>> uploadForm.setFakeFileName(u'/tmp/foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
u'/tmp/foo.txt'
>>> uploadForm.setFakeFileName(u'/tmp-v.1.0/foo-bar.v.0.1.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
u'/tmp-v.1.0/foo-bar.v.0.1.txt'
>>> uploadForm.setFakeFileName(u'D:\\some\\folder\\foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
u'D:\\some\\folder\\foo.txt'
And missing filename extensions are also not allowed by deafault if we skip
the filename:
>>> uploadForm.setFakeFileName(u'/tmp/minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
Traceback (most recent call last):
...
ValueError: Missing filename extension.
``extractContentType(form, id)`` Function
-----------------------------------------
There is also a method which is able to extract the content type for a given
file upload. We can use the stub form from the previous test.
>>> uploadForm = FileUploadFormStub()
>>> uploadForm.setFakeFileName('foo.txt')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'text/plain'
>>> uploadForm.setFakeFileName('foo.gif')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/gif'
>>> uploadForm.setFakeFileName('foo.jpg')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/jpeg'
>>> uploadForm.setFakeFileName('foo.png')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/png'
>>> uploadForm.setFakeFileName('foo.tif')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/tiff'
>>> uploadForm.setFakeFileName('foo.doc')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'application/msword'
>>> uploadForm.setFakeFileName('foo.zip')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'application/zip'
>>> uploadForm.setFakeFileName('foo.unknown')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'text/x-unknown-content-type'
`UniqueOrderedKeys` object
--------------------------
This object manages the keys of a dictionary. It ensures that no values are
added multiple times and retains the original order of the keys.
>>> keys = util.UniqueOrderedKeys([1, 2])
Let's now add another key:
>>> keys.append(3)
>>> keys.data
[1, 2, 3]
Trying to add `3` again causes a `ValueError` exception:
>>> keys.append(3)
Traceback (most recent call last):
...
ValueError: 3
`Manager` object
----------------
The manager object is a base class of a mapping object that keeps track of the
key order as they are added.
>>> manager = util.Manager()
Initially the manager is empty:
>>> len(manager)
0
Since this base class mainly defines a read-interface, we have to add the
values manually:
>>> manager._data_values.append(2)
>>> manager._data_keys.append('b')
>>> manager._data['b'] = 2
>>> manager._data_values.append(1)
>>> manager._data_keys.append('a')
>>> manager._data['a'] = 1
Let's iterate through the manager:
>>> tuple(iter(manager))
('b', 'a')
>>> manager.keys()
['b', 'a']
>>> manager.values()
[2, 1]
>>> manager.items()
[('b', 2), ('a', 1)]
Let's ow look at item access:
>>> 'b' in manager
True
>>> manager.get('b')
2
>>> manager.get('c', 'None')
'None'
It also supports deletion:
>>> del manager['b']
>>> manager.items()
[('a', 1)]
When the `_data_keys` is reset it will always produce a `UniqueOrderedKeys`:
>>> manager._data_keys = []
>>> manager._data_keys
<z3c.form.util.UniqueOrderedKeys ...>
>>> manager._data_keys = util.UniqueOrderedKeys()
>>> manager._data_keys
<z3c.form.util.UniqueOrderedKeys ...>
`SelectionManager` object
-------------------------
The selection manager is an extension to the manager and provides a few more
API functions. Unfortunately, this base class is totally useless without a
sensible constructor:
>>> import zope.interface
>>> class MySelectionManager(util.SelectionManager):
... managerInterface = zope.interface.Interface
...
... def __init__(self, *args):
... super(MySelectionManager, self).__init__()
... args = list(args)
... for arg in args:
... if isinstance(arg, MySelectionManager):
... args += arg.values()
... continue
... self._data_values.append(arg)
... self._data_keys.append(str(arg))
... self._data[str(arg)] = arg
Let's now create two managers:
>>> manager1 = MySelectionManager(1, 2)
>>> manager2 = MySelectionManager(3, 4)
You can add two managers:
>>> manager = manager1 + manager2
>>> manager.values()
[1, 2, 3, 4]
Next, you can select only certain names:
>>> manager.select('1', '2', '3').values()
[1, 2, 3]
Or simply omit a value.
>>> manager.omit('2').values()
[1, 3, 4]
You can also easily copy a manager:
>>> manager.copy() is not manager
True
That's all.
=======
CHANGES
=======
Version 2.0.0 (2009-06-14)
--------------------------
Features
~~~~~~~~
- KGS 3.4 compatibility. This is a real hard thing, because `z3c.form` tests
use `lxml` >= 2.1.1 to check test output, but KGS 3.4 has `lxml`
1.3.6. Therefore we agree on that if tests pass with all package versions
nailed by KGS 3.4 but `lxml` overridden to 2.1.1 then the `z3c.form` package
works with a plain KGS 3.4.
- Removed hard `z3c.ptcompat` and thus `z3c.pt` dependency. If you have
`z3c.ptcompat` on the Python path it will be used.
- Added nested group support. Groups are rendered as fieldsets. Nested
fieldsets are very useful when designing forms.
WARNING: If your group did have an `applyChanges()` (or any added(?)) method
the new one added by this change might not match the signature.
- Added `labelRequired` and `requiredInfo` form attributes. This is useful for
conditional rendering a required info legend in form templates. The
`requiredInfo` label depends by default on a given `labelRequired` message
id and will only return the label if at least one widget field is required.
- Add support for refreshing actions after their execution. This is useful
when button action conditions are changing as a result of action
execution. All you need is to set the `refreshActions` flag of the form to
`True` in your action handler.
- Added support for using sources. Where it was previosly possible to use a
vocabulary it is now also possible to use a source. This works both for
basic and contextual sources.
**IMPORTANT:** The `ChoiceTerms` and `CollectionTerms` in `z3c.form.term`
are now simple functions that query for real `ITerms` adapters for field's
`source` or `value_type` respectively. So if your code inherits the old
`ChoiceTerms` and `CollectionTerms` classes, you'll need to review and adapt
it. See the `z3c.form.term` module and its documentation.
- The new `z3c.form.interfaces.NOT_CHANGED` special value is available to
signal that the current value should be left as is. It's currently handled
in the `z3c.form.form.applyChanges()` function.
- When no file is specified in the file upload widget, instead of overwriting
the value with a missing one, the old data is retained. This is done by
returning the new `NOT_CHANGED` special value from the
`FileUploadDataConvereter`.
- Preliminary support for widgets for the `schema.IObject` field has been
added. However, there is a big caveat, please read the ``object-caveat.txt``
document inside the package.
A new `objectWidgetTemplate` ZCML directive is provided to register widget
templates for specific object field schemas.
- Implemented the `MultiWidget` widget. This widget allows you to use simple
fields like `ITextLine`, `IInt`, `IPassword`, etc. in a `IList` or `ITuple`
sequence.
- Implemented `TextLinesWidget` widget. This widget offers a text area element
and splits lines in sequence items. This is usfull for power user
interfaces. The widget can be used for sequence fields (e.g. `IList`) that
specify a simple value type field (e.g. `ITextLine` or `IInt`).
- Added a new flag `ignoreContext` to the form field, so that one can
individually select which fields should and which ones should not ignore the
context.
- Allow raw request values of sequence widgets to be non-sequence values,
which makes integration with Javascript libraries easier.
- Added support in the file upload widget's testing flavor to specify
'base64'-encoded strings in the hidden text area, so that binary data can be
uploaded as well.
- Allow overriding the `required` widget attribute using `IValue` adapter just
like it's done for `label` and `name` attributes.
- Add the `prompt` attribute of the `SequenceWidget` to the list of adaptable
attributes.
- Added benchmarking suite demonstrating performance gain when using
``z3c.pt``.
- Added support for ``z3c.pt``. Usage is switched on via the "PREFER_Z3C_PT"
environment variable or via ``z3c.ptcompat.config.[enable/diable]()``.
- The `TypeError` message used when a field does not provide `IFormUnicode`
now also contains the type of the field.
- Add support for internationalization of `z3c.form` messages. Added Russian,
French, German and Chinese translations.
- Sphinx documentation for the package can now be created using the new `docs`
script.
- The widget for fields implementing `IChoice` is now looked up by querying
for an adapter for ``(field, field.vocabulary, request)`` so it can be
differentiated according to the type of the source used for the field.
- Move `formErrorsMessage` attribute from `AddForm` and `EditForm` to the
`z3c.form.form.Form` base class as it's very common validation status
message and can be easily reused (especially when translations are
provided).
Refactoring
~~~~~~~~~~~
- Removed compatibility support with Zope 3.3.
- Templates now declare XML namespaces.
- HTML output is now compared using a modified version of the XML-aware output
checker provided by `lxml`.
- Remove unused imports, adjust buildout dependencies in `setup.py`.
- Use the `z3c.ptcompat` template engine compatibility layer.
Fixed Bugs
~~~~~~~~~~
- **IMPORTANT** - The signature of `z3c.form.util.extractFileName` function
changed because of spelling mistake fix in argument name. The
`allowEmtpyPostFix` is now called `allowEmptyPostfix` (note `Empty` instead
of `Emtpy` and `Postfix` instead of `PostFix`).
- **IMPORTANT** - The `z3c.form.interfaces.NOVALUE` special value has been
renamed to `z3c.form.interfaces.NO_VALUE` to follow the common naming
style. The backward-compatibility `NOVALUE` name is still in place, but the
`repr` output of the object has been also changed, thus it may break your
doctests.
- When dealing with `Bytes` fields, we should do a null conversion when going
to its widget value.
- `FieldWidgets` update method were appending keys and values within each
update call. Now the `util.Manager` uses a `UniqueOrderedKeys`
implementation which will ensure that we can't add duplicated manager
keys. The implementation also ensures that we can't override the
`UniqueOrderedKeys` instance with a new list by using a decorator. If this
`UniqueOrderedKeys` implementation doesn't fit for all use cases, we should
probably use a customized `UserList` implementation. Now we can call
``widgets.update()`` more then one time without any side effect.
- `ButtonActions` update where appending keys and values within each update
call. Now we can call ``actions.update()`` more then one time without any
side effect.
- The `CollectionSequenceDataConverter` no longer throws a ``TypeError:
'NoneType' object is not iterable`` when passed the value of a non-required
field (which in the case of a `List` field is `None`).
- The `SequenceDataConverter` and `CollectionSequenceDataConverter` converter
classes now ignore values that are not present in the terms when converting
to a widget value.
- Use ``nocall:`` modifier in `orderedselect_input.pt` to avoid calling list
entry if it is callable.
- `SingleCheckBoxFieldWidget` doesn't repeat the label twice (once in ``<div
class="label">``, and once in the ``<label>`` next to the checkbox).
- Don't cause warnings in Python 2.6.
- `validator.SimpleFieldValidator` is now able to handle
`interfaces.NOT_CHANGED`. This value is set for file uploads when the user
does not choose a file for upload.
Version 1.9.0 (2008-08-26)
--------------------------
- Feature: Use the ``query()`` method in the widget manager to try extract a
value. This ensures that the lookup is never failing, which is particularly
helpful for dictionary-based data managers, where dictionaries might not
have all keys.
- Feature: Changed the ``get()`` method of the data manager to throw an error
when the data for the field cannot be found. Added ``query()`` method to
data manager that returns a default value, if no value can be found.
- Feature: Deletion of widgets from field widget managers is now possible.
- Feature: Groups now produce detailed `ObjectModifiedEvent` descriptions like
regular edit forms do. (Thanks to Carsten Senger for providing a patch.)
- Feature: The widget manager's ``extract()`` method now supports an optional
``setErrors`` (default value: True) flag that allows one to not set errors
on the widgets and widget manager during data extraction. Use case: You want
to inspect the entered data and handle errors manually.
- Bug: The ``ignoreButtons`` flag of the ``z3c.form.form.extends()`` method
was not honored. (Thanks to Carsten Senger for providing a patch.)
- Bug: Group classes now implement ``IGroup``. This also helps with the
detection of group instantiation. (Thanks to Carsten Senger for providing a
patch.)
- Bug: The list of changes in a group were updated incorrectly, since it was
assumed that groups would modify mutually exclusive interfaces. Instead of
using an overwriting dictionary ``update()`` method, a purely additive merge
is used now. (Thanks to Carsten Senger for providing a patch.)
- Bug: Added a widget for ``IDecimal`` field in testing setup.
- Feature: The ``z3c.form.util`` module has a new function, ``createCSSId()``
method that generates readable ids for use with css selectors from any
unicode string.
- Bug: The ``applyChanges()`` method in group forms did not return a changes
dictionary, but simply a boolean. This is now fixed and the group form
changes are now merged with the main form changes.
- Bug: Display widgets did not set the style attribute if it was
available, even though the input widgets did set the style attribute.
Version 1.8.2 (2008-04-24)
--------------------------
- Bug: Display Widgets added spaces (due to code indentation) to the displayed
values, which in some cases, like when displaying Python source code, caused
the appearance to be incorrect.
- Bug: Prevent to call ``__len__`` on ``ITerms`` and use ``is None`` for check
for existence. Because ``__len__`` is not a part of the ITerms API and ``not
widget.terms`` will end in calling ``__len__`` on existing terms.
Version 1.8.1 (2008-04-08)
--------------------------
- Bug: Fixed a bug that prohibited groups from having different contents than
the parent form. Previously, the groups contents were not being properly
updated. Added new documentation on how to use groups to generate
object-based sub-forms. Thanks to Paul Carduner for providing the fix and
documentation.
Version 1.8.0 (2008-01-23)
--------------------------
- Feature: Implemented ``IDisplayForm`` interface.
- Feature: Added integration tests for form interfaces. Added default class
attribute called ``widgets`` in form class with default value ``None``. This
helps to pass the integration tests. Now, the ``widgets`` attribute can also
be used as a indicator for updated forms.
- Feature: Implemented additional ``createAndAdd`` hook in ``AddForm``. This
allows you to implement create and add in a single method. It also supports
graceful abortion of a create and add process if we do not return the new
object. This means it can also be used as a hook for custom error messages
for errors happen during create and add.
- Feature: Add a hidden widget template for the ``ISelectWidget``.
- Feature: Arrows in the ordered select widget replaced by named entities.
- Feature: Added ``CollectionSequenceDataConverter`` to ``setupFormDefaults``.
- Feature: Templates for the CheckBox widget are now registered in
``checkbox.zcml``.
- Feature: If a value cannot be converted from its unicode representation to a
field value using the field's ``IFromUnicode`` interface, the resulting type
error now shows the field name, if available.
- Bug: ``createId`` could not handle arbitrary unicode input. Thanks to
Andreas Reuleaux for reporting the bug and a patch for it. (Added
descriptive doctests for the function in the process.)
- Bug: Interface invariants where not working when not all fields needed for
computing the invariant are in the submitted form.
- Bug: Ordered select didn't submit selected values.
- Bug: Ordered select lists displayed tokens instead of value,
- Bug: ``SequenceWidget`` displayed tokens instead of value.
Version 1.7.0 (2007-10-09)
--------------------------
- Feature: Implemented ``ImageButton``, ``ImageAction``, ``ImageWidget``, and
``ImageFieldWidget`` to support imge submit buttons.
- Feature: The ``AttributeField`` data manager now supports adapting
the content to the fields interface when the content doesn't implement
this interface.
- Feature: Implemented single checkbox widget that can be used for boolean
fields. They are not available by default but can be set using the
``widgetFactory`` attribute.
- Bug: More lingual issues have been fixed in the documentation. Thanks to
Martijn Faassen for doing this.
- Bug: When an error occurred during processing of the request the
widget ended up being security proxied and the system started
throwing `TraversalError`-'s trying to access the `label` attribute of
the widget. Declared that the widgets require the `zope.Public`
permission in order to access these attributes.
- Bug: When rendering a widget the ``style`` attribute was not honored. Thanks
to Andreas Reuleaux for reporting.
- Bug: When an error occurred in the sub-form, the status message was not set
correctly. Fixed the code and the incorrect test. Thanks to Markus
Kemmerling for reporting.
- Bug: Several interfaces had the ``self`` argument in the method
signature. Thanks to Markus Kemmerling for reporting.
Version 1.6.0 (2007-08-24)
--------------------------
- Feature: An event handler for ``ActionErrorOccurred`` events is registered
to merge the action error into the form's error collectors, such as
``form.widgets.errors`` and ``form.widgets['name'].error`` (if
applicable). It also sets the status of the form. (Thanks to Herman
Himmelbauer, who requested the feature, for providing use cases.)
- Feature: Action can now raise ``ActionExecutionError`` exceptions that will
be handled by the framework. These errors wrap the original error. If an
error is specific to a widget, then the widget name is passed to a special
``WidgetActionExecutionError`` error. (Thanks to Herman Himmelbauer, who
requested the feature, for providing use cases.)
- Feature: After an action handler has been executed, an action executed event
is sent to the system. If the execution was successful, the event is
``ActionSuccessfull`` event is sent. If an action execution error was
raised, the ``ActionErrorOccurred`` event is raised. (Thanks to Herman
Himmelbauer, who requested the feature, for providing use cases.)
- Feature: The ``applyChanges()`` function now returns a dictionary of changes
(grouped by interface) instead of a boolean. This allows us to generate a
more detailed object-modified event. If no changes are applied, an empty
dictionary is returned. The new behavior is compatible with the old one, so
no changes to your code are required. (Thanks to Darryl Cousins for the
request and implementation.)
- Feature: A new ``InvalidErrorViewSnippet`` class provides an error view
snippet for ``zope.interface.Invalid`` exceptions, which are frequently used
for invariants.
- Feature: When a widget is required, HTML-based widgets now declare a
"required" class.
- Feature: The validation data wrapper now knows about the context of the
validation, which provides a hook for invariants to access the environment.
- Feature: The BoolTerms term tokens are now cosntants and stay the same, even
if the label has changed. The choice for the token is "true" and "false". By
default it used to be "yes" and "no", so you probably have to change some
unit tests. Functional tests are still okay, because you select by term
title.
- Feature: BoolTerms now expose the labels for the true and false values
to the class. This makes it a matter of doing trivial sub-classing to
change the labels for boolean terms.
- Feature: Exposed several attributes of the widget manager to the form for
convenience. The attributes are: mode, ignoreContext, ignoreRequest,
ignoreReadonly.
- Feature: Provide more user-friendly error messages for number formatting.
- Refactoring: The widget specific class name was in camel-case. A converntion
that later developed uses always dash-based naming of HTML/CSS related
variables. So for example, the class name "textWidget" is now
"text-widget". This change will most likely require some changes to your CSS
declarations!
- Documentation: The text of ``field.txt`` has been reviewed linguistically.
- Documentation: While reviewing the ``form.txt`` with some people, several
unclear and incomplete statements were discovered and fixed.
- Bug (IE): In Internet Explorer, when a label for a radio input field is only
placed around the text describing the choice, then only the text is
surrounded by a dashed box. IE users reported this to be confusing, thus we
now place the label around the text and the input element so that both are
surrounded by the dashed border. In Firefox and KHTML (Safari) only the
radio button is surrounded all the time.
- Bug: When extracting and validating data in the widget manager, invariant
errors were not converted to error view snippets.
- Bug: When error view snippets were not widget-specific -- in other words,
the ``widget`` attribute was ``None`` -- rendering the template would fail.
Version 1.5.0 (2007-07-18)
--------------------------
- Feature: Added a span around values for widgets in display mode. This allows
for easier identification widget values in display mode.
- Feature: Added the concept of widget events and implemented a particular
"after widget update" event that is called right after a widget is updated.
- Feature: Restructured the approach to customize button actions, by requiring
the adapter to provide a new interface ``IButtonAction``. Also, an adapter
is now provided by default, still allowing cusotmization using the usual
methods though.
- Feature: Added button widget. While it is not very useful without
Javascript, it still belongs into this package for completion.
- Feature: All ``IFieldWidget`` instances that are also HTML element widgets
now declare an additional CSS class of the form "<fieldtype.lower()>-field".
- Feature: Added ``addClass()`` method to HTML element widgets, so that adding
a new CSS class is simpler.
- Feature: Renamed "css" attribute of the widget to "klass", because the class
of an HTML element is a classification, not a CSS marker.
- Feature: Reviewed all widget attributes. Added all available HTML attributes
to the widgets.
- Documentation: Removed mentioning of widget's "hint" attribute, since it
does not exist.
- Optimization: The terms for a sequence widget were looked up multiple times
among different components. The widget is now the canonical source for the
terms and other components, such as the converter uses them. This avoids
looking up the terms multiple times, which can be an expensive process for
some applications.
- Bug/Feature: Correctly create labels for radio button choices.
- Bug: Buttons did not honor the name given by the schema, if created within
one, because we were too anxious to give buttons a name. Now name assignment
is delayed until the button is added to the button manager.
- Bug: Button actions were never updated in the actions manager.
- Bug: Added tests for textarea widget.
Version 1.4.0 (2007-06-29)
--------------------------
- Feature: The select widget grew a new ``prompt`` flag, which allows you to
explicitely request a selection prompt as the first option in the selection
(even for required fields). When set, the prompt message is shown. Such a
prompt as option is common in Web-UIs.
- Feature: Allow "no value message" of select widgets to be dynamically
changed using an attribute value adapter.
- Feature: Internationalized data conversion for date, time, date/time,
integer, float and decimal. Now the locale data is used to format and parse
those data types to provide the bridge to text-based widgets. While those
features require the latest zope.i18n package, backward compatibility is
provided.
- Feature: All forms now have an optional label that can be used by the UI.
- Feature: Implemented groups within forms. Groups allow you to combine a set
of fields/widgets into a logical unit. They were designed with ease of use
in mind.
- Feature: Button Actions -- in other words, the widget for the button field
-- can now be specified either as the "actionFactory" on the button field or
as an adapter.
- Bug: Recorded all public select-widget attributes in the interface.
Version 1.3.0 (2007-06-22)
--------------------------
- Feature: In an edit form applying the data and generating all necessary
messages was all done within the "Apply" button handler. Now the actual task
of storing is factored out into a new method called "applyChanges(data)",
which returns whether the data has been changed. This is useful for forms
not dealing with objects.
- Feature: Added support for ``hidden`` fields. You can now use the ``hidden``
mode for widgets which should get rendered as ``<input type="hidden"
/>``.
Note: Make sure you use the new formui templates which will avoid rendering
labels for hidden widgets or adjust your custom form macros.
- Feature: Added ``missing_value`` support to data/time converters
- Feature: Added named vocabulary lookup in ``ChoiceTerms`` and
``CollectionTerms``.
- Feature: Implemented support for ``FileUpload`` in ``FileWidget``.
* Added helper for handling ``FileUpload`` widgets:
+ ``extractContentType(form, id)``
Extracts the content type if ``IBytes``/``IFileWidget`` was used.
+ ``extractFileName(form, id, cleanup=True, allowEmtpyPostFix=False)``
Extracts a filename if ``IBytes``/``IFileWidget`` was used.
Uploads from win/IE need some cleanup because the filename includes also
the path. The option ``cleanup=True`` will do this for you. The option
``allowEmtpyPostFix`` allows you to pass a filename without
extensions. By default this option is set to ``False`` and will raise a
``ValueError`` if a filename doesn't contain an extension.
* Created afile upload data converter registered for
``IBytes``/``IFileWidget`` ensuring that the converter will only be used
for fiel widgets. The file widget is now the default for the bytes
field. If you need to use a text area widget for ``IBytes``, you have to
register a custom widget in the form using::
fields['foobar'].widgetFactory = TextWidget
- Feature: Originally, when an attribute access failed in Unauthorized or
ForbiddenAttribute exceptions, they were ignored as if the attribute would
have no value. Now those errors are propagated and the system will fail
providing the developer with more feedback. The datamanager also grew a new
``query()`` method that returns always a default and the ``get()`` method
propagates any exceptions.
- Feature: When writing to a field is forbidden due to insufficient
priviledges, the resulting widget mode will be set to "display". This
behavior can be overridden by explicitely specifying the mode on a field.
- Feature: Added an add form implementation against ``IAdding``. While this is
not an encouraged method of adding components, many people still use this
API to extend the ZMI.
- Feature: The ``IFields`` class' ``select()`` and ``omit()`` method now
support two ketword arguments "prefix" and "interface" that allow the
selection and omission of prefixed fields and still specify the short
name. Thanks to Nikolay Kim for the idea.
- Feature: HTML element ids containing dots are not very good, because then
the "element#id" CSS selector does not work and at least in Firefox the
attribute selector ("element[attr=value]") does not work for the id
either. Converted the codebase to use dashes in ids instead.
- Bug/Feature: The ``IWidgets`` component is now an adapter of the form
content and not the form context. This guarantees that vocabulary factories
receive a context that is actually useful.
- Bug: The readonly flag within a field was never honored. When a field is
readonly, it is displayed in "display" mode now. This can be overridden by
the widget manager's "ignoreReadonly" flag, which is necessary for add
forms.
- Bug: The mode selection made during the field layout creation was not
honored and the widget manager always overrode the options providing its
value. Now the mode specified in the field is more important than the one
from the widget manager.
- Bug: It sometimes happens that the sequence widget has the no-value token as
one element. This caused ``displayValue()`` to fail, since it tried to find
a term for it. For now we simply ignore the no-value token.
- Bug: Fixed the converter when the incoming value is an empty string. An
empty string really means that we have no value and it is thus missing,
returning the missing value.
- Bug: Fix a slightly incorrect implementation. It did not cause any harm in
real-world forms, but made unit testing much harder, since an API
expectation was not met correctly.
- Bug: When required selections where not selected in radio and checkbox
widgets, then the conversion did not behave correctly. This also revealed
some issues with the converter code that have been fixed now.
- Bug: When fields only had a vocabulary name, the choice terms adaptation
would fail, since the field was not bound. This has now been corrected.
- Documentation: Integrated English language and content review improvements
by Roy Mathew in ``form.txt``.
Version 1.2.0 (2007-05-30)
--------------------------
- Feature: Added ability to change the button action title using an ``IValue``
adapter.
Version 1.1.0 (2007-05-30)
--------------------------
- Feature: Added compatibility for Zope 3.3 and thus Zope 2.10.
Version 1.0.0 (2007-05-24)
--------------------------
- Initial Release
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.
- ``testing.txt`` [informative]
The ``testing`` module provides helper functions that make it easier to tet
form-based code in unit tests. It also provides components that simplify
testing in testbrowser and Selenium.
- ``object-caveat.txt`` [informative]
Explains the current problems of ObjectWidget.
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 are 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 z3c.form import ptcompat as viewpagetemplatefile
>>> from z3c.form import tests
>>> def addTemplate(form):
... form.template = viewpagetemplatefile.bind_template(
... viewpagetemplatefile.ViewPageTemplateFile(
... 'simple_edit.pt', os.path.dirname(tests.__file__)), form)
>>> addTemplate(addForm)
Let's now render the page:
>>> print addForm.render()
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<div class="row">
<label for="form-widgets-id">ID</label>
<input type="text" id="form-widgets-id"
name="form.widgets.id"
class="text-widget required textline-field"
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 xmlns="http://www.w3.org/1999/xhtml">
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
</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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
</html>
The form also provides a label for rendering a required info. This required
info depends by default on the given requiredInfo label and if at least one
field is required:
>>> addForm.requiredInfo
u'<span class="required">*</span>&ndash; required'
If we set the labelRequired to None, we do not get a requiredInfo label:
>>> addForm.labelRequired = None
>>> addForm.requiredInfo is None
True
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 testing.render(addForm, './/xmlns:div[2][@class="row"]')
<div class="row">
<label for="form-widgets-name">Full Name</label>
<input class="text-widget required textline-field"
id="form-widgets-name" name="form.widgets.name" type="text" 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 testing.render(addForm, './/xmlns:div[@class="action"]')
<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 xmlns="http://www.w3.org/1999/xhtml">
<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 are 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 xmlns="http://www.w3.org/1999/xhtml">
<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 testing.render(editForm, './/xmlns:i')
<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...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 testing.render(editForm, './/xmlns:i')
<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 testing.render(editForm, './/xmlns:i')
<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 xmlns="http://www.w3.org/1999/xhtml">
<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 are 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 xmlns="http://www.w3.org/1999/xhtml">
<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 testing.render(myEdit, './/xmlns:input[@id="form-widgets-name"]')
<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 testing.render(hiddenEdit, './/xmlns:input[@id="form-widgets-age"]')
<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)
Applying Changes
----------------
When applying the data of a form to a content component, the function
``applyChanges()`` is called. It simply iterates through the fields of the
form and uses the data managers to store the values. The output of the
function is a list of changes:
>>> roger = Person(u'roger', u'Roger')
>>> roger
<Person u'Roger'>
>>> class BaseForm(form.Form):
... fields = field.Fields(IPerson).select('name')
>>> myForm = BaseForm(roger, TestRequest())
>>> form.applyChanges(myForm, roger, {'name': u'Roger Ineichen'})
{<InterfaceClass __builtin__.IPerson>: ['name']}
>>> roger
<Person u'Roger Ineichen'>
When a field is missing from the data, it is simply skipped:
>>> form.applyChanges(myForm, roger, {})
{}
If the new and old value are identical, storing the data is skipped as well:
>>> form.applyChanges(myForm, roger, {'name': u'Roger Ineichen'})
{}
In some cases the data converter for a field-widget pair returns the
``NOT_CHANGED`` value. In this case, the field is skipped as well:
>>> form.applyChanges(myForm, roger, {'name': interfaces.NOT_CHANGED})
{}
>>> roger
<Person u'Roger Ineichen'>
Refreshing actions
------------------
Sometimes, it's useful to update actions again after executing them,
because some conditions could have changed. For example, imagine
we have a sequence edit form that has a delete button. We don't
want to show delete button when the sequence is empty. The button
condition would handle this, but what if the sequence becomes empty
as a result of execution of the delete action that was available?
In that case we want to refresh our actions to new conditions to make
our delete button not visible anymore. The ``refreshActions`` form
variable is intended to handle this case.
Let's create a simple form with an action that clears our context
sequence.
>>> class SequenceForm(form.Form):
...
... @button.buttonAndHandler(u'Empty', condition=lambda form:bool(form.context))
... def handleEmpty(self, action):
... self.context[:] = []
... self.refreshActions = True
First, let's illustrate simple cases, when no button is pressed.
The button will be available when context is not empty.
>>> context = [1, 2, 3, 4]
>>> request = TestRequest()
>>> myForm = SequenceForm(context, request)
>>> myForm.update()
>>> addTemplate(myForm)
>>> print testing.render(myForm, './/xmlns:div[@class="action"]')
<div class="action">
<input type="submit" id="form-buttons-empty" name="form.buttons.empty"
class="submit-widget button-field" value="Empty" />
</div>
The button will not be available when the context is empty.
>>> context = []
>>> request = TestRequest()
>>> myForm = SequenceForm(context, request)
>>> myForm.update()
>>> addTemplate(myForm)
>>> print testing.render(myForm, './/xmlns:form')
<form action=".">
</form>
Now, the most interesting case when context is not empty, but becomes
empty as a result of pressing the "empty" button. We set the
``refreshActions`` flag in the action handler, so our actions should
be updated to new conditions.
>>> context = [1, 2, 3, 4, 5]
>>> request = TestRequest(form={
... 'form.buttons.empty': u'Empty'}
... )
>>> myForm = SequenceForm(context, request)
>>> myForm.update()
>>> addTemplate(myForm)
>>> print testing.render(myForm, './/xmlns:form')
<form action=".">
</form>
Integration tests
-----------------
Identifying the different forms can be important if it comes to layout
template lookup. Let's 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.TextLine(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 z3c.form.ptcompat 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(
... '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()
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<div class="row">
<label for="form-widgets-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>
<fieldset>
<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>
</fieldset>
<fieldset>
<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 textline-field"
value="" />
</div>
</fieldset>
<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 testing.render(add, './/xmlns:i')
<i>There were some errors.</i>
>>> print testing.render(add, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
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 testing.render(add, './/xmlns:i')
<i>There were some errors.</i>
>>> print testing.render(add, './/xmlns:ul[1]')
<ul>
<li>
Last Name:
<div class="error">Required input is missing.</div>
</li>
</ul>
<ul>
<li>
Address:
<div class="error">Required input is missing.</div>
</li>
</ul>
>>> print testing.render(add, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
Let's now successfully complete the add form.
>>> from zope.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
u'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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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>
<fieldset>
<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>
</fieldset>
<fieldset>
<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 textline-field"
value="2005" />
</div>
</fieldset>
<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 testing.render(edit, './/xmlns:i')
<i>There were some errors.</i>
>>> print testing.render(edit, './/xmlns:ul')
<ul>
<li>
Address:
<div class="error">Required input is missing.</div>
</li>
</ul>
>>> print testing.render(edit, './/xmlns:fieldset/xmlns:ul')
<ul>
<li>
Address: <div class="error">Required input is missing.</div>
</li>
</ul>
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 testing.render(edit, './/xmlns:i')
<i>Data successfully updated.</i>
and the data are 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
u'2006'
Let's look at the event:
>>> event = eventlog[-1]
>>> event
<zope...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')
Group form as instance
----------------------
It is also possible to use group instances in forms. Let's setup our previous
form and assing a group instance:
>>> class RegistrationEditForm(group.GroupForm, form.EditForm):
... fields = field.Fields(IVehicleRegistration).select(
... 'firstName', 'lastName')
...
... template = ViewPageTemplateFile(
... 'simple_groupedit.pt', os.path.dirname(tests.__file__))
>>> request = testing.TestRequest()
>>> edit = RegistrationEditForm(reg, request)
Instanciate the form and use a group class and a group instance:
>>> carGroupInstance = CarGroup(edit.context, request, edit)
>>> edit.groups = (LicenseGroup, carGroupInstance)
>>> edit.update()
>>> print edit.render()
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<div class="row">
<label for="form-widgets-firstName">First Name</label>
<input id="form-widgets-firstName"
name="form.widgets.firstName"
class="text-widget required textline-field"
value="Stephan" type="text" />
</div>
<div class="row">
<label for="form-widgets-lastName">Last Name</label>
<input id="form-widgets-lastName"
name="form.widgets.lastName"
class="text-widget required textline-field"
value="Richter" type="text" />
</div>
<fieldset>
<legend>License</legend>
<div class="row">
<label for="form-widgets-license">License</label>
<input id="form-widgets-license"
name="form.widgets.license"
class="text-widget required textline-field"
value="MA 4038765" type="text" />
</div>
<div class="row">
<label for="form-widgets-address">Address</label>
<input id="form-widgets-address"
name="form.widgets.address"
class="text-widget required textline-field"
value="11 Main St, Maynard, MA" type="text" />
</div>
</fieldset>
<fieldset>
<legend>Car</legend>
<div class="row">
<label for="form-widgets-model">Model</label>
<input id="form-widgets-model" name="form.widgets.model"
class="text-widget required textline-field"
value="Ford" type="text" />
</div>
<div class="row">
<label for="form-widgets-make">Make</label>
<input id="form-widgets-make" name="form.widgets.make"
class="text-widget required textline-field"
value="F150" type="text" />
</div>
<div class="row">
<label for="form-widgets-year">Year</label>
<input id="form-widgets-year" name="form.widgets.year"
class="text-widget required textline-field"
value="2006" type="text" />
</div>
</fieldset>
<div class="action">
<input id="form-buttons-apply" name="form.buttons.apply"
class="submit-widget button-field" value="Apply"
type="submit" />
</div>
</form>
</body>
</html>
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.TextLine(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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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 textline-field" value="2005" />
</div>
<fieldset>
<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>
</fieldset>
<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 testing.render(edit, './/xmlns:i')
<i>Data successfully updated.</i>
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
u'2005'
Nested Groups
-------------
The group can contains groups. Let's adapt the previous RegistrationEditForm:
>>> class OwnerGroup(group.Group):
... label = u'Owner'
... fields = field.Fields(IVehicleOwner, prefix='owner')
...
... def getContent(self):
... return self.context.owner
>>> class VehicleRegistrationGroup(group.Group):
... label = u'Registration'
... fields = field.Fields(IVehicleRegistration).omit(
... 'owner')
... groups = (OwnerGroup,)
...
... template = ViewPageTemplateFile(
... 'simple_groupedit.pt', os.path.dirname(tests.__file__))
>>> class RegistrationEditForm(group.GroupForm, form.EditForm):
... groups = (VehicleRegistrationGroup,)
...
... template = ViewPageTemplateFile(
... 'simple_nested_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()
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 testing.render(edit, './/xmlns:i')
<i>Data successfully updated.</i>
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
u'2005'
So what happens, if errors happen inside a nested group? Let's use an empty
invllaid object for the test missing input errors:
>>> reg = VehicleRegistration(owner=VehicleOwner())
>>> request = testing.TestRequest(form={
... 'form.widgets.owner.firstName': u'',
... 'form.widgets.owner.lastName': u'',
... 'form.widgets.license': u'',
... 'form.widgets.address': u'',
... 'form.widgets.model': u'',
... 'form.widgets.make': u'',
... 'form.widgets.year': u'',
... 'form.buttons.apply': u'Apply'
... })
>>> edit = RegistrationEditForm(reg, request)
>>> edit.update()
>>> data, errors = edit.extractData()
>>> print testing.render(edit, './/xmlns:i')
<i >There were some errors.</i>
>>> print testing.render(edit, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
License:
<div class="error">Required input is missing.</div>
</li>
<li>
Address:
<div class="error">Required input is missing.</div>
</li>
<li>
Model:
<div class="error">Required input is missing.</div>
</li>
<li>
Make:
<div class="error">Required input is missing.</div>
</li>
<li>
Year:
<div class="error">Required input is missing.</div>
</li>
</ul>
<ul>
<li>
First Name:
<div class="error">Required input is missing.</div>
</li>
<li>
Last Name:
<div class="error">Required input is missing.</div>
</li>
</ul>
Group instance in nested group
------------------------------
Let's also test if the Group class can handle group objects as instances:
>>> 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)
>>> vrg = VehicleRegistrationGroup(edit.context, request, edit)
>>> ownerGroup = OwnerGroup(edit.context, request, edit)
Now build the group instance object chain:
>>> vrg.groups = (ownerGroup,)
>>> edit.groups = (vrg,)
Also use refreshActions whihc is not needed but will make coverage this
additional line of code in the update method:
>>> edit.refreshActions = True
Update and render:
>>> edit.update()
>>> print edit.render()
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<form action=".">
<fieldset>
<legend>Registration</legend>
<div class="row">
<label for="form-widgets-license">License</label>
<input id="form-widgets-license"
name="form.widgets.license"
class="text-widget required textline-field"
value="MA 40387" type="text" />
</div>
<div class="row">
<label for="form-widgets-address">Address</label>
<input id="form-widgets-address"
name="form.widgets.address"
class="text-widget required textline-field"
value="10 Main St, Maynard, MA" type="text" />
</div>
<div class="row">
<label for="form-widgets-model">Model</label>
<input id="form-widgets-model" name="form.widgets.model"
class="text-widget required textline-field"
value="BMW" type="text" />
</div>
<div class="row">
<label for="form-widgets-make">Make</label>
<input id="form-widgets-make" name="form.widgets.make"
class="text-widget required textline-field"
value="325" type="text" />
</div>
<div class="row">
<label for="form-widgets-year">Year</label>
<input id="form-widgets-year" name="form.widgets.year"
class="text-widget required textline-field"
value="2005" type="text" />
</div>
<fieldset>
<legend>Owner</legend>
<div class="row">
<label for="form-widgets-owner-firstName">First Name</label>
<input id="form-widgets-owner-firstName"
name="form.widgets.owner.firstName"
class="text-widget required textline-field"
value="Stephan" type="text" />
</div>
<div class="row">
<label for="form-widgets-owner-lastName">Last Name</label>
<input id="form-widgets-owner-lastName"
name="form.widgets.owner.lastName"
class="text-widget required textline-field"
value="Richter" type="text" />
</div>
</fieldset>
</fieldset>
<div class="action">
<input id="form-buttons-apply" name="form.buttons.apply"
class="submit-widget button-field" value="Apply"
type="submit" />
</div>
</form>
</body>
</html>
Now test the error handling if just one missing value is given in a group:
>>> request = testing.TestRequest(form={
... 'form.widgets.owner.firstName': u'Paul',
... 'form.widgets.owner.lastName': u'',
... '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)
>>> vrg = VehicleRegistrationGroup(edit.context, request, edit)
>>> ownerGroup = OwnerGroup(edit.context, request, edit)
>>> vrg.groups = (ownerGroup,)
>>> edit.groups = (vrg,)
>>> edit.update()
>>> data, errors = edit.extractData()
>>> print testing.render(edit, './/xmlns:i')
<i >There were some errors.</i>
>>> print testing.render(edit, './/xmlns:fieldset[1]/xmlns:ul')
<ul>
<li>
Last Name:
<div class="error">Required input is missing.</div>
</li>
</ul>
Just check whether we fully support the interface:
>>> from z3c.form import interfaces
>>> from zope.interface.verify import verifyClass
>>> verifyClass(interfaces.IGroup, group.Group)
True
=========
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 z3c.form.ptcompat import ViewPageTemplateFile
>>> from z3c.form import form, field, tests
>>> templatePath = os.path.dirname(tests.__file__)
>>> class OwnerForm(form.EditForm):
... template = 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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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 xmlns="http://www.w3.org/1999/xhtml">
<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(
... '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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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 xmlns="http://www.w3.org/1999/xhtml">
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
<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() # doctest: +NOPARSE_MARKUP
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
...
<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(
... '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(
... '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 xmlns="http://www.w3.org/1999/xhtml">
<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'
* ``ignoreContext``
While the ``ignoreContext`` flag is usually set on the form, it is sometimes
desirable to set the flag for a particular field.
>>> manager = field.Fields(IPerson)
>>> manager['country'].ignoreContext
>>> manager = field.Fields(IPerson, ignoreContext=True)
>>> manager['country'].ignoreContext
True
>>> manager = field.Fields(IPerson, ignoreContext=False)
>>> manager['country'].ignoreContext
False
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']
As you can see, if we call update twice, we still get the same amount and
order of keys:
>>> manager.update()
>>> 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'
Required fields
---------------
There is a flag for required fields. This flag get set if at least one field
is required. This let us render a required info legend in forms if required
fields get used.
>>> manager.hasRequiredFields
True
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)
>>> manager.ignoreContext = True
>>> manager.update()
>>> from zope.testing.doctestunit import pprint
>>> pprint(manager.extract())
({'firstName': u'Stephan', 'lastName': u'Richter'}, ())
Since all errors are immediately converted to error view snippets, we have to
provide the adapter from a validation error to an error view snippet first:
>>> from z3c.form import error
>>> zope.component.provideAdapter(error.ErrorViewSnippet)
Let's now cause a widget-level error by not submitting the required last
name:
>>> request = TestRequest(form={
... 'form.widgets.firstName': u'Stephan', 'form.widgets.id': u'srichter'})
>>> manager = field.FieldWidgets(personForm, request, context)
>>> manager.ignoreContext = True
>>> manager.update()
>>> manager.extract()
({'firstName': u'Stephan'}, (<ErrorViewSnippet for RequiredMissing>,))
Finally, let's ensure that invariant failures are also caught:
>>> request = TestRequest(form={
... 'form.widgets.id': u'srichter',
... 'form.widgets.firstName': u'Stephan',
... 'form.widgets.lastName': u'Richter-Richter'})
>>> manager = field.FieldWidgets(personForm, request, context)
>>> manager.ignoreContext = True
>>> manager.update()
>>> data, errors = manager.extract()
>>> errors[0].error.doc()
'The last name is too short.'
Note that the errors coming from invariants are all error view snippets as
well, just as it is the case for field-specific validation errors. And that's
really all there is!
By default, the ``extract()`` method not only returns the errors that it
catches, but also sets them on individual widgets and on the manager:
>>> manager.errors
(<ErrorViewSnippet for LastNameTooShort>,)
This behavior can be turned off. To demonstrate, let's make a new request that
causes a widget-level error:
>>> request = TestRequest(form={
... 'form.widgets.firstName': u'Stephan', 'form.widgets.id': u'srichter'})
>>> manager = field.FieldWidgets(personForm, request, context)
>>> manager.ignoreContext = True
>>> manager.update()
We have to set the setErrors property to False before calling extract,
we still get the same result from the method call, ...
>>> manager.setErrors = False
>>> manager.extract()
({'firstName': u'Stephan'}, (<ErrorViewSnippet for RequiredMissing>,))
but there are no side effects on the manager and the widgets:
>>> manager.errors
()
>>> manager['lastName'].error is None
True
Customization of Ignoring the Context
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Note that you can also manually control ignoring the context per field.
>>> class CustomPersonForm(object):
... prefix = 'form.'
... fields = field.Fields(IPerson).select('id')
... fields += field.Fields(IPerson, ignoreContext=True).select(
... 'firstName', 'lastName')
>>> customPersonForm = CustomPersonForm()
Let's now create a manager and update it:
>>> customManager = field.FieldWidgets(customPersonForm, request, context)
>>> customManager.update()
>>> customManager['id'].ignoreContext
False
>>> customManager['firstName'].ignoreContext
True
>>> customManager['lastName'].ignoreContext
True
Fields -- Custom Widget Factories
---------------------------------
It is possible to declare custom widgets for fields within the field's
declaration.
Let's have a look at the default form first. Initially, the standard
registered widgets are used:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.update()
>>> manager['firstName']
<Widget 'form.widgets.firstName'>
Now we would like to have our own custom input widget:
>>> class CustomInputWidget(widget.Widget):
... pass
>>> def CustomInputWidgetFactory(field, request):
... return widget.FieldWidget(field, CustomInputWidget(request))
It can be simply assigned as follows:
>>> personForm.fields['firstName'].widgetFactory = CustomInputWidgetFactory
>>> personForm.fields['lastName'].widgetFactory = CustomInputWidgetFactory
Now this widget should be used instead of the registered default one:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.update()
>>> manager['firstName']
<CustomInputWidget 'form.widgets.firstName'>
In the background the widget factory assignment really just registered the
default factory in the ``WidgetFactories`` object, which manages the
custom widgets for all modes. Now all modes show this input widget:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.mode = interfaces.DISPLAY_MODE
>>> manager.update()
>>> manager['firstName']
<CustomInputWidget 'form.widgets.firstName'>
However, we can also register a specific widget for the display mode:
>>> class CustomDisplayWidget(widget.Widget):
... pass
>>> def CustomDisplayWidgetFactory(field, request):
... return widget.FieldWidget(field, CustomDisplayWidget(request))
>>> personForm.fields['firstName']\
... .widgetFactory[interfaces.DISPLAY_MODE] = CustomDisplayWidgetFactory
>>> personForm.fields['lastName']\
... .widgetFactory[interfaces.DISPLAY_MODE] = CustomDisplayWidgetFactory
Now the display mode should produce the custom display widget, ...
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.mode = interfaces.DISPLAY_MODE
>>> manager.update()
>>> manager['firstName']
<CustomDisplayWidget 'form.widgets.firstName'>
>>> manager['lastName']
<CustomDisplayWidget 'form.widgets.lastName'>
... while the input mode still shows the default custom input widget
on the ``lastName`` field but not on the ``firstName`` field since we
don't have the ``test.Edit`` permission:
>>> manager = field.FieldWidgets(personForm, request, srichter)
>>> manager.mode = interfaces.INPUT_MODE
>>> manager.update()
>>> manager['firstName']
<CustomDisplayWidget 'form.widgets.firstName'>
>>> manager['lastName']
<CustomInputWidget 'form.widgets.lastName'>
The widgets factories component,
>>> factories = personForm.fields['firstName'].widgetFactory
>>> factories
{'display': <function CustomDisplayWidgetFactory at ...>}
is pretty much a standard dictionary that also manages a default value:
>>> factories.default
<function CustomInputWidgetFactory at ...>
When getting a value for a key, if the key is not found, the default is
returned:
>>> factories.keys()
['display']
>>> factories[interfaces.DISPLAY_MODE]
<function CustomDisplayWidgetFactory at ...>
>>> factories[interfaces.INPUT_MODE]
<function CustomInputWidgetFactory at ...>
>>> factories.get(interfaces.DISPLAY_MODE)
<function CustomDisplayWidgetFactory at ...>
>>> factories.get(interfaces.INPUT_MODE)
<function CustomInputWidgetFactory at ...>
If no default is specified,
>>> factories.default = None
then the dictionary behaves as usual:
>>> factories[interfaces.DISPLAY_MODE]
<function CustomDisplayWidgetFactory at ...>
>>> factories[interfaces.INPUT_MODE]
Traceback (most recent call last):
...
KeyError: 'input'
>>> factories.get(interfaces.DISPLAY_MODE)
<function CustomDisplayWidgetFactory at ...>
>>> factories.get(interfaces.INPUT_MODE)
=======
Buttons
=======
Buttons are a method to declare actions for a form. Like fields describe
widgets within a form, buttons describe actions. The symmetry goes even
further; like fields, buttons are schema fields within schema. When the form
is instantiated and updated, the buttons are converted to actions.
>>> from z3c.form import button
Schema Defined Buttons
----------------------
Let's now create a schema that describes the buttons of a form. Having button
schemas allows one to more easily reuse button declarations and to group them
logically. ``Button`` objects are just a simple extension to ``Field``
objects, so they behave identical within a schema:
>>> import zope.interface
>>> class IButtons(zope.interface.Interface):
... apply = button.Button(title=u'Apply')
... cancel = button.Button(title=u'Cancel')
In reality, only the title and name is relevant. Let's now create a form that
provides those buttons.
>>> from z3c.form import interfaces
>>> class Form(object):
... zope.interface.implements(
... interfaces.IButtonForm, interfaces.IHandlerForm)
... buttons = button.Buttons(IButtons)
... prefix = 'form'
...
... @button.handler(IButtons['apply'])
... def apply(self, action):
... print 'successfully applied'
...
... @button.handler(IButtons['cancel'])
... def cancel(self, action):
... self.request.response.redirect('index.html')
Let's now create an action manager for the button manager in the form. To do
that we first need a request and a form instance:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> form = Form()
We also have to register a button action factory for the buttons:
>>> zope.component.provideAdapter(
... button.ButtonAction, provides=interfaces.IButtonAction)
Action managers are instantiated using the form, request, and
context/content. A special button-action-manager implementation is available
in the ``button`` package:
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
Once the action manager is updated, the buttons should be available as
actions:
>>> actions.keys()
['apply', 'cancel']
>>> actions['apply']
<ButtonAction 'form.buttons.apply' u'Apply'>
It is possible to customize how a button is transformed into an action
by registering an adapter for the request and the button that provides
``IButtonAction``.
>>> import zope.component
>>> from zope.publisher.interfaces.browser import IBrowserRequest
>>> class CustomButtonAction(button.ButtonAction):
... """Custom Button Action Class."""
>>> zope.component.provideAdapter(
... CustomButtonAction, provides=interfaces.IButtonAction)
Now if we rerun update we will get this other ButtonAction
implementation. Note, there are two strategies what now could happen. We can
remove the existing action and get the new adapter based action or we can
reuse the existing action. Since the ButtonActions class offers an API for
remove existing actions, we reuse the existing action because it very uncommon
to replace existing action during an for update call with an adapter. If
someone really will add an action adapter during process time via directly
provided interface, he is also responsible for remove existing actions.
As you can see we still will get the old button action if we only call update:
>>> actions.update()
>>> actions.keys()
['apply', 'cancel']
>>> actions['apply']
<ButtonAction 'form.buttons.apply' u'Apply'>
This means we have to remove the previous action before we call update:
>>> del actions['apply']
>>> actions.update()
Make sure we do not append a button twice to the key and value lists by calling
update twice:
>>> actions.keys()
['apply', 'cancel']
>>> actions['apply']
<CustomButtonAction 'form.buttons.apply' u'Apply'>
Alternatively, customize an individual button by setting its
actionFactory attribute.
>>> def customButtonActionFactory(request, field):
... print "This button factory creates a button only once."
... button = CustomButtonAction(request, field)
... button.css = "happy"
... return button
>>> form.buttons['apply'].actionFactory = customButtonActionFactory
Again, remove the old button action befor we call update:
>>> del actions['apply']
>>> actions.update()
This button factory creates a button only once.
>>> actions.update()
>>> actions['apply'].css
'happy'
Since we only create a button once from an adapter or a factory, we can change
the button attributes without to lose changes:
>>> actions['apply'].css = 'very happy'
>>> actions['apply'].css
'very happy'
>>> actions.update()
>>> actions['apply'].css
'very happy'
But let's not digress too much and get rid of this customization
>>> form.buttons['apply'].actionFactory = None
>>> actions.update()
Button actions are locations:
>>> apply = actions['apply']
>>> apply.__name__
'apply'
>>> apply.__parent__
<ButtonActions None>
A button action is also a submit widget. The attributes translate as follows:
>>> interfaces.ISubmitWidget.providedBy(apply)
True
>>> apply.value == apply.title
True
>>> apply.id == apply.name.replace('.', '-')
True
Next we want to display our button actions. To be able to do this, we have to
register a template for the submit widget:
>>> from z3c.form import testing, widget
>>> templatePath = testing.getPath('submit_input.pt')
>>> factory = widget.WidgetTemplateFactory(templatePath, 'text/html')
>>> from zope.pagetemplate.interfaces import IPageTemplate
>>> zope.component.provideAdapter(factory,
... (zope.interface.Interface, TestRequest, None, None,
... interfaces.ISubmitWidget),
... IPageTemplate, name='input')
A widget template has many discriminators: context, request, view, field, and
widget. We can now render each action:
>>> print actions['apply'].render()
<input type="submit" id="form-buttons-apply"
name="form.buttons.apply" class="submit-widget button-field"
value="Apply" />
So displaying is nice, but how do button handlers get executed? The action
manager provides attributes and method to check whether actions were
executed. Initially there are no executed actions:
>>> list(actions.executedActions)
[]
So in this case executing the actions does not do anything:
>>> actions.execute()
But if the request contains the information that the button was pressed, the
execution works:
>>> request = TestRequest(form={'form.buttons.apply': 'Apply'})
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
Aehm, something should have happened. But in order for the system to look at
the handlers declared in the form, a special action handler has to be
registered with the system:
>>> zope.component.provideAdapter(button.ButtonActionHandler)
And voila, the execution works:
>>> actions.execute()
successfully applied
Finally, if there is no handler for a button, then the button click is
silently ignored:
>>> form.handlers = button.Handlers()
>>> actions.execute()
While this might seem awkward at first, this is an intended feature. Sometimes
there are several sub-forms that listen to a particular button and one form or
another might simply not care about the button at all and not provide a
handler.
In-Form Button Declarations
---------------------------
Some readers might find it cumbersome to declare a full schema just to create
some buttons. A faster method is to write simple arguments to the button
manager:
>>> class Form(object):
... zope.interface.implements(
... interfaces.IButtonForm, interfaces.IHandlerForm)
... buttons = button.Buttons(
... button.Button('apply', title=u'Apply'))
... prefix = 'form.'
...
... @button.handler(buttons['apply'])
... def apply(self, action):
... print 'successfully applied'
The first argument of the ``Button`` class constructor is the name of the
button. Optionally, this can also be one of the following keyword arguments:
>>> button.Button(name='apply').__name__
'apply'
>>> button.Button(__name__='apply').__name__
'apply'
If no name is specified, the button will not have a name immediately, ...
>>> button.Button(title=u'Apply').__name__
''
because if the button is created within an interface, the name is assigned
later:
>>> class IActions(zope.interface.Interface):
... apply = button.Button(title=u'Apply')
>>> IActions['apply'].__name__
'apply'
However, once the button is added to a button manager, a name will be
assigned:
>>> btns = button.Buttons(button.Button(title=u'Apply'))
>>> btns['apply'].__name__
'apply'
>>> btns = button.Buttons(button.Button(title=u'Apply and more'))
>>> btns['4170706c7920616e64206d6f7265'].__name__
'4170706c7920616e64206d6f7265'
This declaration behaves identical to the one before:
>>> form = Form()
>>> request = TestRequest()
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
When sending in the right information, the actions are executed:
>>> request = TestRequest(form={'form.buttons.apply': 'Apply'})
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
successfully applied
An even simpler method -- resembling closest the API provided by formlib -- is
to create the button and handler at the same time:
>>> class Form(object):
... zope.interface.implements(
... interfaces.IButtonForm, interfaces.IHandlerForm)
... prefix = 'form.'
...
... @button.buttonAndHandler(u'Apply')
... def apply(self, action):
... print 'successfully applied'
In this case the ``buttonAndHandler`` decorator creates a button and a handler
for it. By default the name is computed from the title of the button, which is
required. All (keyword) arguments are forwarded to the button
constructor. Let's now render the form:
>>> request = TestRequest(form={'form.buttons.apply': 'Apply'})
>>> actions = button.ButtonActions(form, request, None)
>>> actions.update()
>>> actions.execute()
successfully applied
If the title is a more complex string, then the name of the button becomes a
hex-encoded string:
>>> class Form(object):
...
... @button.buttonAndHandler(u'Apply and Next')
... def apply(self, action):
... print 'successfully applied'
>>> Form.buttons.keys()
['4170706c7920616e64204e657874']
Of course, you can use the ``__name__`` argument to specify a name
yourself. The decorator, however, also allows the keyword ``name``:
>>> class Form(object):
...
... @button.buttonAndHandler(u'Apply and Next', name='applyNext')
... def apply(self, action):
... print 'successfully applied'
>>> Form.buttons.keys()
['applyNext']
This helper function also supports a keyword argument ``provides``, which
allows the developer to specify a sequence of interfaces that the generated
button should directly provide. Those provided interfaces can be used for a
multitude of things, including handler discrimination and UI layout:
>>> class IMyButton(zope.interface.Interface):
... pass
>>> class Form(object):
...
... @button.buttonAndHandler(u'Apply', provides=(IMyButton,))
... def apply(self, action):
... print 'successfully applied'
>>> IMyButton.providedBy(Form.buttons['apply'])
True
Button Conditions
-----------------
Sometimes it is desirable to only show a button when a certain condition is
fulfilled. The ``Button`` field supports conditions via a simple argument. The
``condition`` argument must be a callable taking the form as argument and
returning a truth-value. If the condition is not fulfilled, the button will not
be converted to an action:
>>> class Form(object):
... prefix='form'
... showApply = True
...
... @button.buttonAndHandler(
... u'Apply', condition=lambda form: form.showApply)
... def apply(self, action):
... print 'successfully applied'
In this case a form variable specifies the availability. Initially the button
is available as action:
>>> myform = Form()
>>> actions = button.ButtonActions(myform, TestRequest(), None)
>>> actions.update()
>>> actions.keys()
['apply']
If we set the show-apply attribute to false, the action will not be available.
>>> myform.showApply = False
>>> actions = button.ButtonActions(myform, TestRequest(), None)
>>> actions.update()
>>> actions.keys()
[]
This feature is very helpful in multi-forms and wizards.
Customizing the Title
---------------------
As for widgets, it is often desirable to change attributes of the button
actions without altering any original code. Again we will be using attribute
value adapters to complete the task. Originally, our title is as follows:
>>> myform = Form()
>>> actions = button.ButtonActions(myform, TestRequest(), None)
>>> actions.update()
>>> actions['apply'].title
u'Apply'
Let's now create a custom label for the action:
>>> ApplyLabel = button.StaticButtonActionAttribute(
... u'Apply now', button=myform.buttons['apply'])
>>> zope.component.provideAdapter(ApplyLabel, name='title')
Once the button action manager is updated, the new title is chosen:
>>> actions.update()
>>> actions['apply'].title
u'Apply now'
The Button Manager
------------------
The button manager contains several additional API methods that make the
management of buttons easy.
First, you are able to add button managers:
>>> bm1 = button.Buttons(IButtons)
>>> bm2 = button.Buttons(button.Button('help', title=u'Help'))
>>> bm1 + bm2
<z3c.form.button.Buttons object at ...>
>>> list(bm1 + bm2)
['apply', 'cancel', 'help']
The result of the addition is another button manager. Also note that the order
of the buttons is preserved throughout the addition. Adding anything else is
not well-defined:
>>> bm1 + 1
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'Buttons' and 'int'
Second, you can select the buttons in a particular order:
>>> bm = bm1 + bm2
>>> list(bm)
['apply', 'cancel', 'help']
>>> list(bm.select('help', 'apply', 'cancel'))
['help', 'apply', 'cancel']
The ``select()`` method can also be used to eliminate another button:
>>> list(bm.select('help', 'apply'))
['help', 'apply']
Of course, in the example above we eliminated one and reorganized the buttons.
Third, you can omit one or more buttons:
>>> list(bm.omit('cancel'))
['apply', 'help']
Finally, while the constructor is very flexible, you cannot just pass in
anything:
>>> button.Buttons(1, 2)
Traceback (most recent call last):
...
TypeError: ('Unrecognized argument type', 1)
When creating a new form derived from another, you often want to keep existing
buttons and add new ones. In order not to change the super-form class, you need
to copy the button manager:
>>> bm.keys()
['apply', 'cancel', 'help']
>>> bm.copy().keys()
['apply', 'cancel', 'help']
The Handlers Object
-------------------
All handlers of a form are collected in the ``handlers`` attribute, which is a
``Handlers`` instance:
>>> isinstance(form.handlers, button.Handlers)
True
>>> form.handlers
<Handlers [<Handler for <Button 'apply' u'Apply'>>]>
Internally the object uses an adapter registry to manage the handlers for
buttons. If a handler is registered for a button, it simply behaves as an
instance-adapter.
The object itself is pretty simple. You can get a handler as follows:
>>> apply = form.buttons['apply']
>>> form.handlers.getHandler(apply)
<Handler for <Button 'apply' u'Apply'>>
But you can also register handlers for groups of buttons, either by interface
or class:
>>> class SpecialButton(button.Button):
... pass
>>> def handleSpecialButton(form, action):
... return 'Special button action'
>>> form.handlers.addHandler(
... SpecialButton, button.Handler(SpecialButton, handleSpecialButton))
>>> form.handlers
<Handlers
[<Handler for <Button 'apply' u'Apply'>>,
<Handler for <class 'SpecialButton'>>]>
Now all special buttons should use that handler:
>>> button1 = SpecialButton(name='button1', title=u'Button 1')
>>> button2 = SpecialButton(name='button2', title=u'Button 2')
>>> form.handlers.getHandler(button1)(form, None)
'Special button action'
>>> form.handlers.getHandler(button2)(form, None)
'Special button action'
However, registering a more specific handler for button 1 will override the
general handler:
>>> def handleButton1(form, action):
... return 'Button 1 action'
>>> form.handlers.addHandler(
... button1, button.Handler(button1, handleButton1))
>>> form.handlers.getHandler(button1)(form, None)
'Button 1 action'
>>> form.handlers.getHandler(button2)(form, None)
'Special button action'
You can also add handlers objects:
>>> handlers2 = button.Handlers()
>>> button3 = SpecialButton(name='button3', title=u'Button 3')
>>> handlers2.addHandler(
... button3, button.Handler(button3, None))
>>> form.handlers + handlers2
<Handlers
[<Handler for <Button 'apply' u'Apply'>>,
<Handler for <class 'SpecialButton'>>,
<Handler for <SpecialButton 'button1' u'Button 1'>>,
<Handler for <SpecialButton 'button3' u'Button 3'>>]>
However, adding other components is not supported:
>>> form.handlers + 1
Traceback (most recent call last):
...
NotImplementedError
The handlers also provide a method to copy the handlers to a new instance:
>>> copy = form.handlers.copy()
>>> isinstance(copy, button.Handlers)
True
>>> copy is form.handlers
False
This is commonly needed when one wants to extend the handlers of a super-form.
Image Buttons
-------------
A special type of button is the image button. Instead of creating a "submit"-
or "button"-type input, an "image" button is created. An image button is a
simple extension of a button, requiring an `image` argument to the constructor:
>>> imgSubmit = button.ImageButton(
... name='submit',
... title=u'Submit',
... image=u'submit.png')
>>> imgSubmit
<ImageButton 'submit' u'submit.png'>
Some browsers do not submit the value of the input, but only the coordinates
of the image where the mouse click occurred. Thus we also need a special
button action:
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> imgSubmitAction = button.ImageButtonAction(request, imgSubmit)
>>> imgSubmitAction
<ImageButtonAction 'submit' u'Submit'>
Initially, we did not click on the image:
>>> imgSubmitAction.isExecuted()
False
Now the button is clicked:
>>> request = TestRequest(form={'submit.x': '3', 'submit.y': '4'})
>>> imgSubmitAction = button.ImageButtonAction(request, imgSubmit)
>>> imgSubmitAction.isExecuted()
True
The "image" type of the "input"-element also requires there to be a `src`
attribute, which is the URL to the image to be used. The attribute is also
supported by the Python API. However, in order for the attribute to work, the
image must be available as a resource, so let's do that now:
# Traversing setup
>>> from zope.traversing import testing
>>> testing.setUp()
# Resource namespace
>>> import zope.component
>>> from zope.traversing.interfaces import ITraversable
>>> from zope.traversing.namespace import resource
>>> zope.component.provideAdapter(
... resource, (None,), ITraversable, name="resource")
>>> zope.component.provideAdapter(
... resource, (None, None), ITraversable, name="resource")
# Register the "submit.png" resource
>>> from zope.app.publisher.browser.resource import Resource
>>> testing.browserResource('submit.png', Resource)
Now the attribute can be called:
>>> imgSubmitAction.src
u'http://127.0.0.1/@@/submit.png'
==========
Directives
==========
Widget template directive
-------------------------
Show how we can use the widget template directive. Register the meta
configuration for the directive.
>>> import sys
>>> from zope.configuration import xmlconfig
>>> import z3c.form
>>> context = xmlconfig.file('meta.zcml', z3c.form)
We need a custom widget template
>>> import os, tempfile
>>> temp_dir = tempfile.mkdtemp()
>>> file = os.path.join(temp_dir, 'widget.pt')
>>> open(file, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <input type="text" id="" name="" value="" size=""
... tal:attributes="id view/id;
... name view/name;
... size view/size;
... value view/value;" />
... </html>
... ''')
and a interface
>>> import zope.interface
>>> from z3c.form import interfaces
>>> class IMyWidget(interfaces.IWidget):
... """My widget interface."""
and a widget class:
>>> from z3c.form.testing import TestRequest
>>> from z3c.form.browser import text
>>> class MyWidget(text.TextWidget):
... zope.interface.implements(IMyWidget)
>>> request = TestRequest()
>>> myWidget = MyWidget(request)
Make them available under the fake package ``custom``:
>>> sys.modules['custom'] = type(
... 'Module', (),
... {'IMyWidget': IMyWidget})()
and register them as a widget template within the ``z3c:widgetTemplate``
directive:
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:widgetTemplate
... template="%s"
... widget="custom.IMyWidget"
... />
... </configure>
... """ % file, context=context)
Let's get the template
>>> import zope.component
>>> from z3c.template.interfaces import IPageTemplate
>>> template = zope.component.queryMultiAdapter((None, request, None, None,
... myWidget), interface=IPageTemplate, name='input')
and check it:
>>> from z3c.form.ptcompat import ViewPageTemplateFile
>>> isinstance(template, ViewPageTemplateFile)
True
Let's use the template within the widget.
>>> print template(myWidget)
<input type="text" value="" />
We normally render the widget which returns the registered template.
>>> print myWidget.render()
<input type="text" value="" />
If the template does not exist, then the widget directive should fail
immediately:
>>> unknownFile = os.path.join(temp_dir, 'unknown.pt')
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:widgetTemplate
... template="%s"
... widget="custom.IMyWidget"
... />
... </configure>
... """ % unknownFile, context=context)
Traceback (most recent call last):
...
ZopeXMLConfigurationError: File "<string>", line 4.2-7.8
ConfigurationError: ('No such file', '...unknown.pt')
Object Widget template directive
--------------------------------
Show how we can use the objectwidget template directive.
The big difference between the 'simple' Widget template and the Object Widget
directive is that the Object Widget template takes the field's schema into
account. That makes it easy to register different widget templates for different
sub-schemas. You can use this together with SubformAdapter to get a totally
custom subwidget.
We need a custom widget template
>>> file = os.path.join(temp_dir, 'widget.pt')
>>> open(file, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <div class="object-widget" tal:attributes="class view/klass">
... yeah, this can get comlex
... </div>
... </html>
... ''')
and a interface
>>> class IMyObjectWidget(interfaces.IObjectWidget):
... """My objectwidget interface."""
and a widget class:
>>> from z3c.form.browser import object
>>> class MyObjectWidget(object.ObjectWidget):
... zope.interface.implements(IMyObjectWidget)
>>> request = TestRequest()
>>> myObjectWidget = MyObjectWidget(request)
>>> from z3c.form.testing import IMySubObject
>>> import zope.schema
>>> field = zope.schema.Object(
... __name__='subobject',
... title=u'my object widget',
... schema=IMySubObject)
>>> myObjectWidget.field = field
Make them available under the fake package ``custom``:
>>> sys.modules['custom'] = type(
... 'Module', (),
... {'IMyObjectWidget': IMyObjectWidget})()
and register them as a widget template within the ``z3c:objectWidgetTemplate``
directive:
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:objectWidgetTemplate
... template="%s"
... widget="custom.IMyObjectWidget"
... />
... </configure>
... """ % file, context=context)
Let's get the template
>>> template = zope.component.queryMultiAdapter((None, request, None, None,
... myObjectWidget, None), interface=IPageTemplate, name='input')
and check it:
>>> isinstance(template, ViewPageTemplateFile)
True
Let's use the template within the widget.
>>> print template(myObjectWidget)
<div class="object-widget">yeah, this can get comlex</div>
We normally render the widget which returns the registered template.
>>> print myObjectWidget.render()
<div class="object-widget">yeah, this can get comlex</div>
If the template does not exist, then the widget directive should fail
immediately:
>>> unknownFile = os.path.join(temp_dir, 'unknown.pt')
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:objectWidgetTemplate
... template="%s"
... widget="custom.IMyObjectWidget"
... />
... </configure>
... """ % unknownFile, context=context)
Traceback (most recent call last):
...
ZopeXMLConfigurationError: File "<string>", line 4.2-7.8
ConfigurationError: ('No such file', '...unknown.pt')
Register a specific template for a schema:
We need a custom widget template
>>> file = os.path.join(temp_dir, 'widgetspec.pt')
>>> open(file, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <div class="object-widget" tal:attributes="class view/klass">
... this one is specific
... </div>
... </html>
... ''')
>>> context = xmlconfig.string("""
... <configure
... xmlns:z3c="http://namespaces.zope.org/z3c">
... <z3c:objectWidgetTemplate
... template="%s"
... widget="custom.IMyObjectWidget"
... schema="z3c.form.testing.IMySubObject"
... />
... </configure>
... """ % file, context=context)
Let's get the template
>>> template = zope.component.queryMultiAdapter((None, request, None, None,
... myObjectWidget, None), interface=IPageTemplate, name='input')
and check it:
>>> print myObjectWidget.render()
<div class="object-widget">this one is specific</div>
Cleanup
-------
Now we need to clean up the custom module.
>>> del sys.modules['custom']
Also let's not leave temporary files lying around
>>> import shutil
>>> shutil.rmtree(temp_dir)
==========
Validators
==========
Validators are components that validate submitted data. This is certainly not
a new concept, but in the previous form frameworks validation was hidden in
many places:
* Field/Widget Validation
The schema field consists of a ``validate()`` method. Validation is
automatically invoked when converting a unicode string to a field value
using ``fromUnicode()``. This makes it very hard to customize the field
validation. No hooks were provided to exert dditional restriction at the
presentation level.
* Schema/Form Validation
This type of validation was not supported at all initially. ``zope.formlib``
fixed this problem by validating against schema invariants. While this was a
first good step, it still made it hard to customize validators, since it
required touching the base implementations of the forms.
* Action Validation
``zope.formlib`` supports the notion of action validatos. Actions have a
success and failure handler. If the validation succeeds, the success handler
is called, otherwise the failure handler is chosen. We believe that this
design was ill-conceived, especially the default, which required the data to
completely validate in order for the action to successful. There are many
actions that do not even care about the data in the form, such as "Help",
"Cancel" and "Reset" buttons. Thus validation should be part of the data
retrieval process and not the action.
For me, the primary goals of the validator framework are as follows:
* Assert additional restrictions on the data at the presentation
level.
There are several use cases for this. Sometimes clients desire additional
restrictions on data for their particular version of the software. It is not
always desireable to adjust the model for this client, since the framework
knows how to handle the less restrictive case anyways. In another case,
additional restrictions might be applied to a particular form due to limited
restrictions.
* Make validation pluggable.
Like most other components of this package, it should be possible to control
the validation adapters at a fine grained level.
* Widgets: context, request, view, field[1], widget
* Widget Managers: context, request, view, schema[2], manager
[1].. This is optional, since widgets must not necessarily have fields.
[2].. This is optional, since widget managers must not necessarily have
manage field widgets and thus know about schemas.
* Provide good defaults that behave sensibly.
Good defaults are, like in anywhere in this pacakge, very important. We have
chosen to implement the ``zope.formlib`` behavior as the default, since it
worked very well -- with exception of action validation, of course.
For this package, we have decided to support validators at the widget and
widget manager level. By default the framework only supports field widgets,
since the validation of field-absent widgets is generally not
well-defined. Thus, we first need to create a schema.
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... login = zope.schema.TextLine(
... title=u'Login',
... min_length=1,
... max_length=10)
...
... email = zope.schema.TextLine(
... title=u'E-mail')
...
... @zope.interface.invariant
... def isLoginPartOfEmail(person):
... if not person.email.startswith(person.login):
... raise zope.interface.Invalid("The login not part of email.")
Widget Validators
-----------------
Widget validators only validate the data of one particular widget. The
validated value is always assumed to be an internal value and not a widget
value.
By default, the system uses the simple field validator, which simply uses the
``validate()`` method of the field. For instantiation, all validators have the
following signature for its discriminators: context, request, view, field, and
widget
>>> from z3c.form import validator
>>> simple = validator.SimpleFieldValidator(
... None, None, None, IPerson['login'], None)
A validator has a single method ``validate()``. When the validation is
successful, ``None`` is returned:
>>> simple.validate(u'srichter')
A validation error is raised, when the validation fails:
>>> simple.validate(u'StephanCaveman3')
Traceback (most recent call last):
...
TooLong: (u'StephanCaveman3', 10)
Let's now create a validator that also requires at least 1 numerical character
in the login name:
>>> import re
>>> class LoginValidator(validator.SimpleFieldValidator):
...
... def validate(self, value):
... super(LoginValidator, self).validate(value)
... if re.search('[0-9]', value) is None:
... raise zope.interface.Invalid('No numerical character found.')
Let's now try our new validator:
>>> login = LoginValidator(None, None, None, IPerson['login'], None)
>>> login.validate(u'srichter1')
>>> login.validate(u'srichter')
Traceback (most recent call last):
...
Invalid: No numerical character found.
We can now register the validator with the component architecture, ...
>>> import zope.component
>>> zope.component.provideAdapter(LoginValidator)
and look up the adapter using the usual way:
>>> from z3c.form import interfaces
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['login'], None),
... interfaces.IValidator)
<LoginValidator for IPerson['login']>
Unfortunately, the adapter is now registered for all fields, so that the
E-mail field also has this restriction (which is okay in this case, but not
generally):
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['email'], None),
... interfaces.IValidator)
<LoginValidator for IPerson['email']>
The validator module provides a helper function to set the discriminators for
a validator, which can include instances:
>>> validator.WidgetValidatorDiscriminators(
... LoginValidator, field=IPerson['login'])
Let's now clean up the component architecture and register the login validator
again:
>>> from zope.testing import cleanup
>>> cleanup.cleanUp()
>>> zope.component.provideAdapter(LoginValidator)
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['login'], None),
... interfaces.IValidator)
<LoginValidator for IPerson['login']>
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson['email'], None),
... interfaces.IValidator)
Widget Validators and File-Uploads
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
File-Uploads behave a bit different than the other form
elements. Whether the user did not choose a file to upload
``interfaces.NOT_CHANGED`` is set as value. But the validator knows
how to handle this.
The example has two bytes fields where File-Uploads are possible, one
field is required the other one not:
>>> class IPhoto(zope.interface.Interface):
... data = zope.schema.Bytes(
... title=u'Photo')
...
... thumb = zope.schema.Bytes(
... title=u'Thumbnail',
... required=False)
There are several possible cases to differentiate between:
No widget
+++++++++
If there is no widget or the widget does not provide
``interfaces.IContextAware``, no value is looked up from the
context. So the not required field validates successfully but the
required one has an required missing error, as the default value of
the field is looked up on the field:
>>> simple_thumb = validator.SimpleFieldValidator(
... None, None, None, IPhoto['thumb'], None)
>>> simple_thumb.validate(interfaces.NOT_CHANGED)
>>> simple_data = validator.SimpleFieldValidator(
... None, None, None, IPhoto['data'], None)
>>> simple_data.validate(interfaces.NOT_CHANGED)
Traceback (most recent call last):
RequiredMissing
Widget which ignores context
++++++++++++++++++++++++++++
If the context is ignored in the widget - as in the add form - the
behavior is the same as if there was no widget:
>>> import z3c.form.widget
>>> widget = z3c.form.widget.Widget(None)
>>> zope.interface.alsoProvides(widget, interfaces.IContextAware)
>>> widget.ignoreContext = True
>>> simple_thumb = validator.SimpleFieldValidator(
... None, None, None, IPhoto['thumb'], widget)
>>> simple_thumb.validate(interfaces.NOT_CHANGED)
>>> simple_data = validator.SimpleFieldValidator(
... None, None, None, IPhoto['data'], widget)
>>> simple_data.validate(interfaces.NOT_CHANGED)
Traceback (most recent call last):
RequiredMissing
Look up value from default adapter
++++++++++++++++++++++++++++++++++
When the value is ``interfaces.NOT_CHANGED`` the validator tries to
look up the default value using a ``interfaces.IValue``
adapter. Whether the adapter is found, its value is used as default,
so the validation of the required field is successful here:
>>> data_default = z3c.form.widget.StaticWidgetAttribute(
... 'data', context=None, request=None, view=None,
... field=IPhoto['data'], widget=widget)
>>> zope.component.provideAdapter(data_default, name='default')
>>> simple_data.validate(interfaces.NOT_CHANGED)
Look up value from context
++++++++++++++++++++++++++
If there is a context aware widget which does not ignore its context,
the value is looked up on the context using a data manager:
>>> class Photo(object):
... zope.interface.implements(IPhoto)
...
... data = None
... thumb = None
>>> photo = Photo()
>>> widget.ignoreContext = False
>>> zope.component.provideAdapter(z3c.form.datamanager.AttributeField)
>>> simple_thumb = validator.SimpleFieldValidator(
... photo, None, None, IPhoto['thumb'], widget)
>>> simple_thumb.validate(interfaces.NOT_CHANGED)
If the value is not set on the context it is a required missing as
neither context nor input have a valid value:
>>> simple_data = validator.SimpleFieldValidator(
... photo, None, None, IPhoto['data'], widget)
>>> simple_data.validate(interfaces.NOT_CHANGED)
Traceback (most recent call last):
RequiredMissing
After setting the value validation is successful:
>>> photo.data = 'data'
>>> simple_data.validate(interfaces.NOT_CHANGED)
Clean-up
++++++++
>>> gsm = zope.component.getGlobalSiteManager()
>>> gsm.unregisterAdapter(z3c.form.datamanager.AttributeField)
True
>>> gsm.unregisterAdapter(data_default, name='default')
True
Widget Manager Validators
-------------------------
The widget manager validator, while similar in spirit, works somewhat
different. The discriminators of the widget manager validator are: context,
request, view, schema, and manager.
A simple default implementation is provided that checks the invariants of the
schemas:
>>> invariants = validator.InvariantsValidator(
... None, None, None, IPerson, None)
Widget manager validators have the option to validate a data dictionary,
>>> invariants.validate(
... {'login': u'srichter', 'email': u'srichter@foo.com'})
()
or an object implementing the schema:
>>> class Person(object):
... zope.interface.implements(IPerson)
... login = u'srichter'
... email = u'srichter@foo.com'
>>> stephan = Person()
>>> invariants.validateObject(stephan)
()
Since multiple errors can occur during the validation process, all errors are
collected in a tuple, which is returned. If the tuple is empty, the validation
was successful. Let's now generate a failure:
>>> errors = invariants.validate(
... {'login': u'srichter', 'email': u'strichter@foo.com'})
>>> for e in errors:
... print e.__class__.__name__ + ':', e
Invalid: The login not part of email.
Let's now have a look at writing a custom validator. In this case, we want to
ensure that the E-mail address is at most twice as long as the login:
>>> class CustomValidator(validator.InvariantsValidator):
... def validateObject(self, obj):
... errors = super(CustomValidator, self).validateObject(obj)
... if len(obj.email) > 2 * len(obj.login):
... errors += (zope.interface.Invalid('Email too long.'),)
... return errors
Since the ``validate()`` method of ``InvatiantsValidator`` simply uses
``validateObject()`` it is enough to only override ``validateObject()``. Now
we can use the validator:
>>> custom = CustomValidator(
... None, None, None, IPerson, None)
>>> custom.validate(
... {'login': u'srichter', 'email': u'srichter@foo.com'})
()
>>> errors = custom.validate(
... {'login': u'srichter', 'email': u'srichter@foobar.com'})
>>> for e in errors:
... print e.__class__.__name__ + ':', e
Invalid: Email too long.
To register the custom validator only for this schema, we have to use the
discriminator generator again.
>>> from z3c.form import util
>>> validator.WidgetsValidatorDiscriminators(
... CustomValidator, schema=util.getSpecification(IPerson, force=True))
Note: Of course we could have used the ``zope.component.adapts()`` function
from within the class, but I think it is too tedious, since you have to
specify all discriminators and not only the specific ones you are
interested in.
After registering the validator,
>>> zope.component.provideAdapter(CustomValidator)
it becomes the validator for this schema:
>>> zope.component.queryMultiAdapter(
... (None, None, None, IPerson, None), interfaces.IManagerValidator)
<CustomValidator for IPerson>
>>> class ICar(zope.interface.Interface):
... pass
>>> zope.component.queryMultiAdapter(
... (None, None, None, ICar, None), interfaces.IManagerValidator)
The Data Wrapper
----------------
The ``Data`` class provides a wrapper to present a dictionary as a class
instance. This is used to check for invariants, which always expect an
object. While the common use cases of the data wrapper are well tested in the
code above, there are some corner cases that need to be addressed.
So let's start by creating a data object:
>>> context = object()
>>> data = validator.Data(IPerson, {'login': 'srichter', 'other': 1}, context)
When we try to access a name that is not in the schema, we get an attribute
error:
>>> data.address
Traceback (most recent call last):
...
AttributeError: address
>>> data.other
Traceback (most recent call last):
...
AttributeError: other
If the field found is a method, then a runtime error is raised:
>>> class IExtendedPerson(IPerson):
... def compute():
... """Compute something."""
>>> data = validator.Data(IExtendedPerson, {'compute': 1}, context)
>>> data.compute
Traceback (most recent call last):
...
RuntimeError: ('Data value is not a schema field', 'compute')
Finally, the context is available as attribute directly:
>>> data.__context__ is context
True
It is used by the validators (especially invariant validators) to provide a
context of validation, for example to look up a vocabulary or access the
parent of an object. Note that the context will be different between add and
edit forms.
Validation of interface variants when not all fields are displayed in form
--------------------------------------------------------------------------
We need to register the data manager to access the data on the context object:
>>> from z3c.form import datamanager
>>> zope.component.provideAdapter(datamanager.AttributeField)
Sometimes you might leave out fields in the form which need to compute the
invariant. An exception should be raised. The data wrapper is used to test
the invariants and looks up values on the context object that are left out in
the form.
>>> invariants = validator.InvariantsValidator(
... stephan, None, None, IPerson, None)
>>> errors = invariants.validate({'email': 'foo@bar.com'})
>>> errors[0].__class__.__name__
'Invalid'
>>> errors[0].args[0]
'The login not part of email.'
=======
Widgets
=======
Widgets are small UI components that accept and process the textual user
input. The only responsibility of a widget is to represent a value to the
user, allow it to be modified and then return a new value. Good examples of
widgets include the Qt widgets and HTML widgets. The widget is not responsible
for converting its value to the desired internal value or validate the
incoming data. These responsibilities are passed data converters and
validators, respectively.
There are several problems that can be identified in the original Zope 3 widget
implementation located at ``zope.app.form``.
(1) Field Dependence -- Widgets are always views of fields. While this might
be a correct choice for a high-level API, it is fundamentally wrong. It
disallows us to use widgets without defining fields. This also couples
certain pieces of information too tightly to the field, especially, value
retrieval from and storage to the context, validation and raw data
conversion.
(2) Form Dependence -- While widgets do not have to be located within a form,
they are usually tightly coupled to it. It is very difficult to use
widgets outside the context of a form.
(3) Traversability -- Widgets cannot be traversed, which means that they
cannot interact easily using Javascript. This is not a fundamental
problem, but simply a lack of the current design to recognize that small
UI components must also be traversable and thus have a URI.
(4) Customizability -- A consequence of issue (1) is that widgets are not
customizable enough. Implementing real-world projects has shown that
widgets often want a very fine-grained ability to customize values. A
prime example is the label. Because the label of a widget is retrieved
from the field title, it is impossible to provide an alternative label for
a widget. While the label could be changed from the form, this would
require rewriting the entire form to change a label. Instead, we often
endde up writing cusom schemas.
(5) Flexibility -- Oftentimes it is desired to have one widget, but multiple
styles of representation. For example, in one scenario the widget uses a
plain HTML widget and in another a fancy JavaScript widget is used. The
current implementation makes it very hard to provide alternative styles
for a widget.
Creating and Using Simple Widgets
---------------------------------
When using the widget API by itself, the simplest way to use it is to just
instantiate it using the request:
>>> from z3c.form.testing import TestRequest
>>> from z3c.form import widget
>>> request = TestRequest()
>>> age = widget.Widget(request)
In this case we instantiated a generic widget. A full set of simple
browser-based widgets can be found in the ``browser/`` package. Since no
helper components are around to fill the attributes of the widget, we have to
do it by hand:
>>> age.name = 'age'
>>> age.label = u'Age'
>>> age.value = '39'
The most important attributes are the "name" and the "value". The name is used
to identify the widget within the form. The value is either the value to be
manipulated or the default value. The value must be provided in the form the
widget needs it. It is the responsibility of a data converter to convert
between the widget value and the desired internal value.
Before we can render the widget, we have to register a template for the
widget. The first step is to define the template:
>>> import tempfile
>>> textWidgetTemplate = tempfile.mktemp('text.pt')
>>> open(textWidgetTemplate, 'w').write('''
... <html xmlns="http://www.w3.org/1999/xhtml"
... xmlns:tal="http://xml.zope.org/namespaces/tal"
... tal:omit-tag="">
... <input type="text" name="" value=""
... tal:attributes="name view/name; value view/value;" />
... </html>
... ''')
Next, we have to create a template factory for the widget:
>>> from z3c.form.widget import WidgetTemplateFactory
>>> factory = WidgetTemplateFactory(
... textWidgetTemplate, widget=widget.Widget)
The first argument, which is also required, is the path to the template
file. An optional ``content_type`` keyword argument allows the developer to
specify the output content type, usually "text/html". Then there are five
keyword arguments that specify the discriminators of the template:
* ``context`` -- This is the context in which the widget is displayed. In a
simple widget like the one we have now, the context is ``None``.
* ``request`` -- This discriminator allows you to specify the type of request
for which the widget will be available. In our case this would be a browser
request. Note that browser requests can be further broken into layer, so you
could also specify a layer interface here.
* ``view`` -- This is the view from which the widget is used. The simple
widget at hand, does not have a view associated with it though.
* ``field`` -- This is the field for which the widget provides a
representation. Again, this simple widget does not use a field, so it is
``None``.
* ``widget`` -- This is the widget itself. With this discriminator you can
specify for which type of widget you are providing a template.
We can now register the template factory. The name of the factory is the mode
of the widget. By default, there are two widget modes: "input" and
"display". However, since the mode is just a string, one can develop other
kinds of modes as needed for a project. The default mode is "input":
>>> from z3c.form import interfaces
>>> age.mode is interfaces.INPUT_MODE
True
>>> import zope.component
>>> zope.component.provideAdapter(factory, name=interfaces.INPUT_MODE)
Once everything is set up, the widget is updated and then rendered:
>>> age.update()
>>> print age.render()
<input type="text" name="age" value="39" />
If a value is found in the request, it takes precedence, since the user
entered the value:
>>> age.request = TestRequest(form={'age': '25'})
>>> age.update()
>>> print age.render()
<input type="text" name="age" value="25" />
However, there is an option to turn off all request data:
>>> age.value = '39'
>>> age.ignoreRequest = True
>>> age.update()
>>> print age.render()
<input type="text" name="age" value="39" />
Creating and Using Field Widgets
--------------------------------
An extended form of the widget allows fields to control several of the
widget's properties. Let's create a field first:
>>> ageField = zope.schema.Int(
... __name__ = 'age',
... title = u'Age',
... min = 0,
... max = 130)
We can now use our simple widget and create a field widget from it:
>>> ageWidget = widget.FieldWidget(ageField, age)
Such a widget provides ``IFieldWidget``:
>>> interfaces.IFieldWidget.providedBy(ageWidget)
True
Of course, this is more commonly done using an adapter. Commonly those
adapters look like this:
>>> @zope.component.adapter(zope.schema.Int, TestRequest)
... @zope.interface.implementer(interfaces.IFieldWidget)
... def IntWidget(field, request):
... return widget.FieldWidget(field, widget.Widget(request))
>>> zope.component.provideAdapter(IntWidget)
>>> ageWidget = zope.component.getMultiAdapter((ageField, request),
... interfaces.IFieldWidget)
Now we just have to update and render the widget:
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" />
There is no initial value for the widget, since there is no value in the
request and the field does not provide a default. Let's now give our field a
default value and see what happens:
>>> ageField.default = 30
>>> ageWidget.update()
Traceback (most recent call last):
...
TypeError: ('Could not adapt', <Widget 'age'>,
<InterfaceClass z3c.form.interfaces.IDataConverter>)
In order for the widget to be able to take the field's default value and use
it to provide an initial value the widget, we need to provide a data converter
that defines how to convert from the field value to the widget value.
>>> from z3c.form import converter
>>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)
>>> zope.component.provideAdapter(converter.FieldDataConverter)
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="30" />
Again, the request value is honored above everything else:
>>> ageWidget.request = TestRequest(form={'age': '25'})
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="25" />
Creating and Using Context Widgets
----------------------------------
When widgets represent an attribute value of an object, then this object must
be set as the context of the widget:
>>> class Person(object):
... age = 45
>>> ageWidget.context = Person()
>>> zope.interface.alsoProvides(ageWidget, interfaces.IContextAware)
The result is that the context value takes over precendence over the default
value:
>>> ageWidget.request = TestRequest()
>>> ageWidget.update()
Traceback (most recent call last):
...
ComponentLookupError: ((...), <InterfaceClass ...IDataManager>, u'')
This call fails because the widget does not know how to extract the value from
the context. Registering a data manager for the widget does the trick:
>>> from z3c.form import datamanager
>>> zope.component.provideAdapter(datamanager.AttributeField)
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="45" />
The context can be explicitely ignored, making the widget display the default
value again:
>>> ageWidget.ignoreContext = True
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="30" />
Again, the request value is honored above everything else:
>>> ageWidget.request = TestRequest(form={'age': '25'})
>>> ageWidget.ignoreContext = False
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="25" />
But what happens if the object we are working on is security proxied? In
particular, what happens, if the access to the attribute is denied. To see
what happens, we have to create a proxied person:
>>> from zope.security import checker
>>> PersonChecker = checker.Checker({'age': 'Access'}, {'age': 'Edit'})
>>> ageWidget.request = TestRequest()
>>> ageWidget.context = checker.ProxyFactory(Person(), PersonChecker)
After changing the security policy, ...
>>> from zope.security import management
>>> from z3c.form import testing
>>> management.endInteraction()
>>> newPolicy = testing.SimpleSecurityPolicy()
>>> oldPolicy = management.setSecurityPolicy(newPolicy)
>>> management.newInteraction()
it is not possible anymore to update the widget:
>>> ageWidget.update()
Traceback (most recent call last):
...
Unauthorized: (<Person object at ...>, 'age', 'Access')
If no security declaration has been made at all, we get a
``ForbiddenAttribute`` error:
>>> ageWidget.context = checker.ProxyFactory(Person(), checker.Checker({}))
>>> ageWidget.update()
Traceback (most recent call last):
...
ForbiddenAttribute: ('age', <Person object at ...>)
Let's clean up the setup:
>>> management.endInteraction()
>>> newPolicy = management.setSecurityPolicy(oldPolicy)
>>> management.newInteraction()
>>> ageWidget.context = Person()
Dynamically Changing Attribute Values
-------------------------------------
Once widgets are used within a framework, it is very tedious to write Python
code to adjust certain attributes, even though hooks exist. The easiest way to
change those attribute values is actually to provide an adapter that provides
the custom value.
We can create a custom label for the age widget:
>>> AgeLabel = widget.StaticWidgetAttribute(
... u'Current Age',
... context=None, request=None, view=None, field=ageField, widget=None)
Clearly, this code does not require us to touch the orginal form and widget
code, given that we have enough control over the selection. In the example
above, all the selection discriminators are listed for demonstration
purposes. Of course, the label in this case can be created as follows:
>>> AgeLabel = widget.StaticWidgetAttribute(u'Current Age', field=ageField)
Much better, isn't it? Initially the label is the title of the field:
>>> ageWidget.label
u'Age'
Let's now simply register the label as a named adapter; the name is the name
of the attribute to change:
>>> zope.component.provideAdapter(AgeLabel, name='label')
Asking the widget for the label now will return the newly registered label:
>>> ageWidget.update()
>>> ageWidget.label
u'Current Age'
Of course, simply setting the label or changing the label extraction via a
sub-class are other options you might want to consider. Furthermore, you
could also create a computed attribute value or implement your own component.
Overriding other attributes, such as ``required``, is done in the same
way. If any widget provides new attributes, they are also overridable this
way. For example, the selection widget defines a label for the option that no
value was selected. We often want to override this, because the German
translation sucks or the wording is often too generic. Widget implementation
should add names of overridable attributes to their "_adapterValueAttributes"
internal attribute.
Let's try to override the ``required`` attribute. By default the widget is required,
because the field is required as well:
>>> ageWidget.required
True
Let's provide a static widget attribute adapter with name "required":
>>> AgeNotRequired = widget.StaticWidgetAttribute(False, field=ageField)
>>> zope.component.provideAdapter(AgeNotRequired, name="required")
Now, let's check if it works:
>>> ageWidget.update()
>>> ageWidget.required
False
Overriding the default value is somewhat special due to the complexity of
obtaining the value. So let's register one now:
>>> AgeDefault = widget.StaticWidgetAttribute(50, field=ageField)
>>> zope.component.provideAdapter(AgeDefault, name="default")
Let's now instantiate, update and render the widget to see the default value:
>>> ageWidget = zope.component.getMultiAdapter((ageField, request),
... interfaces.IFieldWidget)
>>> ageWidget.update()
>>> print ageWidget.render()
<input type="text" name="age" value="50" />
Sequence Widget
---------------
A common use case in user interfaces is to ask the user to select one or more
items from a set of options/choices. The ``widget`` module provides a basic
widget implementation to support this use case.
The options available for selections are known as terms. Initially, there are
no terms:
>>> request = TestRequest()
>>> seqWidget = widget.SequenceWidget(request)
>>> seqWidget.name = 'seq'
>>> seqWidget.terms is None
True
There are two ways terms can be added, either manually or via an
adapter. Those term objects must provide ``ITerms``. There is no simple
default implementation, so we have to provide one ourselves:
>>> from zope.schema import vocabulary
>>> class Terms(vocabulary.SimpleVocabulary):
... zope.interface.implements(interfaces.ITerms)
... def getValue(self, token):
... return self.getTermByToken(token).value
>>> terms = Terms(
... [Terms.createTerm(1, 'v1', u'Value 1'),
... Terms.createTerm(2, 'v2', u'Value 2'),
... Terms.createTerm(3, 'v3', u'Value 3')])
>>> seqWidget.terms = terms
Once the ``terms`` attribute is set, updating the widgets does not change the
terms:
>>> seqWidget.update()
>>> [term.value for term in seqWidget.terms]
[1, 2, 3]
The value of a sequence widget is a tuple/list of term tokens. When extracting
values from the request, the values must be valid tokens, otherwise the
default value is returned:
>>> seqWidget.request = TestRequest(form={'seq': ['v1']})
>>> seqWidget.extract()
['v1']
>>> seqWidget.request = TestRequest(form={'seq': ['v4']})
>>> seqWidget.extract()
<NO_VALUE>
>>> seqWidget.request = TestRequest(form={'seq-empty-marker': '1'})
>>> seqWidget.extract()
[]
Note that we also support single values being returned outside a sequence. The
extracted value is then wrapped by a tuple. This feature is useful when
integrating with third-party client frameworks that do not know about the Zope
naming conventions.
>>> seqWidget.request = TestRequest(form={'seq': 'v1'})
>>> seqWidget.extract()
('v1',)
If the no-value token has been selected, it is returned without further
verification:
>>> seqWidget.request = TestRequest(form={'seq': [seqWidget.noValueToken]})
>>> seqWidget.extract()
['--NOVALUE--']
Since the value of the widget is a tuple of tokens, when displaying the
values, they have to be converted to the title of the term:
>>> seqWidget.value = ('v1', 'v2')
>>> seqWidget.displayValue
[u'Value 1', u'Value 2']
When input forms are directly switched to display forms within the same
request, it can happen that the value contains the "--NOVALUE--" token
entry. This entry should be silently ignored:
>>> seqWidget.value = (seqWidget.noValueToken,)
>>> seqWidget.displayValue
[]
To demonstrate how the terms is automatically chosen by a widget, we should
instantiate a field widget. Let's do this with a choice field:
>>> seqField = zope.schema.Choice(
... title=u'Sequence Field',
... vocabulary=terms)
Let's now create the field widget:
>>> seqWidget = widget.FieldWidget(seqField, widget.SequenceWidget(request))
>>> seqWidget.terms
The terms should be available as soon as the widget is updated:
>>> seqWidget.update()
Traceback (most recent call last):
...
ComponentLookupError: ((...), <InterfaceClass ...ITerms>, u'')
This failed, because we did not register an adapter for the terms yet. After
the adapter is registered, everything should work as expected:
>>> from z3c.form import term
>>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
>>> zope.component.provideAdapter(term.ChoiceTerms)
>>> seqWidget.update()
>>> seqWidget.terms
<z3c.form.term.ChoiceTermsVocabulary object at ...>
So that's it. Everything else is the same from then on.
Multi Widget
------------
A common use case in user interfaces is to ask the user to define one or more
items. The ``widget`` module provides a basic widget implementation to support
this use case.
The `MultiWidget` allows to store none, one or more values for a sequence
field. Don't get cinfused by the term sequence. Ther sequence used in
`SequenceWidget` means that the widget can choose from a sequence of values
which is a really a collection. The `MultiWidget` can collect values for build
and store a sequence of values like used in `ITuple` or `IList` field.
>>> request = TestRequest()
>>> multiWidget = widget.MultiWidget(request)
>>> multiWidget.name = 'multi.name'
>>> multiWidget.id = 'multi-id'
>>> multiWidget.value
[]
Let's define a field for our multi widget:
>>> multiField = zope.schema.List(
... value_type=zope.schema.Int(default=42))
>>> multiWidget.field = multiField
The value of a multi widget is always list. When extracting values from the
request, the values must be a list of valid values based on the value_type
field used from the used sequence field. The widget also uses a counter which
is required for processing the input from a request. The counter is a marker
for build the right amount of enumerated widgets.
If we provide no request we will get no value:
>>> multiWidget.extract()
<NO_VALUE>
If we provide an empty counter we will get no value:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'0'})
>>> multiWidget.extract()
<NO_VALUE>
If we provide real values within the request, we will get it back:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
... 'multi.name.0':u'42',
... 'multi.name.1':u'43'})
>>> multiWidget.extract()
[u'42', u'43']
If we provide a bad value we will get the bad value within the extract method.
Our widget update process will validate this bad value later:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'1',
... 'multi.name.0':u'bad'})
>>> multiWidget.extract()
[u'bad']
Storing a widget value forces to update the (sub) widgets. this forces also to
validate the (sub) widget values. For showing this we nee dot register a
validator:
>>> from z3c.form.validator import SimpleFieldValidator
>>> zope.component.provideAdapter(SimpleFieldValidator)
Since the value of the widget is a list of (widget) value items, when
displaying the values, they can be used as they are:
>>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
... 'multi.name.0':u'42',
... 'multi.name.1':u'43'})
>>> multiWidget.value = multiWidget.extract()
>>> multiWidget.value
[u'42', u'43']
Each widget normaly gets first processed by it's update method call after
intialization. This update call forces to call extract, which first will get
the right amount of (sub) widgets by the given counter value. Based on that
counter value the right amount of widgets will get created. Each widget will
return it's own value and this collected values get returned by the extract
method. The multi widget update method will then store this values if any given
as multi widget value argument. If extract doesn't return a value the multi
widget update method will use it's default value. If we store a given value
from the extract as multi widget value, this will force to setup the multi
widget widgets based on the given values and apply the right value for them.
After that the mutli widget is ready for rendering. The good thing about that
pattern is that it is possible to set a value before or after the update method
is called. At any time if we change the mutli widget value the (sub) widgets
get updated within the new relevant value.
>>> multiRequest = TestRequest(form={'multi.name.count':'2',
... 'multi.name.0':u'42',
... 'multi.name.1':u'43'})
>>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(
... multiRequest))
>>> multiWidget.name = 'multi.name'
>>> multiWidget.value
[]
>>> multiWidget.update()
>>> multiWidget.widgets[0].value
u'42'
>>> multiWidget.widgets[1].value
u'43'
>>> multiWidget.value
[u'42', u'43']
MultiWidget also declares the ``allowAdding`` and ``allowRemoving``
attributes that can be used in browser presentation to control add/remove
button availability. To ease working with common cases, the
``updateAllowAddRemove`` method provided that will set those attributes
in respect to field's min_length and max_length, if the field provides
zope.schema.interfaces.IMinMaxLen interface.
Let's define a field with min and max length constraints and create
a widget for it.
>>> multiField = zope.schema.List(
... value_type=zope.schema.Int(),
... min_length=2,
... max_length=5)
>>> request = TestRequest()
>>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(request))
Now, let's check if the function will do the right thing depending on
the value:
No value:
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(True, False)
Minimum length:
>>> multiWidget.value = [u'3', u'5']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(True, False)
Some allowed length:
>>> multiWidget.value = [u'3', u'5', u'8', u'6']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(True, True)
Maximum length:
>>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(False, True)
Over maximum length:
>>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42', u'45']
>>> multiWidget.updateAllowAddRemove()
>>> multiWidget.allowAdding, multiWidget.allowRemoving
(False, True)
I know a guy who once switched widget mode in the middle. All simple widgets
are easy to hack, but multiWidget needs to update all subwidgets:
>>> [w.mode for w in multiWidget.widgets]
['input', 'input', 'input', 'input', 'input', 'input']
Switch the multiWidget mode:
>>> multiWidget.mode = interfaces.DISPLAY_MODE
Yes, all subwigets switch mode:
>>> [w.mode for w in multiWidget.widgets]
['display', 'display', 'display', 'display', 'display', 'display']
Widget Events
-------------
Widget-system interaction can be very rich and wants to be extended in
unexpected ways. Thus there exists a generic widget event that can be used by
other code.
>>> event = widget.WidgetEvent(ageWidget)
>>> event
<WidgetEvent <Widget 'age'>>
These events provide the ``IWidgetEvent`` interface:
>>> interfaces.IWidgetEvent.providedBy(event)
True
There exists a special event that can be send out after a widget has been
updated, ...
>>> afterUpdate = widget.AfterWidgetUpdateEvent(ageWidget)
>>> afterUpdate
<AfterWidgetUpdateEvent <Widget 'age'>>
which provides another special interface:
>>> interfaces.IAfterWidgetUpdateEvent.providedBy(afterUpdate)
True
This event should be used by widget-managing components and is not created and
sent out internally by the widget's ``update()`` method. The event was
designed to provide an additional hook between updating the widget and
rendering it.
Cleanup
-------
Let's not leave temporary files lying around
>>> import os
>>> os.remove(textWidgetTemplate)
===============
Action Managers
===============
Action managers are components that manage all actions that can be taken
within a view, usually a form. They are also responsible for executing actions
when asked to do so.
Creating an action manager
--------------------------
An action manager is a form-related adapter that has the following
discriminator: form, request, and content. While there is a base
implementation for an action manager, the ``action`` module does not provide a
full implementation.
So we first have to build a simple implementation based on the ``Actions``
manager base class which allows us to add actions. Note that the following
implementation is for demonstration purposes. If you want to see a real action
manager implementation, then have a look at ``ButtonActions``. Let's now
implement our simple action manager:
>>> from z3c.form import action
>>> class SimpleActions(action.Actions):
... """Simple sample."""
...
... def append(self, name, action):
... """See z3c.form.interfaces.IActions."""
... if not name in self:
... self._data_keys.append(name)
... self._data_values.append(action)
... self._data[name] = action
Before we can initialise the action manager, we have to create instances for
our three discriminators, just enough to get it working:
>>> import zope.interface
>>> from z3c.form import interfaces
>>> class Form(object):
... zope.interface.implements(interfaces.IForm)
>>> form = Form()
>>> class Content(object):
... zope.interface.implements(zope.interface.Interface)
>>> content = Content()
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
We are now ready to create the action manager, which is a simple
triple-adapter:
>>> manager = SimpleActions(form, request, content)
>>> manager
<SimpleActions None>
As we can see in the manager representation above, the name of the manager is
``None``, since we have not specified one:
>>> manager.__name__ = 'example'
>>> manager
<SimpleActions 'example'>
Managing and Accessing Actions
------------------------------
Initially there are no actions in the manager:
>>> manager.keys()
[]
Our simple implementation of has an additional ``append()`` method, which we
will use to add actions:
>>> apply = action.Action(request, u'Apply')
>>> manager.append(apply.name, apply)
The action is added immediately:
>>> manager.keys()
['apply']
However, you should not rely on it being added, and always update the manager
once all actions were defined:
>>> manager.update()
Note: If the title of the action is a more complex unicode string and no name
is specified for the action, then a hexadecimal name is created from the
title:
>>> action.Action(request, u'Apply Now!').name
'4170706c79204e6f7721'
Since the action manager is an enumerable mapping, ...
>>> from zope.interface.common.mapping import IEnumerableMapping
>>> IEnumerableMapping.providedBy(manager)
True
there are several API methods available:
>>> manager['apply']
<Action 'apply' u'Apply'>
>>> manager['foo']
Traceback (most recent call last):
...
KeyError: 'foo'
>>> manager.get('apply')
<Action 'apply' u'Apply'>
>>> manager.get('foo', 'default')
'default'
>>> 'apply' in manager
True
>>> 'foo' in manager
False
>>> manager.values()
[<Action 'apply' u'Apply'>]
>>> manager.items()
[('apply', <Action 'apply' u'Apply'>)]
>>> len(manager)
1
Executing actions
-----------------
When an action is executed, an execution adapter is looked up. If there is no
adapter, nothing happens. So let's create a request that submits the apply
button:
>>> request = TestRequest(form={'apply': 'Apply'})
>>> manager = SimpleActions(form, request, content)
We also want to have two buttons in this case, so that we can ensure that only
one is executed:
>>> apply = action.Action(request, u'Apply')
>>> manager.append(apply.name, apply)
>>> cancel = action.Action(request, u'Cancel')
>>> manager.append(cancel.name, cancel)
>>> manager.update()
Now that the manager is updated, we can ask it for the "executed" actions:
>>> manager.executedActions
[<Action 'apply' u'Apply'>]
Executing the actions does nothing, because there are no handlers yet:
>>> manager.execute()
Let's now register an action handler that listens to the "Apply" action. An
action handler has four discriminators: form, request, content, and
action. All those objects are available to the handler under those names. When
using the base action handler from the ``action`` module, ``__call__()`` is
the only method that needs to be implemented:
>>> from z3c.form import util
>>> class SimpleActionHandler(action.ActionHandlerBase):
... zope.component.adapts(
... None, TestRequest, None, util.getSpecification(apply))
... def __call__(self):
... print 'successfully applied'
>>> zope.component.provideAdapter(SimpleActionHandler)
As you can see, we registered the action specifically for the apply
action. Now, executing the actions calls this handler:
>>> manager.execute()
successfully applied
Of course it only works for the "Apply" action and not ""Cancel":
>>> request = TestRequest(form={'cancel': 'Cancel'})
>>> manager.request = apply.request = cancel.request = request
>>> manager.execute()
Further, when a handler is successfully executed, an event is sent out, so
let's register an event handler:
>>> eventlog = []
>>> @zope.component.adapter(interfaces.IActionEvent)
... def handleEvent(event):
... eventlog.append(event)
>>> zope.component.provideHandler(handleEvent)
Let's now execute the "Apply" action again:
>>> request = TestRequest(form={'apply': 'Apply'})
>>> manager.request = apply.request = cancel.request = request
>>> manager.execute()
successfully applied
>>> eventlog[-1]
<ActionSuccessful for <Action 'apply' u'Apply'>>
Action handlers, however, can also raise action errors. These action errors
are caught and an event is created notifying the system of the problem. The
error is not further propagated. Other errors are not handled by the system to
avoid hiding real failures of the code.
Let's see how action errors can be used by implementing a handler for the
cancel action:
>>> class ErrorActionHandler(action.ActionHandlerBase):
... zope.component.adapts(
... None, TestRequest, None, util.getSpecification(cancel))
... def __call__(self):
... raise interfaces.ActionExecutionError(
... zope.interface.Invalid('Something went wrong'))
>>> zope.component.provideAdapter(ErrorActionHandler)
As you can see, the action execution error wraps some other execption, in this
case a simple invalid error.
Executing the "Cancel" action now produces the action error event:
>>> request = TestRequest(form={'cancel': 'Cancel'})
>>> manager.request = apply.request = cancel.request = request
>>> manager.execute()
>>> eventlog[-1]
<ActionErrorOccurred for <Action 'cancel' u'Cancel'>>
>>> eventlog[-1].error
<ActionExecutionError wrapping ...Invalid...>
========================
Attribute Value Adapters
========================
In advanced, highly customized projects it is often the case that a property
wants to be overridden for a particular customer in a particular case. A prime
example is the label of a widget. Until this implementation of a form
framework was written, widgets only could get their label from the field they
were representing. Thus, wanting to change the label of a widget meant
implementing a custom schema and re-registering the form in question for the
custom schema. It is needless to say that this was very annoying.
For this form framework, we are providing multiple levels of customization.
The user has the choice to change the value of an attribute through attribute
assignment or adapter lookup. The chronological order of an attribute value
assignment is as follows:
1. During initialization or right thereafter, the attribute value can be set
by direct attribute assignment, i.e. ``obj.attr = value``
2. While updating the object, an adapter is looked up for the attribute. If an
adapter is found, the attribute value will be overridden. Of course, if the
object does not have an ``update()`` method, one can choose another
location to do the adapter lookup.
3. After updating, the developer again has the choice to override the attribute
allowing granularity above and beyond the adapter.
The purpose of this module is to implement the availability of an attribute
value using an adapter.
>>> from z3c.form import value
The module provides helper functions and classes, to create those adapters
with as little code as possible.
Static Value Adapter
--------------------
To demonstrate the static value adapter, let's go back to our widget label
example. Let's create a couple of simple widgets and forms first:
>>> class TextWidget(object):
... label = u'Text'
>>> tw = TextWidget()
>>> class CheckboxWidget(object):
... label = u'Checkbox'
>>> cbw = CheckboxWidget()
>>> class Form1(object):
... pass
>>> form1 = Form1()
>>> class Form2(object):
... pass
>>> form2 = Form2()
We can now create a generic widget property adapter:
>>> WidgetAttribute = value.StaticValueCreator(
... discriminators = ('widget', 'view')
... )
Creating the widget attribute object, using the helper function above, allows
us to define the discriminators (or the granulatrity) that can be used to
control a widget attribute by an adapter. In our case this is the widget
itself and the form/view in which the widget is displayed. In other words, it
will be possible to register a widget attribute value specifically for a
particular widget, a particular form, or a combination thereof.
Let's now create a label attribute adapter for the text widget, since our
customer does not like the default label:
>>> TextLabel = WidgetAttribute(u'My Text', widget=TextWidget)
The first argument of any static attribute value is the value itself, in our
case the string "My Text". The following keyword arguments are the
discriminators specified in the property factory. Since we only specify the
widget, the label will be available to all widgets. But first we have to
register the adapter:
>>> import zope.component
>>> zope.component.provideAdapter(TextLabel, name='label')
The name of the adapter is the attribute name of the widget. Let's now see how
we can get the label:
>>> from z3c.form import interfaces
>>> staticValue = zope.component.getMultiAdapter(
... (tw, form1), interfaces.IValue, name='label')
>>> staticValue
<StaticValue u'My Text'>
The resulting value object has one public method ``get()``, which returns the
actual value:
>>> staticValue.get()
u'My Text'
As we said before, the value should be available to all forms, ...
>>> zope.component.getMultiAdapter(
... (tw, form2), interfaces.IValue, name='label')
<StaticValue u'My Text'>
... but only to the ``TextWidget``:
>>> zope.component.getMultiAdapter(
... (cbw, form2), interfaces.IValue, name='label')
Traceback (most recent call last):
...
ComponentLookupError: ((<CheckboxWidget...>, <Form2...>),
<InterfaceClass ...IValue>, 'label')
By the way, the attribute adapter factory notices, if you specify a
discriminator that was not specified:
>>> WidgetAttribute(u'My Text', form=Form2)
Traceback (most recent call last):
...
ValueError: One or more keyword arguments did not match the discriminators.
>>> WidgetAttribute.discriminators
('widget', 'view')
Computed Value Adapter
----------------------
A second implementation of the value adapter in the evaluated value, where one
can specify a function that computes the value to be returned. The only
argument to the function is the value adapter instance itself, which then
contains all the discriminators as specified when creating the generic widget
attribute factory. Let's take the same use case as before, but generating the
value as follows:
>>> def getLabelValue(adapter):
... return adapter.widget.label + ' (1)'
Now we create the value adapter for it:
>>> WidgetAttribute = value.ComputedValueCreator(
... discriminators = ('widget', 'view')
... )
>>> TextLabel = WidgetAttribute(getLabelValue, widget=TextWidget)
After registering the adapter, ...
>>> zope.component.provideAdapter(TextLabel, name='label')
we now get the answers:
>>> from z3c.form import interfaces
>>> zope.component.getMultiAdapter(
... (tw, form1), interfaces.IValue, name='label')
<ComputedValue u'Text (1)'>
__Note__: The two implementations of the attribute value adapters are not
meant to be canonical features that must always be used. The API is
kept simple to allow you to quickly implement your own value
adapter.
Automatic Interface Assignment
------------------------------
Oftentimes it is desirable to register an attribute value adapter for an
instance. A good example is a field, so let's create a small schema:
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... firstName = zope.schema.TextLine(title=u'First Name')
... lastName = zope.schema.TextLine(title=u'Last Name')
The customer now requires that the title -- which is the basis of the widget
label for field widgets -- of the last name should be "Surname". Until now the
option was to write a new schema changing the title. With this attribute value
module, as introduced thus far, we would need to provide a special interface
for the last name field, since registering a label adapter for all text fields
would also change the first name.
Before demonstrating the solution to this problem, let's first create a field
attribute value:
>>> FieldAttribute = value.StaticValueCreator(
... discriminators = ('field',)
... )
We can now create the last name title, changing only the title of the
``lastName`` field. Instead of passing in an interface of class as the field
discriminator, we pass in the field instance:
>>> LastNameTitle = FieldAttribute(u'Surname', field=IPerson['lastName'])
The attribute value factory will automatically detect instances, create an
interface on the fly, directly provide it on the field and makes it the
discriminator interface for the adapter registratioon.
So after registering the adapter, ...
>>> zope.component.provideAdapter(LastNameTitle, name='title')
the adapter is only available to the last name field and not the first name:
>>> zope.component.queryMultiAdapter(
... (IPerson['lastName'],), interfaces.IValue, name='title')
<StaticValue u'Surname'>
>>> zope.component.queryMultiAdapter(
... (IPerson['firstName'],), interfaces.IValue, name='title')
=============
Data Managers
=============
For the longest time the way widgets retrieved and stored their values on the
actual content/model was done by binding the field to a context and then
setting and getting the attribute from it. This has several distinct design
shortcomings:
1. The field has too much responsibility by knowing about its implementations.
2. There is no way of redefining the method used to store and access data
other than rewriting fields.
3. Finding the right content/model to modify is an implicit policy: Find an
adapter for the field's schema and then set the value there.
While implementing some real-world projects, we noticed that this approach is
too limiting and we often could not use the form framework when we wanted or
had to jump through many hoops to make it work for us. For example, if we want
to display a form to collect data that does not correspond to a set of content
components, we were forced to not only write a schema for the form, but also
implement that schema as a class. but all we wanted was a dictionary. For
edit-form like tasks we often also had an initial dictionary, which we just
wanted modified.
Data managers abstract the getting and setting of the data. A data manager is
responsible for setting one piece of data in a particular context.
>>> from z3c.form import datamanager
Attribute Field Manager
-----------------------
The most common case, of course, is the management of class attributes through
fields. In this case, the data manager needs to know about the context and the
field it is managing the data for.
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... name = zope.schema.TextLine(
... title=u'Name',
... default=u'<no name>')
... phone = zope.schema.TextLine(
... title=u'Phone')
>>> class Person(object):
... zope.interface.implements(IPerson)
... name = u''
... def __init__(self, name):
... self.name = name
>>> stephan = Person(u'Stephan Richter')
We can now instantiate the data manager for Stephan's name:
>>> nameDm = datamanager.AttributeField(stephan, IPerson['name'])
The data manager consists of a few simple methods to accomplish its
purpose. Getting the value is done using the ``get()`` or ``query()`` method:
>>> nameDm.get()
u'Stephan Richter'
>>> nameDm.query()
u'Stephan Richter'
The value can be set using ``set()``:
>>> nameDm.set(u'Stephan "Caveman" Richter')
>>> nameDm.get()
u'Stephan "Caveman" Richter'
>>> stephan.name
u'Stephan "Caveman" Richter'
If an attribute is not available, ``get()`` fails and ``query()`` returns a
default value:
>>> phoneDm = datamanager.AttributeField(stephan, IPerson['phone'])
>>> phoneDm.get()
Traceback (most recent call last):
...
AttributeError: 'Person' object has no attribute 'phone'
>>> phoneDm.query()
<NO_VALUE>
>>> phoneDm.query('nothing')
'nothing'
A final feature that is supported by the data manager is the check whether a
value can be accessed and written. When the context is not security proxied,
both, accessing and writing, is allowed:
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
True
To demonstrate the behavior for a security-proxied component, we first have to
provide security declarations for our person:
>>> from zope.security.management import endInteraction
>>> from zope.security.management import newInteraction
>>> from zope.security.management import setSecurityPolicy
>>> import z3c.form.testing
>>> endInteraction()
>>> newPolicy = z3c.form.testing.SimpleSecurityPolicy()
>>> newPolicy.allowedPermissions = ('View', 'Edit')
>>> oldpolicy = setSecurityPolicy(newPolicy)
>>> newInteraction()
>>> from zope.security.checker import Checker
>>> from zope.security.checker import defineChecker
>>> personChecker = Checker({'name':'View', 'name':'Edit'})
>>> defineChecker(Person, personChecker)
We now need to wrap stephan into a proxy:
>>> protectedStephan = zope.security.checker.ProxyFactory(stephan)
Since we are not logged in as anyone, we cannot acces or write the value:
>>> nameDm = datamanager.AttributeField(protectedStephan, IPerson['name'])
>>> nameDm.canAccess()
False
>>> nameDm.canWrite()
False
Clearly, this also means that ``get()`` and ``set()`` are also shut off:
>>> nameDm.get()
Traceback (most recent call last):
...
Unauthorized: (<Person object at ...>, 'name', 'Edit')
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
ForbiddenAttribute: ('name', <Person object at ...>)
Now we have to setup the security system and "log in" as a user:
>>> newPolicy.allowedPermissions = ('View', 'Edit')
>>> newPolicy.loggedIn = True
The created principal, with which we are logged in now, can only access the
attribute:
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
False
Thus only the ``get()`` method is allowed:
>>> nameDm.get()
u'Stephan "Caveman" Richter'
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
ForbiddenAttribute: ('name', <Person object at ...>)
If field's schema is not directly provided by the context, the datamanager
will attempt to find an adapter. Let's give the person an address for example:
>>> class IAddress(zope.interface.Interface):
... city = zope.schema.TextLine(title=u'City')
>>> class Address(object):
... zope.component.adapts(IPerson)
... zope.interface.implements(IAddress)
... def __init__(self, person):
... self.person = person
... @apply
... def city():
... def get(self):
... return getattr(self.person, '_city', None)
... def set(self, value):
... self.person._city = value
... return property(get, set)
>>> zope.component.provideAdapter(Address)
Now we can create a data manager for the city attribute:
>>> cityDm = datamanager.AttributeField(stephan, IAddress['city'])
We can access and write to the city attribute:
>>> cityDm.canAccess()
True
>>> cityDm.canWrite()
True
Initially there is no value, but of course we can create one:
>>> cityDm.get()
>>> cityDm.set(u'Maynard')
>>> cityDm.get()
u'Maynard'
The value can be accessed through the adapter itself as well:
>>> IAddress(stephan).city
u'Maynard'
While we think that implicitly looking up an adapter is not the cleanest
solution, it allows us to mimic the behavior of ``zope.formlib``. We think
that we will eventually provide alternative ways to accomplish the same in a
more explicit way.
If we try to set a value that is read-only, a type error is raised:
>>> readOnlyName = zope.schema.TextLine(
... __name__='name',
... readonly=True)
>>> nameDm = datamanager.AttributeField(stephan, readOnlyName)
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
TypeError: Can't set values on read-only fields
(name=name, class=__builtin__.Person)
Finally, we instantiate the data manager with a ``zope.schema``
field. And we can access the different methods like before.
>>> nameDm = datamanager.AttributeField(
... stephan, zope.schema.TextLine(__name__ = 'name'))
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
True
>>> nameDm.get()
u'Stephan "Caveman" Richter'
>>> nameDm.query()
u'Stephan "Caveman" Richter'
>>> nameDm.set(u'Stephan Richter')
>>> nameDm.get()
u'Stephan Richter'
Dictionary Field Manager
------------------------
Another implementation of the data manager interface is provided by the
dictionary field manager, which does not expect an instance with attributes as
its context, but a dictionary. It still uses a field to determine the key to
modify.
>>> personDict = {}
>>> nameDm = datamanager.DictionaryField(personDict, IPerson['name'])
The datamanager can really only deal with dictionaries and no other types:
>>> datamanager.DictionaryField([], IPerson['name'])
Traceback (most recent call last):
...
ValueError: Data are not a dictionary: <type 'list'>
Let's now access the name:
>>> nameDm.get()
Traceback (most recent call last):
...
KeyError: 'name'
>>> nameDm.query()
<NO_VALUE>
Initially we get the default value (as specified in the field), since the
person dictionariy has no entry. If no default value has been specified in the
field, the missing value is returned.
Now we set a value and it should be available:
>>> nameDm.set(u'Roger Ineichen')
>>> nameDm.get()
u'Roger Ineichen'
>>> personDict
{'name': u'Roger Ineichen'}
Since this dictionary is not security proxied, any field can be accessed and
written to:
>>> nameDm.canAccess()
True
>>> nameDm.canWrite()
True
As with the attribute data manager, readonly fields cannot be set:
>>> nameDm = datamanager.DictionaryField(personDict, readOnlyName)
>>> nameDm.set(u'Stephan')
Traceback (most recent call last):
...
TypeError: Can't set values on read-only fields name=name
Cleanup
-------
We clean up the changes we made in these examples:
>>> endInteraction()
>>> ignore = setSecurityPolicy(oldpolicy)
==============
Data Converter
==============
The data converter is the component that converts an internal data value as
described by a field to an external value as required by a widget and vice
versa. The goal of the converter is to avoid field and widget proliferation
solely to handle different types of values. The purpose of fields is to
describe internal data types and structures and that of widgets to provide one
particular mean of input.
The only two discriminators for the converter are the field and the widget.
Let's look at the ``Int`` field to ``TextWidget`` converter as an example:
>>> import zope.schema
>>> age = zope.schema.Int(
... __name__='age',
... title=u'Age',
... min=0)
>>> from z3c.form.testing import TestRequest
>>> from z3c.form import widget
>>> text = widget.Widget(TestRequest())
>>> from z3c.form import converter
>>> conv = converter.FieldDataConverter(age, text)
The field data converter is a generic data converter that can be used for all
fields that implement ``IFromUnicode``. If, for example, a ``Date`` field
-- which does not provide ``IFromUnicode`` -- is passed in, then a type error
is raised:
>>> converter.FieldDataConverter(zope.schema.Date(), text)
Traceback (most recent call last):
...
TypeError: Field of type ``Date`` must provide ``IFromUnicode``.
A named field will tell it's name:
>>> converter.FieldDataConverter(zope.schema.Date(__name__="foobar"), text)
Traceback (most recent call last):
...
TypeError: Field ``foobar`` of type ``Date`` must provide ``IFromUnicode``.
However, the ``FieldDataConverter`` is registered for ``IField``, since many
fields (like ``Decimal``) for which we want to create custom converters
provide ``IFromUnicode`` more specifically than their characterizing interface
(like ``IDecimal``).
The converter can now convert any integer to a the value the test widget deals
with, which is an ASCII string:
>>> conv.toWidgetValue(34)
u'34'
When the missing value is passed in, an empty string should be returned:
>>> conv.toWidgetValue(age.missing_value)
u''
Of course, values can also be converted from a widget value to field value:
>>> conv.toFieldValue('34')
34
An empty string means simply that the value is missing and the missing value
of the field is returned:
>>> age.missing_value = -1
>>> conv.toFieldValue('')
-1
Of course, trying to convert a non-integer string representation fails in a
conversion error:
>>> conv.toFieldValue('3.4')
Traceback (most recent call last):
...
ValueError: invalid literal for int(): 3.4
Also, the conversion to the field value also validates the data; in this case
negative values are not allowed:
>>> conv.toFieldValue('-34')
Traceback (most recent call last):
...
TooSmall: (-34, 0)
That's pretty much the entire API. When dealing with converters within the
component architecture, everything is a little bit simpler. So let's register
the converter:
>>> import zope.component
>>> zope.component.provideAdapter(converter.FieldDataConverter)
Once we ensure that our widget is a text widget, we can lookup the adapter:
>>> import zope.interface
>>> from z3c.form import interfaces
>>> zope.interface.alsoProvides(text, interfaces.ITextWidget)
>>> zope.component.getMultiAdapter((age, text), interfaces.IDataConverter)
<FieldDataConverter converts from Int to Widget>
For field-widgets there is a helper adapter that makes the lookup even
simpler:
>>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)
After converting our simple widget to a field widget,
>>> fieldtext = widget.FieldWidget(age, text)
we can now lookup the data converter adapter just by the field widget itself:
>>> interfaces.IDataConverter(fieldtext)
<FieldDataConverter converts from Int to Widget>
Number Data Converters
----------------------
As hinted on above, the package provides a specific data converter for each of
the three main numerical types: ``int``, ``float``, ``Decimal``. Specifically,
those data converters support full localization of the number formatting.
>>> age = zope.schema.Int()
>>> intdc = converter.IntegerDataConverter(age, text)
>>> intdc
<IntegerDataConverter converts from Int to Widget>
Since the age is so small, the formatting is trivial:
>>> intdc.toWidgetValue(34)
u'34'
But if we increase the number, the grouping seprator will be used:
>>> intdc.toWidgetValue(3400)
u'3,400'
An empty string is returned, if the missing value is passed in:
>>> intdc.toWidgetValue(None)
u''
Of course, parsing these outputs again, works as well:
>>> intdc.toFieldValue(u'34')
34
But if we increase the number, the grouping seprator will be used:
>>> intdc.toFieldValue(u'3,400')
3400
Luckily our parser is somewhat forgiving, and even allows for missing group
characters:
>>> intdc.toFieldValue(u'3400')
3400
If an empty string is passed in, the missing value of the field is returned:
>>> intdc.toFieldValue(u'')
Finally, if the input does not match at all, then a validation error is
returned:
>>> intdc.toFieldValue(u'fff')
Traceback (most recent call last):
...
FormatterValidationError:
(u'The entered value is not a valid integer literal.', u'fff')
The formatter validation error derives from the regular validation error, but
allows you to specify the message that is output when asked for the
documentation:
>>> err = converter.FormatterValidationError(u'Something went wrong.', None)
>>> err.doc()
u'Something went wrong.'
Let's now look at the float data converter.
>>> rating = zope.schema.Float()
>>> floatdc = converter.FloatDataConverter(rating, text)
>>> floatdc
<FloatDataConverter converts from Float to Widget>
Again, you can format and parse values:
>>> floatdc.toWidgetValue(7.43)
u'7.43'
>>> floatdc.toWidgetValue(10239.43)
u'10,239.43'
>>> floatdc.toFieldValue(u'7.43')
7.4299999999999997
>>> floatdc.toFieldValue(u'10,239.43')
10239.43
The error message, however, is customized to the floating point:
>>> floatdc.toFieldValue(u'fff')
Traceback (most recent call last):
...
FormatterValidationError:
(u'The entered value is not a valid decimal literal.', u'fff')
The decimal converter works like the other two before.
>>> money = zope.schema.Decimal()
>>> decimaldc = converter.DecimalDataConverter(money, text)
>>> decimaldc
<DecimalDataConverter converts from Decimal to Widget>
Formatting and parsing should work just fine:
>>> import decimal
>>> decimaldc.toWidgetValue(decimal.Decimal('7.43'))
u'7.43'
>>> decimaldc.toWidgetValue(decimal.Decimal('10239.43'))
u'10,239.43'
>>> decimaldc.toFieldValue(u'7.43')
Decimal("7.43")
>>> decimaldc.toFieldValue(u'10,239.43')
Decimal("10239.43")
Again, the error message, is customized to the floating point:
>>> floatdc.toFieldValue(u'fff')
Traceback (most recent call last):
...
FormatterValidationError:
(u'The entered value is not a valid decimal literal.', u'fff')
Date Data Converter
-------------------
Since the ``Date`` field does not provide ``IFromUnicode``, we have to provide
a custom data converter. This default one is not very sophisticated and is
inteded for use with the text widget:
>>> date = zope.schema.Date()
>>> ddc = converter.DateDataConverter(date, text)
>>> ddc
<DateDataConverter converts from Date to Widget>
Dates are simply converted to ISO format:
>>> import datetime
>>> bday = datetime.date(1980, 1, 25)
>>> ddc.toWidgetValue(bday)
u'80/01/25'
If the date is the missing value, an empty string is returned:
>>> ddc.toWidgetValue(None)
u''
The converter only knows how to convert this particular format back to a
datetime value:
>>> ddc.toFieldValue(u'80/01/25')
datetime.date(1980, 1, 25)
By default the converter converts missing input to missin_input value:
>>> ddc.toFieldValue(u'') is None
True
If the passed in string cannot be parsed, a formatter validation error is
raised:
>>> ddc.toFieldValue(u'8.6.07')
Traceback (most recent call last):
...
FormatterValidationError: ("The datetime string did not match the pattern
u'yy/MM/dd'.", u'8.6.07')
Time Data Converter
-------------------
Since the ``Time`` field does not provide ``IFromUnicode``, we have to provide
a custom data converter. This default one is not very sophisticated and is
inteded for use with the text widget:
>>> time = zope.schema.Time()
>>> tdc = converter.TimeDataConverter(time, text)
>>> tdc
<TimeDataConverter converts from Time to Widget>
Dates are simply converted to ISO format:
>>> noon = datetime.time(12, 0, 0)
>>> tdc.toWidgetValue(noon)
u'12:00'
The converter only knows how to convert this particular format back to a
datetime value:
>>> tdc.toFieldValue(u'12:00')
datetime.time(12, 0)
By default the converter converts missing input to missin_input value:
>>> tdc.toFieldValue(u'') is None
True
Datetime Data Converter
-----------------------
Since the ``Datetime`` field does not provide ``IFromUnicode``, we have to
provide a custom data converter. This default one is not very sophisticated
and is inteded for use with the text widget:
>>> dtField = zope.schema.Datetime()
>>> dtdc = converter.DatetimeDataConverter(dtField, text)
>>> dtdc
<DatetimeDataConverter converts from Datetime to Widget>
Dates are simply converted to ISO format:
>>> bdayNoon = datetime.datetime(1980, 1, 25, 12, 0, 0)
>>> dtdc.toWidgetValue(bdayNoon)
u'80/01/25 12:00'
The converter only knows how to convert this particular format back to a
datetime value:
>>> dtdc.toFieldValue(u'80/01/25 12:00')
datetime.datetime(1980, 1, 25, 12, 0)
By default the converter converts missing input to missin_input value:
>>> dtdc.toFieldValue(u'') is None
True
Timedelta Data Converter
------------------------
Since the ``Timedelta`` field does not provide ``IFromUnicode``, we have to
provide a custom data converter. This default one is not very sophisticated
and is inteded for use with the text widget:
>>> timedelta = zope.schema.Timedelta()
>>> tddc = converter.TimedeltaDataConverter(timedelta, text)
>>> tddc
<TimedeltaDataConverter converts from Timedelta to Widget>
Dates are simply converted to ISO format:
>>> allOnes = datetime.timedelta(1, 3600+60+1)
>>> tddc.toWidgetValue(allOnes)
u'1 day, 1:01:01'
The converter only knows how to convert this particular format back to a
datetime value:
>>> tddc.toFieldValue(u'1 day, 1:01:01')
datetime.timedelta(1, 3661)
By default the converter converts missing input to missin_input value:
>>> tddc.toFieldValue(u'') is None
True
File Upload Data Converter
--------------------------
Since the ``Bytes`` field can contain a ``FileUpload`` object, we have to make
sure we can convert ``FileUpload`` objects to bytes too.
>>> import z3c.form.browser.file
>>> fileWidget = z3c.form.browser.file.FileWidget(TestRequest())
>>> bytes = zope.schema.Bytes()
>>> fudc = converter.FileUploadDataConverter(bytes, fileWidget)
>>> fudc
<FileUploadDataConverter converts from Bytes to FileWidget>
The file upload widget usually provides a file object. But sometimes is also
provides a string:
>>> simple = 'foobar'
>>> fudc.toFieldValue(simple)
'foobar'
The converter can also convert ``FileUpload`` objects. So we need to setup a
fields storage stub ...
>>> class FieldStorageStub:
... def __init__(self, file):
... self.file = file
... self.headers = {}
... self.filename = 'foo.bar'
and a ``FileUpload`` component:
>>> import cStringIO
>>> from zope.publisher.browser import FileUpload
>>> myfile = cStringIO.StringIO('File upload contents.')
>>> aFieldStorage = FieldStorageStub(myfile)
>>> myUpload = FileUpload(aFieldStorage)
Let's try to convert the input now:
>>> fudc.toFieldValue(myUpload)
'File upload contents.'
By default the converter converts missing input to the ``NOT_CHANGED`` value:
>>> fudc.toFieldValue('')
<NOT_CHANGED>
This allows machinery later to ignore the field without sending all the data
around.
If we get an empty filename in a ``FileUpload`` obejct, we also get the
``missing_value``. But this really means that there was an error somewhere in
the upload, since you are normaly not able to upload a file without a filename:
>>> class EmptyFilenameFieldStorageStub:
... def __init__(self, file):
... self.file = file
... self.headers = {}
... self.filename = ''
>>> myfile = cStringIO.StringIO('')
>>> aFieldStorage = EmptyFilenameFieldStorageStub(myfile)
>>> myUpload = FileUpload(aFieldStorage)
>>> bytes = zope.schema.Bytes()
>>> fudc = converter.FileUploadDataConverter(bytes, fileWidget)
>>> fudc.toFieldValue(myUpload) is None
True
There is also a ``ValueError`` if we don't get a seekable file from the
``FieldStorage`` during the upload:
>>> myfile = ''
>>> aFieldStorage = FieldStorageStub(myfile)
>>> myUpload = FileUpload(aFieldStorage)
>>> bytes = zope.schema.Bytes()
>>> fudc = converter.FileUploadDataConverter(bytes, fileWidget)
>>> fudc.toFieldValue(myUpload) is None
Traceback (most recent call last):
...
ValueError: (u'Bytes data are not a file object', ...AttributeError...)
When converting to the widget value, not conversion should be done, since
bytes are not convertable in that sense.
>>> fudc.toWidgetValue('bytes')
'bytes'
When the file upload widget is not used and a text-based widget is desired,
then the regular field data converter will be chosen. Using a text widget,
however, must be setup manually in the form with code like this::
fields['bytesField'].widgetFactory = TextWidget
Sequence Data Converter
-----------------------
For widgets and fields that work with choices of a sequence, a special data
converter is required that works with terms. A prime example is a choice
field. Before we can use the converter, we have to register some adapters:
>>> from z3c.form import term
>>> import zc.sourcefactory.browser.source
>>> import zc.sourcefactory.browser.token
>>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
>>> zope.component.provideAdapter(term.ChoiceTermsSource)
>>> zope.component.provideAdapter(term.ChoiceTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.source.FactoredTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.token.fromInteger)
The choice fields can be used together with vocabularies and sources.
Using vocabulary
~~~~~~~~~~~~~~~~
Let's now create a choice field (using a vocabulary) and a widget:
>>> from zope.schema.vocabulary import SimpleVocabulary
>>> gender = zope.schema.Choice(
... vocabulary = SimpleVocabulary([
... SimpleVocabulary.createTerm(0, 'm', u'male'),
... SimpleVocabulary.createTerm(1, 'f', u'female'),
... ]) )
>>> from z3c.form import widget
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = gender
We now use the field and widget to instantiate the converter:
>>> sdv = converter.SequenceDataConverter(gender, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> sdv.toWidgetValue(0)
['m']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> sdv.toFieldValue(['m'])
0
Sometimes a field is not required. In those cases, the internal value is the
missing value of the field. The converter interprets that as no value being
selected:
>>> gender.missing_value = 'missing'
>>> sdv.toWidgetValue(gender.missing_value)
[]
If the internal value is not a valid item in the terms, it is treated as
missing:
>>> sdv.toWidgetValue(object())
[]
If "no value" has been specified in the widget, the missing value
of the field is returned:
>>> sdv.toFieldValue([u'--NOVALUE--'])
'missing'
An empty list will also cause the missing value to be returned:
>>> sdv.toFieldValue([])
'missing'
Using source
~~~~~~~~~~~~
Let's now create a choice field (using a source) and a widget:
>>> from zc.sourcefactory.basic import BasicSourceFactory
>>> class GenderSourceFactory(BasicSourceFactory):
... _mapping = {0: u'male', 1: u'female'}
... def getValues(self):
... return self._mapping.keys()
... def getTitle(self, value):
... return self._mapping[value]
>>> gender_source = zope.schema.Choice(
... source = GenderSourceFactory())
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = gender_source
We now use the field and widget to instantiate the converter:
>>> sdv = converter.SequenceDataConverter(gender, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> sdv.toWidgetValue(0)
['0']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> sdv.toFieldValue(['0'])
0
Sometimes a field is not required. In those cases, the internalvalue is the
missing value of the field. The converter interprets that as no value being
selected:
>>> gender.missing_value = 'missing'
>>> sdv.toWidgetValue(gender.missing_value)
[]
If "no value" has been specified in the widget, the missing value
of the field is returned:
>>> sdv.toFieldValue([u'--NOVALUE--'])
'missing'
An empty list will also cause the missing value to be returned:
>>> sdv.toFieldValue([])
'missing'
Collection Sequence Data Converter
----------------------------------
For widgets and fields that work with a sequence of choices, another data
converter is required that works with terms. A prime example is a list
field. Before we can use the converter, we have to register the terms adapters:
>>> from z3c.form import term
>>> zope.component.provideAdapter(term.CollectionTerms)
>>> zope.component.provideAdapter(term.CollectionTermsVocabulary)
>>> zope.component.provideAdapter(term.CollectionTermsSource)
Collections can also use either vocabularies or sources.
Using vocabulary
~~~~~~~~~~~~~~~~
Let's now create a list field (using the previously defined field using
a vocabulary) and a widget:
>>> genders = zope.schema.List(value_type=gender)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
We now use the field and widget to instantiate the converter:
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> csdv.toWidgetValue([0])
['m']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> csdv.toFieldValue(['m'])
[0]
Of course, a collection field can also have multiple values:
>>> csdv.toWidgetValue([0, 1])
['m', 'f']
>>> csdv.toFieldValue(['m', 'f'])
[0, 1]
If any of the values are not a valid choice, they are simply ignored:
>>> csdv.toWidgetValue([0, 3])
['m']
Sometimes a field is not required. In those cases, the internal value is the
missing value of the field. The converter interprets that as no values being
given:
>>> genders.missing_value is None
True
>>> csdv.toWidgetValue(genders.missing_value)
[]
For some field, like the ``Set``, the collection type is a tuple. Sigh. In
these cases we use the last entry in the tuple as the type to use:
>>> genders = zope.schema.Set(value_type=gender)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
>>> csdv.toWidgetValue(set([0]))
['m']
>>> csdv.toFieldValue(['m'])
set([0])
Getting Terms
+++++++++++++
As an optimization of this converter, the converter actually does not look up
the terms itself but uses the widget's ``terms`` attribute. If the terms are
not yet retrieved, the converter will ask the widget to do so when in need.
So let's see how this works when getting the widget value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
>>> csdv.toWidgetValue([0])
['m']
>>> seqWidget.terms
<z3c.form.term.CollectionTermsVocabulary object ...>
The same is true when getting the field value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
>>> csdv.toFieldValue(['m'])
set([0])
>>> seqWidget.terms
<z3c.form.term.CollectionTermsVocabulary object ...>
Corner case: Just in case the field has a sequence as ``_type``:
>>> class myField(zope.schema.List):
... _type = (list, tuple)
>>> genders = myField(value_type=gender)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders
We now use the field and widget to instantiate the converter:
>>> csdv = converter.CollectionSequenceDataConverter(genders, seqWidget)
The converter uses the latter type (tuple) to convert:
>>> csdv.toFieldValue(['m'])
(0,)
Using source
~~~~~~~~~~~~
Let's now create a list field (using the previously defined field using
a source) and a widget:
>>> genders_source = zope.schema.List(value_type=gender_source)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
We now use the field and widget to instantiate the converter:
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
We can now convert a real value to a widget value, which will be the term's
token:
>>> csdv.toWidgetValue([0])
['0']
The result is always a sequence, since sequence widgets only deal collections
of values. Of course, we can convert the widget value back to an internal
value:
>>> csdv.toFieldValue(['0'])
[0]
For some field, like the ``Set``, the collection type is a tuple. Sigh. In
these cases we use the last entry in the tuple as the type to use:
>>> genders_source = zope.schema.Set(value_type=gender_source)
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
>>> csdv.toWidgetValue(set([0]))
['0']
>>> csdv.toFieldValue(['0'])
set([0])
Getting Terms
+++++++++++++
As an optimization of this converter, the converter actually does not look up
the terms itself but uses the widget's ``terms`` attribute. If the terms are
not yet retrieved, the converter will ask the widget to do so when in need.
So let's see how this works when getting the widget value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
>>> csdv.toWidgetValue([0])
['0']
>>> seqWidget.terms
<z3c.form.term.CollectionTermsSource object ...>
The same is true when getting the field value:
>>> seqWidget = widget.SequenceWidget(TestRequest())
>>> seqWidget.field = genders_source
>>> seqWidget.terms
>>> csdv = converter.CollectionSequenceDataConverter(
... genders_source, seqWidget)
>>> csdv.toFieldValue(['0'])
set([0])
>>> seqWidget.terms
<z3c.form.term.CollectionTermsSource object ...>
Boolean to Single Checkbox Data Converter
-----------------------------------------
The conversion from any field to the single checkbox widget value is a special
case, because it has to be defined what selecting the value means. In the case
of the boolean field, "selected" means ``True`` and if unselected, ``False``
is returned:
>>> boolField = zope.schema.Bool()
>>> bscbx = converter.BoolSingleCheckboxDataConverter(boolField, seqWidget)
>>> bscbx
<BoolSingleCheckboxDataConverter converts from Bool to SequenceWidget>
Let's now convert boolean field to widget values:
>>> bscbx.toWidgetValue(True)
['selected']
>>> bscbx.toWidgetValue(False)
[]
Converting back is equally simple:
>>> bscbx.toFieldValue(['selected'])
True
>>> bscbx.toFieldValue([])
False
Note that this widget has no concept of missing value, since it can only
represent two states by desgin.
Text Lines Data Converter
-------------------------
For sequence widgets and fields that work with a sequence of `TextLine` value
fields, a simple data converter is required. Let's create a list of text lines
field and a widget:
>>> languages = zope.schema.List(
... value_type=zope.schema.TextLine(),
... default=[],
... missing_value=None,
... )
>>> from z3c.form.browser import textlines
>>> tlWidget = textlines.TextLinesWidget(TestRequest())
>>> tlWidget.field = languages
We now use the field and widget to instantiate the converter:
>>> tlc = converter.TextLinesConverter(languages, tlWidget)
We can now convert a real value to a widget value:
>>> tlc.toWidgetValue([u'de', u'fr', u'en'])
u'de\nfr\nen'
The result is always a string, since text lines widgets only deal with textarea
as input field. Of course, we can convert the widget value back to an internal
value:
>>> tlc.toFieldValue('de\nfr\nen')
[u'de', u'fr', u'en']
Each line should be one item:
>>> tlc.toFieldValue('this morning\ntomorrow evening\nyesterday')
[u'this morning', u'tomorrow evening', u'yesterday']
An empty string will also cause the missing value to be returned:
>>> tlc.toFieldValue('') is None
True
It also should work for schema fields that define their type as tuple,
for instance zope.schema.Int declares its type as (int, long).
>>> ids = zope.schema.List(
... value_type=zope.schema.Int(),
... )
Let's illustrate the problem:
>>> zope.schema.Int._type
(<type 'int'>, <type 'long'>)
The converter will use the first one.
>>> tlWidget.field = ids
>>> tlc = converter.TextLinesConverter(ids, tlWidget)
Of course, it still can convert to the widget value:
>>> tlc.toWidgetValue([1,2,3])
u'1\n2\n3'
And back:
>>> tlc.toFieldValue(u'1\n2\n3\n')
[1, 2, 3]
An empty string will also cause the missing value to be returned:
>>> tlc.toFieldValue('') is None
True
Converting Missing value to Widget value returns '':
>>> tlc.toWidgetValue(tlc.field.missing_value)
u''
Just in case the field has sequence as its ``_type``:
>>> class myField(zope.schema.List):
... _type = (list, tuple)
>>> ids = myField(
... value_type=zope.schema.Int(),
... )
The converter will use the latter one.
>>> tlWidget.field = ids
>>> tlc = converter.TextLinesConverter(ids, tlWidget)
Of course, it still can convert to the widget value:
>>> tlc.toWidgetValue([1,2,3])
u'1\n2\n3'
And back:
>>> tlc.toFieldValue(u'1\n2\n3\n')
(1, 2, 3)
Multi Data Converter
--------------------
For multi widgets and fields that work with a sequence of other basic types, a
separate data converter is required. Let's create a list of integers field and
a widget:
>>> numbers = zope.schema.List(
... value_type=zope.schema.Int(),
... default=[],
... missing_value=None,
... )
>>> from z3c.form.browser import multi
>>> multiWidget = multi.MultiWidget(TestRequest())
>>> multiWidget.field = numbers
Before we can convert, we have to regsiter a widget for the integer field:
>>> from z3c.form.browser import text
>>> zope.component.provideAdapter(
... text.TextFieldWidget,
... (zope.schema.Int, TestRequest))
We now use the field and widget to instantiate the converter:
>>> conv = converter.MultiConverter(numbers, multiWidget)
We can now convert a list of integers to the multi-widget internal
representation:
>>> conv.toWidgetValue([1, 2, 3])
[u'1', u'2', u'3']
If the value is the missing value, an empty list is returned:
>>> conv.toWidgetValue(None)
[]
Now, let's look at the reverse:
>>> conv.toFieldValue([u'1', u'2', u'3'])
[1, 2, 3]
If the list is empty, the missing value is returned:
>>> conv.toFieldValue([]) is None
True
=====
Terms
=====
Terms are used to provide choices for sequence widgets or any other construct
needing them. Since Zope 3 already has sources and vocabularies, the base
terms class simply builds on them.
Vocabularies
------------
Thus, let's create a vocabulary first:
>>> from zope.schema import vocabulary
>>> ratings = vocabulary.SimpleVocabulary([
... vocabulary.SimpleVocabulary.createTerm(0, '0', u'bad'),
... vocabulary.SimpleVocabulary.createTerm(1, '1', u'okay'),
... vocabulary.SimpleVocabulary.createTerm(2, '2', u'good')
... ])
Terms
~~~~~
Now we can create the terms object:
>>> from z3c.form import term
>>> terms = term.Terms()
>>> terms.terms = ratings
Getting a term from a given value is simple:
>>> terms.getTerm(0).title
u'bad'
>>> terms.getTerm(3)
Traceback (most recent call last):
...
LookupError: 3
When converting values from their Web representation back to the internal
representation, we have to be able to look up a term by its token:
>>> terms.getTermByToken('0').title
u'bad'
>>> terms.getTerm('3')
Traceback (most recent call last):
...
LookupError: 3
However, often we just want the value so asking for the value that is
represented by a token saves usually one line of code:
>>> terms.getValue('0')
0
>>> terms.getValue('3')
Traceback (most recent call last):
...
LookupError: 3
You can also iterate through all terms:
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Or ask how many terms you have in the first place:
>>> len(terms)
3
Finally the API allows you to check whether a particular value is available in
the terms:
>>> 0 in terms
True
>>> 3 in terms
False
Now, there are several terms implementations that were designed for particular
fields. Within the framework, terms are used as adapters with the follwoing
discriminators: context, request, form, field, vocabulary/source and widget.
Choice field
~~~~~~~~~~~~
The first terms implementation is for ``Choice`` fields. Choice fields
unfortunately can have a vocabulary and a source which behave differently.
Let's have a look a the vocabulary first:
>>> import zope.component
>>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
>>> import z3c.form.testing
>>> request = z3c.form.testing.TestRequest()
>>> import z3c.form.widget
>>> widget = z3c.form.widget.Widget(request)
>>> import zope.schema
>>> ratingField = zope.schema.Choice(
... title=u'Rating',
... vocabulary=ratings)
>>> terms = term.ChoiceTerms(
... None, request, None, ratingField, widget)
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Sometimes choice fields only specify a vocabulary name and the actual
vocabulary is looked up at run time.
>>> ratingField2 = zope.schema.Choice(
... title=u'Rating',
... vocabulary='Ratings')
Initially we get an error because the "Ratings" vocabulary is not defined:
>>> terms = term.ChoiceTerms(
... None, request, None, ratingField2, widget)
Traceback (most recent call last):
...
VocabularyRegistryError: unknown vocabulary: 'Ratings'
Let's now register the vocabulary under this name:
>>> def RatingsVocabulary(obj):
... return ratings
>>> from zope.schema import vocabulary
>>> vr = vocabulary.getVocabularyRegistry()
>>> vr.register('Ratings', RatingsVocabulary)
We should now be able to get all terms as before:
>>> terms = term.ChoiceTerms(
... None, request, None, ratingField, widget)
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Bool fields
+++++++++++
A similar terms implementation exists for a ``Bool`` field:
>>> truthField = zope.schema.Bool()
>>> terms = term.BoolTerms(None, None, None, truthField, None)
>>> [entry.title for entry in terms]
[u'yes', u'no']
In case you don't like the choice of 'yes' and 'no' for the labels, we
can subclass the ``BoolTerms`` class to control the display labels.
>>> class MyBoolTerms(term.BoolTerms):
... trueLabel = u'True'
... falseLabel = u'False'
>>> terms = MyBoolTerms(None, None, None, truthField, None)
>>> [entry.title for entry in terms]
[u'True', u'False']
Collections
+++++++++++
Finally, there are a terms adapters for all collections. But we have to
register some adapters before using it:
>>> from z3c.form import term
>>> zope.component.provideAdapter(term.CollectionTerms)
>>> zope.component.provideAdapter(term.CollectionTermsVocabulary)
>>> zope.component.provideAdapter(term.CollectionTermsSource)
>>> ratingsField = zope.schema.List(
... title=u'Ratings',
... value_type=ratingField)
>>> terms = term.CollectionTerms(
... None, request, None, ratingsField, widget)
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
Sources
-------
Basic sources
~~~~~~~~~~~~~
Basic sources need no context to compute their value. Let's create a
source first:
>>> from zc.sourcefactory.basic import BasicSourceFactory
>>> class RatingSourceFactory(BasicSourceFactory):
... _mapping = {10: u'ugly', 20: u'nice', 30: u'great'}
... def getValues(self):
... return self._mapping.keys()
... def getTitle(self, value):
... return self._mapping[value]
As we did not include the configure.zcml of zc.sourcefactory we have
to register some required adapters manually. We also need the
ChoiceTermsSource adapter:
>>> import zope.component
>>> import zc.sourcefactory.browser.source
>>> import zc.sourcefactory.browser.token
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.source.FactoredTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.token.fromInteger)
>>> zope.component.provideAdapter(term.ChoiceTermsSource)
Choice fields
+++++++++++++
Sources can be used with ``Choice`` fields like vocabularies. First
we create a field based on the source:
>>> sourceRatingField = zope.schema.Choice(
... title=u'Sourced Rating',
... source=RatingSourceFactory())
We connect the field to a widget to see the ITerms adapter for sources
at work:
>>> terms = term.ChoiceTerms(
... None, request, None, sourceRatingField, widget)
Iterating over the terms adapter returnes the term objects:
>>> [entry for entry in terms]
[<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>]
>>> len(terms)
3
>>> [entry.token for entry in terms]
['10', '20', '30']
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
Using a token it is possible to look up the term and the value:
>>> terms.getTermByToken('20').title
u'nice'
>>> terms.getValue('30')
30
With can test if a value is in the source:
>>> 30 in terms
True
>>> 25 in terms
False
Collections
+++++++++++
Finally, there are terms adapters for all collections:
>>> sourceRatingsField = zope.schema.List(
... title=u'Sourced Ratings',
... value_type=sourceRatingField)
>>> terms = term.CollectionTerms(
... None, request, None, sourceRatingsField, widget)
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
Contextual sources
~~~~~~~~~~~~~~~~~~
Contextual sources depend on the context they are called on. Let's
create a context and a contextual source:
>>> from zc.sourcefactory.contextual import BasicContextualSourceFactory
>>> class RatingContext(object):
... base_value = 10
>>> class ContextualRatingSourceFactory(BasicContextualSourceFactory):
... _mapping = {10: u'ugly', 20: u'nice', 30: u'great'}
... def getValues(self, context):
... return [context.base_value + x for x in self._mapping.keys()]
... def getTitle(self, context, value):
... return self._mapping[value - context.base_value]
As we did not include the configure.zcml of zc.sourcefactory we have
to register some required adapters manually. We also need the
ChoiceTermsSource adapter:
>>> import zope.component
>>> import zc.sourcefactory.browser.source
>>> import zc.sourcefactory.browser.token
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.source.FactoredContextualTerms)
>>> zope.component.provideAdapter(
... zc.sourcefactory.browser.token.fromInteger)
>>> zope.component.provideAdapter(term.ChoiceTermsSource)
Choice fields
+++++++++++++
Contextual sources can be used with ``Choice`` fields like
vocabularies. First we create a field based on the source:
>>> contextualSourceRatingField = zope.schema.Choice(
... title=u'Context Sourced Rating',
... source=ContextualRatingSourceFactory())
We create an context object and connect the field to a widget to see
the ITerms adapter for sources at work:
>>> rating_context = RatingContext()
>>> rating_context.base_value = 100
>>> terms = term.ChoiceTerms(
... rating_context, request, None, contextualSourceRatingField, widget)
Iterating over the terms adapter returnes the term objects:
>>> [entry for entry in terms]
[<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>,
<zc.sourcefactory.browser.source.FactoredTerm object at 0x...>]
>>> len(terms)
3
>>> [entry.token for entry in terms]
['110', '120', '130']
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
Using a token, it is possible to look up the term and the value:
>>> terms.getTermByToken('120').title
u'nice'
>>> terms.getValue('130')
130
With can test if a value is in the source:
>>> 130 in terms
True
>>> 125 in terms
False
Collections
+++++++++++
Finally, there are terms adapters for all collections:
>>> contextualSourceRatingsField = zope.schema.List(
... title=u'Contextual Sourced Ratings',
... value_type=contextualSourceRatingField)
>>> terms = term.CollectionTerms(
... rating_context, request, None, contextualSourceRatingsField, widget)
>>> [entry.title for entry in terms]
[u'ugly', u'nice', u'great']
=============================
Utility Functions and Classes
=============================
This file documents the utility functions and classes that are otherwise not
tested.
>>> from z3c.form import util
``creeateId(name)`` Function
----------------------------
This function converts an arbitrary unicode string into a valid Python
identifier. If the name is a valid identifier, then it is just returned, but
all upper case letters are lowered:
>>> util.createId(u'Change')
'change'
>>> util.createId(u'Change_2')
'change_2'
If a name is not a valid identifier, a hex code of the string is created:
>>> util.createId(u'Change 3')
'4368616e67652033'
The function can also handle non-ASCII characters:
>>> util.createId(u'&#196;ndern')
'c383c2846e6465726e'
``createCSSId(name)`` Function
------------------------------
This function takes any unicode name and coverts it into an id that
can be easily referenced by CSS selectors. Characters that are in the
ascii alphabet, are numbers, or are '-' or '_' will be left the same.
All other characters will be converted to ordinal numbers:
>>> util.createCSSId(u'NormalId')
'NormalId'
>>> util.createCSSId(u'&#1593;&#1614;&#1585;&#1614;')
'c398c2b9c399c28ec398c2b1c399c28e'
>>> util.createCSSId(u'This has spaces')
'This20has20spaces'
``getWidgetById(form, id)`` Function
------------------------------------
Given a form and a widget id, this function extracts the widget for you. First
we need to create a properly developed form:
>>> import zope.interface
>>> import zope.schema
>>> class IPerson(zope.interface.Interface):
... name = zope.schema.TextLine(title=u'Name')
>>> from z3c.form import form, field
>>> class AddPerson(form.AddForm):
... fields = field.Fields(IPerson)
>>> from z3c.form import testing
>>> testing.setupFormDefaults()
>>> addPerson = AddPerson(None, testing.TestRequest())
>>> addPerson.update()
We can now ask for the widget:
>>> util.getWidgetById(addPerson, 'form-widgets-name')
<TextWidget 'form.widgets.name'>
The widget id can be split into a prefix and a widget name. The id must always
start with the correct prefix, otherwise a value error is raised:
>>> util.getWidgetById(addPerson, 'myform-widgets-name')
Traceback (most recent call last):
...
ValueError: Name 'myform.widgets.name' must start with prefix 'form.widgets.'
If the widget is not found but the prefix is correct, ``None`` is returned:
>>> util.getWidgetById(addPerson, 'form-widgets-myname') is None
True
``extractFileName(form, id, cleanup=True, allowEmptyPostfix=False)`` Function
-----------------------------------------------------------------------------
Test the filename extraction method:
>>> class IDocument(zope.interface.Interface):
... data = zope.schema.Bytes(title=u'Data')
Define a widgets stub and a upload widget stub class and setup them as a
faked form:
>>> class FileUploadWidgetStub(object):
... def __init__(self):
... self.filename = None
>>> class WidgetsStub(object):
... def __init__(self):
... self.data = FileUploadWidgetStub()
... self.prefix = 'widgets.'
... def get(self, name, default):
... return self.data
>>> class FileUploadFormStub(form.AddForm):
... def __init__(self):
... self.widgets = WidgetsStub()
...
... def setFakeFileName(self, filename):
... self.widgets.data.filename = filename
Now we can setup the stub form. Note this form is just a fake it's not a real
implementation. We just provide a form like class which simulates the
FileUpload object in the a widget. See z3c.form.browser.file.txt for a real
file upload test uscase:
>>> uploadForm = FileUploadFormStub()
>>> uploadForm.setFakeFileName('foo.txt')
And extract the filename
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
'foo.txt'
Test a unicode filename:
>>> uploadForm.setFakeFileName(u'foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test a windows IE uploaded filename:
>>> uploadForm.setFakeFileName(u'D:\\some\\folder\\foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test another filename:
>>> uploadForm.setFakeFileName(u'D:/some/folder/foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test another filename:
>>> uploadForm.setFakeFileName(u'/tmp/folder/foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.txt'
Test special characters in filename, e.g. dots:
>>> uploadForm.setFakeFileName(u'/tmp/foo.bar.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo.bar.txt'
Test some other special characters in filename:
>>> uploadForm.setFakeFileName(u'/tmp/foo-bar.v.0.1.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo-bar.v.0.1.txt'
Test special characters in file path of filename:
>>> uploadForm.setFakeFileName(u'/tmp-v.1.0/foo-bar.v.0.1.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
u'foo-bar.v.0.1.txt'
Test optional keyword arguments. But remember it's hard for Zope to guess the
content type for filenames without extensions:
>>> uploadForm.setFakeFileName(u'minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True,
... allowEmptyPostfix=True)
u'minimal'
>>> uploadForm.setFakeFileName(u'/tmp/minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True,
... allowEmptyPostfix=True)
u'minimal'
>>> uploadForm.setFakeFileName(u'D:\\some\\folder\\minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True,
... allowEmptyPostfix=True)
u'minimal'
There will be a ValueError if we get a empty filename by default:
>>> uploadForm.setFakeFileName(u'/tmp/minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=True)
Traceback (most recent call last):
...
ValueError: Missing filename extension.
We also can skip removing a path from a upload. Note only IE will upload a
path in a upload ``<input type="file" ...>`` field:
>>> uploadForm.setFakeFileName(u'/tmp/foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
u'/tmp/foo.txt'
>>> uploadForm.setFakeFileName(u'/tmp-v.1.0/foo-bar.v.0.1.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
u'/tmp-v.1.0/foo-bar.v.0.1.txt'
>>> uploadForm.setFakeFileName(u'D:\\some\\folder\\foo.txt')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
u'D:\\some\\folder\\foo.txt'
And missing filename extensions are also not allowed by deafault if we skip
the filename:
>>> uploadForm.setFakeFileName(u'/tmp/minimal')
>>> util.extractFileName(uploadForm, 'form.widgets.data', cleanup=False)
Traceback (most recent call last):
...
ValueError: Missing filename extension.
``extractContentType(form, id)`` Function
-----------------------------------------
There is also a method which is able to extract the content type for a given
file upload. We can use the stub form from the previous test.
>>> uploadForm = FileUploadFormStub()
>>> uploadForm.setFakeFileName('foo.txt')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'text/plain'
>>> uploadForm.setFakeFileName('foo.gif')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/gif'
>>> uploadForm.setFakeFileName('foo.jpg')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/jpeg'
>>> uploadForm.setFakeFileName('foo.png')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/png'
>>> uploadForm.setFakeFileName('foo.tif')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'image/tiff'
>>> uploadForm.setFakeFileName('foo.doc')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'application/msword'
>>> uploadForm.setFakeFileName('foo.zip')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'application/zip'
>>> uploadForm.setFakeFileName('foo.unknown')
>>> util.extractContentType(uploadForm, 'form.widgets.data')
'text/x-unknown-content-type'
`UniqueOrderedKeys` object
--------------------------
This object manages the keys of a dictionary. It ensures that no values are
added multiple times and retains the original order of the keys.
>>> keys = util.UniqueOrderedKeys([1, 2])
Let's now add another key:
>>> keys.append(3)
>>> keys.data
[1, 2, 3]
Trying to add `3` again causes a `ValueError` exception:
>>> keys.append(3)
Traceback (most recent call last):
...
ValueError: 3
`Manager` object
----------------
The manager object is a base class of a mapping object that keeps track of the
key order as they are added.
>>> manager = util.Manager()
Initially the manager is empty:
>>> len(manager)
0
Since this base class mainly defines a read-interface, we have to add the
values manually:
>>> manager._data_values.append(2)
>>> manager._data_keys.append('b')
>>> manager._data['b'] = 2
>>> manager._data_values.append(1)
>>> manager._data_keys.append('a')
>>> manager._data['a'] = 1
Let's iterate through the manager:
>>> tuple(iter(manager))
('b', 'a')
>>> manager.keys()
['b', 'a']
>>> manager.values()
[2, 1]
>>> manager.items()
[('b', 2), ('a', 1)]
Let's ow look at item access:
>>> 'b' in manager
True
>>> manager.get('b')
2
>>> manager.get('c', 'None')
'None'
It also supports deletion:
>>> del manager['b']
>>> manager.items()
[('a', 1)]
When the `_data_keys` is reset it will always produce a `UniqueOrderedKeys`:
>>> manager._data_keys = []
>>> manager._data_keys
<z3c.form.util.UniqueOrderedKeys ...>
>>> manager._data_keys = util.UniqueOrderedKeys()
>>> manager._data_keys
<z3c.form.util.UniqueOrderedKeys ...>
`SelectionManager` object
-------------------------
The selection manager is an extension to the manager and provides a few more
API functions. Unfortunately, this base class is totally useless without a
sensible constructor:
>>> import zope.interface
>>> class MySelectionManager(util.SelectionManager):
... managerInterface = zope.interface.Interface
...
... def __init__(self, *args):
... super(MySelectionManager, self).__init__()
... args = list(args)
... for arg in args:
... if isinstance(arg, MySelectionManager):
... args += arg.values()
... continue
... self._data_values.append(arg)
... self._data_keys.append(str(arg))
... self._data[str(arg)] = arg
Let's now create two managers:
>>> manager1 = MySelectionManager(1, 2)
>>> manager2 = MySelectionManager(3, 4)
You can add two managers:
>>> manager = manager1 + manager2
>>> manager.values()
[1, 2, 3, 4]
Next, you can select only certain names:
>>> manager.select('1', '2', '3').values()
[1, 2, 3]
Or simply omit a value.
>>> manager.omit('2').values()
[1, 3, 4]
You can also easily copy a manager:
>>> manager.copy() is not manager
True
That's all.
=======
CHANGES
=======
Version 2.0.0 (2009-06-14)
--------------------------
Features
~~~~~~~~
- KGS 3.4 compatibility. This is a real hard thing, because `z3c.form` tests
use `lxml` >= 2.1.1 to check test output, but KGS 3.4 has `lxml`
1.3.6. Therefore we agree on that if tests pass with all package versions
nailed by KGS 3.4 but `lxml` overridden to 2.1.1 then the `z3c.form` package
works with a plain KGS 3.4.
- Removed hard `z3c.ptcompat` and thus `z3c.pt` dependency. If you have
`z3c.ptcompat` on the Python path it will be used.
- Added nested group support. Groups are rendered as fieldsets. Nested
fieldsets are very useful when designing forms.
WARNING: If your group did have an `applyChanges()` (or any added(?)) method
the new one added by this change might not match the signature.
- Added `labelRequired` and `requiredInfo` form attributes. This is useful for
conditional rendering a required info legend in form templates. The
`requiredInfo` label depends by default on a given `labelRequired` message
id and will only return the label if at least one widget field is required.
- Add support for refreshing actions after their execution. This is useful
when button action conditions are changing as a result of action
execution. All you need is to set the `refreshActions` flag of the form to
`True` in your action handler.
- Added support for using sources. Where it was previosly possible to use a
vocabulary it is now also possible to use a source. This works both for
basic and contextual sources.
**IMPORTANT:** The `ChoiceTerms` and `CollectionTerms` in `z3c.form.term`
are now simple functions that query for real `ITerms` adapters for field's
`source` or `value_type` respectively. So if your code inherits the old
`ChoiceTerms` and `CollectionTerms` classes, you'll need to review and adapt
it. See the `z3c.form.term` module and its documentation.
- The new `z3c.form.interfaces.NOT_CHANGED` special value is available to
signal that the current value should be left as is. It's currently handled
in the `z3c.form.form.applyChanges()` function.
- When no file is specified in the file upload widget, instead of overwriting
the value with a missing one, the old data is retained. This is done by
returning the new `NOT_CHANGED` special value from the
`FileUploadDataConvereter`.
- Preliminary support for widgets for the `schema.IObject` field has been
added. However, there is a big caveat, please read the ``object-caveat.txt``
document inside the package.
A new `objectWidgetTemplate` ZCML directive is provided to register widget
templates for specific object field schemas.
- Implemented the `MultiWidget` widget. This widget allows you to use simple
fields like `ITextLine`, `IInt`, `IPassword`, etc. in a `IList` or `ITuple`
sequence.
- Implemented `TextLinesWidget` widget. This widget offers a text area element
and splits lines in sequence items. This is usfull for power user
interfaces. The widget can be used for sequence fields (e.g. `IList`) that
specify a simple value type field (e.g. `ITextLine` or `IInt`).
- Added a new flag `ignoreContext` to the form field, so that one can
individually select which fields should and which ones should not ignore the
context.
- Allow raw request values of sequence widgets to be non-sequence values,
which makes integration with Javascript libraries easier.
- Added support in the file upload widget's testing flavor to specify
'base64'-encoded strings in the hidden text area, so that binary data can be
uploaded as well.
- Allow overriding the `required` widget attribute using `IValue` adapter just
like it's done for `label` and `name` attributes.
- Add the `prompt` attribute of the `SequenceWidget` to the list of adaptable
attributes.
- Added benchmarking suite demonstrating performance gain when using
``z3c.pt``.
- Added support for ``z3c.pt``. Usage is switched on via the "PREFER_Z3C_PT"
environment variable or via ``z3c.ptcompat.config.[enable/diable]()``.
- The `TypeError` message used when a field does not provide `IFormUnicode`
now also contains the type of the field.
- Add support for internationalization of `z3c.form` messages. Added Russian,
French, German and Chinese translations.
- Sphinx documentation for the package can now be created using the new `docs`
script.
- The widget for fields implementing `IChoice` is now looked up by querying
for an adapter for ``(field, field.vocabulary, request)`` so it can be
differentiated according to the type of the source used for the field.
- Move `formErrorsMessage` attribute from `AddForm` and `EditForm` to the
`z3c.form.form.Form` base class as it's very common validation status
message and can be easily reused (especially when translations are
provided).
Refactoring
~~~~~~~~~~~
- Removed compatibility support with Zope 3.3.
- Templates now declare XML namespaces.
- HTML output is now compared using a modified version of the XML-aware output
checker provided by `lxml`.
- Remove unused imports, adjust buildout dependencies in `setup.py`.
- Use the `z3c.ptcompat` template engine compatibility layer.
Fixed Bugs
~~~~~~~~~~
- **IMPORTANT** - The signature of `z3c.form.util.extractFileName` function
changed because of spelling mistake fix in argument name. The
`allowEmtpyPostFix` is now called `allowEmptyPostfix` (note `Empty` instead
of `Emtpy` and `Postfix` instead of `PostFix`).
- **IMPORTANT** - The `z3c.form.interfaces.NOVALUE` special value has been
renamed to `z3c.form.interfaces.NO_VALUE` to follow the common naming
style. The backward-compatibility `NOVALUE` name is still in place, but the
`repr` output of the object has been also changed, thus it may break your
doctests.
- When dealing with `Bytes` fields, we should do a null conversion when going
to its widget value.
- `FieldWidgets` update method were appending keys and values within each
update call. Now the `util.Manager` uses a `UniqueOrderedKeys`
implementation which will ensure that we can't add duplicated manager
keys. The implementation also ensures that we can't override the
`UniqueOrderedKeys` instance with a new list by using a decorator. If this
`UniqueOrderedKeys` implementation doesn't fit for all use cases, we should
probably use a customized `UserList` implementation. Now we can call
``widgets.update()`` more then one time without any side effect.
- `ButtonActions` update where appending keys and values within each update
call. Now we can call ``actions.update()`` more then one time without any
side effect.
- The `CollectionSequenceDataConverter` no longer throws a ``TypeError:
'NoneType' object is not iterable`` when passed the value of a non-required
field (which in the case of a `List` field is `None`).
- The `SequenceDataConverter` and `CollectionSequenceDataConverter` converter
classes now ignore values that are not present in the terms when converting
to a widget value.
- Use ``nocall:`` modifier in `orderedselect_input.pt` to avoid calling list
entry if it is callable.
- `SingleCheckBoxFieldWidget` doesn't repeat the label twice (once in ``<div
class="label">``, and once in the ``<label>`` next to the checkbox).
- Don't cause warnings in Python 2.6.
- `validator.SimpleFieldValidator` is now able to handle
`interfaces.NOT_CHANGED`. This value is set for file uploads when the user
does not choose a file for upload.
Version 1.9.0 (2008-08-26)
--------------------------
- Feature: Use the ``query()`` method in the widget manager to try extract a
value. This ensures that the lookup is never failing, which is particularly
helpful for dictionary-based data managers, where dictionaries might not
have all keys.
- Feature: Changed the ``get()`` method of the data manager to throw an error
when the data for the field cannot be found. Added ``query()`` method to
data manager that returns a default value, if no value can be found.
- Feature: Deletion of widgets from field widget managers is now possible.
- Feature: Groups now produce detailed `ObjectModifiedEvent` descriptions like
regular edit forms do. (Thanks to Carsten Senger for providing a patch.)
- Feature: The widget manager's ``extract()`` method now supports an optional
``setErrors`` (default value: True) flag that allows one to not set errors
on the widgets and widget manager during data extraction. Use case: You want
to inspect the entered data and handle errors manually.
- Bug: The ``ignoreButtons`` flag of the ``z3c.form.form.extends()`` method
was not honored. (Thanks to Carsten Senger for providing a patch.)
- Bug: Group classes now implement ``IGroup``. This also helps with the
detection of group instantiation. (Thanks to Carsten Senger for providing a
patch.)
- Bug: The list of changes in a group were updated incorrectly, since it was
assumed that groups would modify mutually exclusive interfaces. Instead of
using an overwriting dictionary ``update()`` method, a purely additive merge
is used now. (Thanks to Carsten Senger for providing a patch.)
- Bug: Added a widget for ``IDecimal`` field in testing setup.
- Feature: The ``z3c.form.util`` module has a new function, ``createCSSId()``
method that generates readable ids for use with css selectors from any
unicode string.
- Bug: The ``applyChanges()`` method in group forms did not return a changes
dictionary, but simply a boolean. This is now fixed and the group form
changes are now merged with the main form changes.
- Bug: Display widgets did not set the style attribute if it was
available, even though the input widgets did set the style attribute.
Version 1.8.2 (2008-04-24)
--------------------------
- Bug: Display Widgets added spaces (due to code indentation) to the displayed
values, which in some cases, like when displaying Python source code, caused
the appearance to be incorrect.
- Bug: Prevent to call ``__len__`` on ``ITerms`` and use ``is None`` for check
for existence. Because ``__len__`` is not a part of the ITerms API and ``not
widget.terms`` will end in calling ``__len__`` on existing terms.
Version 1.8.1 (2008-04-08)
--------------------------
- Bug: Fixed a bug that prohibited groups from having different contents than
the parent form. Previously, the groups contents were not being properly
updated. Added new documentation on how to use groups to generate
object-based sub-forms. Thanks to Paul Carduner for providing the fix and
documentation.
Version 1.8.0 (2008-01-23)
--------------------------
- Feature: Implemented ``IDisplayForm`` interface.
- Feature: Added integration tests for form interfaces. Added default class
attribute called ``widgets`` in form class with default value ``None``. This
helps to pass the integration tests. Now, the ``widgets`` attribute can also
be used as a indicator for updated forms.
- Feature: Implemented additional ``createAndAdd`` hook in ``AddForm``. This
allows you to implement create and add in a single method. It also supports
graceful abortion of a create and add process if we do not return the new
object. This means it can also be used as a hook for custom error messages
for errors happen during create and add.
- Feature: Add a hidden widget template for the ``ISelectWidget``.
- Feature: Arrows in the ordered select widget replaced by named entities.
- Feature: Added ``CollectionSequenceDataConverter`` to ``setupFormDefaults``.
- Feature: Templates for the CheckBox widget are now registered in
``checkbox.zcml``.
- Feature: If a value cannot be converted from its unicode representation to a
field value using the field's ``IFromUnicode`` interface, the resulting type
error now shows the field name, if available.
- Bug: ``createId`` could not handle arbitrary unicode input. Thanks to
Andreas Reuleaux for reporting the bug and a patch for it. (Added
descriptive doctests for the function in the process.)
- Bug: Interface invariants where not working when not all fields needed for
computing the invariant are in the submitted form.
- Bug: Ordered select didn't submit selected values.
- Bug: Ordered select lists displayed tokens instead of value,
- Bug: ``SequenceWidget`` displayed tokens instead of value.
Version 1.7.0 (2007-10-09)
--------------------------
- Feature: Implemented ``ImageButton``, ``ImageAction``, ``ImageWidget``, and
``ImageFieldWidget`` to support imge submit buttons.
- Feature: The ``AttributeField`` data manager now supports adapting
the content to the fields interface when the content doesn't implement
this interface.
- Feature: Implemented single checkbox widget that can be used for boolean
fields. They are not available by default but can be set using the
``widgetFactory`` attribute.
- Bug: More lingual issues have been fixed in the documentation. Thanks to
Martijn Faassen for doing this.
- Bug: When an error occurred during processing of the request the
widget ended up being security proxied and the system started
throwing `TraversalError`-'s trying to access the `label` attribute of
the widget. Declared that the widgets require the `zope.Public`
permission in order to access these attributes.
- Bug: When rendering a widget the ``style`` attribute was not honored. Thanks
to Andreas Reuleaux for reporting.
- Bug: When an error occurred in the sub-form, the status message was not set
correctly. Fixed the code and the incorrect test. Thanks to Markus
Kemmerling for reporting.
- Bug: Several interfaces had the ``self`` argument in the method
signature. Thanks to Markus Kemmerling for reporting.
Version 1.6.0 (2007-08-24)
--------------------------
- Feature: An event handler for ``ActionErrorOccurred`` events is registered
to merge the action error into the form's error collectors, such as
``form.widgets.errors`` and ``form.widgets['name'].error`` (if
applicable). It also sets the status of the form. (Thanks to Herman
Himmelbauer, who requested the feature, for providing use cases.)
- Feature: Action can now raise ``ActionExecutionError`` exceptions that will
be handled by the framework. These errors wrap the original error. If an
error is specific to a widget, then the widget name is passed to a special
``WidgetActionExecutionError`` error. (Thanks to Herman Himmelbauer, who
requested the feature, for providing use cases.)
- Feature: After an action handler has been executed, an action executed event
is sent to the system. If the execution was successful, the event is
``ActionSuccessfull`` event is sent. If an action execution error was
raised, the ``ActionErrorOccurred`` event is raised. (Thanks to Herman
Himmelbauer, who requested the feature, for providing use cases.)
- Feature: The ``applyChanges()`` function now returns a dictionary of changes
(grouped by interface) instead of a boolean. This allows us to generate a
more detailed object-modified event. If no changes are applied, an empty
dictionary is returned. The new behavior is compatible with the old one, so
no changes to your code are required. (Thanks to Darryl Cousins for the
request and implementation.)
- Feature: A new ``InvalidErrorViewSnippet`` class provides an error view
snippet for ``zope.interface.Invalid`` exceptions, which are frequently used
for invariants.
- Feature: When a widget is required, HTML-based widgets now declare a
"required" class.
- Feature: The validation data wrapper now knows about the context of the
validation, which provides a hook for invariants to access the environment.
- Feature: The BoolTerms term tokens are now cosntants and stay the same, even
if the label has changed. The choice for the token is "true" and "false". By
default it used to be "yes" and "no", so you probably have to change some
unit tests. Functional tests are still okay, because you select by term
title.
- Feature: BoolTerms now expose the labels for the true and false values
to the class. This makes it a matter of doing trivial sub-classing to
change the labels for boolean terms.
- Feature: Exposed several attributes of the widget manager to the form for
convenience. The attributes are: mode, ignoreContext, ignoreRequest,
ignoreReadonly.
- Feature: Provide more user-friendly error messages for number formatting.
- Refactoring: The widget specific class name was in camel-case. A converntion
that later developed uses always dash-based naming of HTML/CSS related
variables. So for example, the class name "textWidget" is now
"text-widget". This change will most likely require some changes to your CSS
declarations!
- Documentation: The text of ``field.txt`` has been reviewed linguistically.
- Documentation: While reviewing the ``form.txt`` with some people, several
unclear and incomplete statements were discovered and fixed.
- Bug (IE): In Internet Explorer, when a label for a radio input field is only
placed around the text describing the choice, then only the text is
surrounded by a dashed box. IE users reported this to be confusing, thus we
now place the label around the text and the input element so that both are
surrounded by the dashed border. In Firefox and KHTML (Safari) only the
radio button is surrounded all the time.
- Bug: When extracting and validating data in the widget manager, invariant
errors were not converted to error view snippets.
- Bug: When error view snippets were not widget-specific -- in other words,
the ``widget`` attribute was ``None`` -- rendering the template would fail.
Version 1.5.0 (2007-07-18)
--------------------------
- Feature: Added a span around values for widgets in display mode. This allows
for easier identification widget values in display mode.
- Feature: Added the concept of widget events and implemented a particular
"after widget update" event that is called right after a widget is updated.
- Feature: Restructured the approach to customize button actions, by requiring
the adapter to provide a new interface ``IButtonAction``. Also, an adapter
is now provided by default, still allowing cusotmization using the usual
methods though.
- Feature: Added button widget. While it is not very useful without
Javascript, it still belongs into this package for completion.
- Feature: All ``IFieldWidget`` instances that are also HTML element widgets
now declare an additional CSS class of the form "<fieldtype.lower()>-field".
- Feature: Added ``addClass()`` method to HTML element widgets, so that adding
a new CSS class is simpler.
- Feature: Renamed "css" attribute of the widget to "klass", because the class
of an HTML element is a classification, not a CSS marker.
- Feature: Reviewed all widget attributes. Added all available HTML attributes
to the widgets.
- Documentation: Removed mentioning of widget's "hint" attribute, since it
does not exist.
- Optimization: The terms for a sequence widget were looked up multiple times
among different components. The widget is now the canonical source for the
terms and other components, such as the converter uses them. This avoids
looking up the terms multiple times, which can be an expensive process for
some applications.
- Bug/Feature: Correctly create labels for radio button choices.
- Bug: Buttons did not honor the name given by the schema, if created within
one, because we were too anxious to give buttons a name. Now name assignment
is delayed until the button is added to the button manager.
- Bug: Button actions were never updated in the actions manager.
- Bug: Added tests for textarea widget.
Version 1.4.0 (2007-06-29)
--------------------------
- Feature: The select widget grew a new ``prompt`` flag, which allows you to
explicitely request a selection prompt as the first option in the selection
(even for required fields). When set, the prompt message is shown. Such a
prompt as option is common in Web-UIs.
- Feature: Allow "no value message" of select widgets to be dynamically
changed using an attribute value adapter.
- Feature: Internationalized data conversion for date, time, date/time,
integer, float and decimal. Now the locale data is used to format and parse
those data types to provide the bridge to text-based widgets. While those
features require the latest zope.i18n package, backward compatibility is
provided.
- Feature: All forms now have an optional label that can be used by the UI.
- Feature: Implemented groups within forms. Groups allow you to combine a set
of fields/widgets into a logical unit. They were designed with ease of use
in mind.
- Feature: Button Actions -- in other words, the widget for the button field
-- can now be specified either as the "actionFactory" on the button field or
as an adapter.
- Bug: Recorded all public select-widget attributes in the interface.
Version 1.3.0 (2007-06-22)
--------------------------
- Feature: In an edit form applying the data and generating all necessary
messages was all done within the "Apply" button handler. Now the actual task
of storing is factored out into a new method called "applyChanges(data)",
which returns whether the data has been changed. This is useful for forms
not dealing with objects.
- Feature: Added support for ``hidden`` fields. You can now use the ``hidden``
mode for widgets which should get rendered as ``<input type="hidden"
/>``.
Note: Make sure you use the new formui templates which will avoid rendering
labels for hidden widgets or adjust your custom form macros.
- Feature: Added ``missing_value`` support to data/time converters
- Feature: Added named vocabulary lookup in ``ChoiceTerms`` and
``CollectionTerms``.
- Feature: Implemented support for ``FileUpload`` in ``FileWidget``.
* Added helper for handling ``FileUpload`` widgets:
+ ``extractContentType(form, id)``
Extracts the content type if ``IBytes``/``IFileWidget`` was used.
+ ``extractFileName(form, id, cleanup=True, allowEmtpyPostFix=False)``
Extracts a filename if ``IBytes``/``IFileWidget`` was used.
Uploads from win/IE need some cleanup because the filename includes also
the path. The option ``cleanup=True`` will do this for you. The option
``allowEmtpyPostFix`` allows you to pass a filename without
extensions. By default this option is set to ``False`` and will raise a
``ValueError`` if a filename doesn't contain an extension.
* Created afile upload data converter registered for
``IBytes``/``IFileWidget`` ensuring that the converter will only be used
for fiel widgets. The file widget is now the default for the bytes
field. If you need to use a text area widget for ``IBytes``, you have to
register a custom widget in the form using::
fields['foobar'].widgetFactory = TextWidget
- Feature: Originally, when an attribute access failed in Unauthorized or
ForbiddenAttribute exceptions, they were ignored as if the attribute would
have no value. Now those errors are propagated and the system will fail
providing the developer with more feedback. The datamanager also grew a new
``query()`` method that returns always a default and the ``get()`` method
propagates any exceptions.
- Feature: When writing to a field is forbidden due to insufficient
priviledges, the resulting widget mode will be set to "display". This
behavior can be overridden by explicitely specifying the mode on a field.
- Feature: Added an add form implementation against ``IAdding``. While this is
not an encouraged method of adding components, many people still use this
API to extend the ZMI.
- Feature: The ``IFields`` class' ``select()`` and ``omit()`` method now
support two ketword arguments "prefix" and "interface" that allow the
selection and omission of prefixed fields and still specify the short
name. Thanks to Nikolay Kim for the idea.
- Feature: HTML element ids containing dots are not very good, because then
the "element#id" CSS selector does not work and at least in Firefox the
attribute selector ("element[attr=value]") does not work for the id
either. Converted the codebase to use dashes in ids instead.
- Bug/Feature: The ``IWidgets`` component is now an adapter of the form
content and not the form context. This guarantees that vocabulary factories
receive a context that is actually useful.
- Bug: The readonly flag within a field was never honored. When a field is
readonly, it is displayed in "display" mode now. This can be overridden by
the widget manager's "ignoreReadonly" flag, which is necessary for add
forms.
- Bug: The mode selection made during the field layout creation was not
honored and the widget manager always overrode the options providing its
value. Now the mode specified in the field is more important than the one
from the widget manager.
- Bug: It sometimes happens that the sequence widget has the no-value token as
one element. This caused ``displayValue()`` to fail, since it tried to find
a term for it. For now we simply ignore the no-value token.
- Bug: Fixed the converter when the incoming value is an empty string. An
empty string really means that we have no value and it is thus missing,
returning the missing value.
- Bug: Fix a slightly incorrect implementation. It did not cause any harm in
real-world forms, but made unit testing much harder, since an API
expectation was not met correctly.
- Bug: When required selections where not selected in radio and checkbox
widgets, then the conversion did not behave correctly. This also revealed
some issues with the converter code that have been fixed now.
- Bug: When fields only had a vocabulary name, the choice terms adaptation
would fail, since the field was not bound. This has now been corrected.
- Documentation: Integrated English language and content review improvements
by Roy Mathew in ``form.txt``.
Version 1.2.0 (2007-05-30)
--------------------------
- Feature: Added ability to change the button action title using an ``IValue``
adapter.
Version 1.1.0 (2007-05-30)
--------------------------
- Feature: Added compatibility for Zope 3.3 and thus Zope 2.10.
Version 1.0.0 (2007-05-24)
--------------------------
- Initial Release
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
z3c.form-2.0.0.tar.gz
(387.4 kB
view hashes)