zope.formlib-4.2.0/ 0000755 0000765 0000024 00000000000 12055141032 014205 5 ustar sweh staff 0000000 0000000 zope.formlib-4.2.0/bootstrap.py 0000644 0000765 0000024 00000010115 12055134250 016576 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2006 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Bootstrap a buildout-based project
Simply run this script in a directory containing a buildout.cfg.
The script accepts buildout command-line options, so you can
use the -c option to specify an alternate configuration file.
$Id: bootstrap.py 122633 2011-08-20 16:11:34Z hannosch $
"""
import os, shutil, sys, tempfile, urllib2
from optparse import OptionParser
tmpeggs = tempfile.mkdtemp()
is_jython = sys.platform.startswith('java')
# parsing arguments
parser = OptionParser(
'This is a custom version of the zc.buildout %prog script. It is '
'intended to meet a temporary need if you encounter problems with '
'the zc.buildout 1.5 release.')
parser.add_option("-v", "--version", dest="version", default='1.4.4',
help='Use a specific zc.buildout version. *This '
'bootstrap script defaults to '
'1.4.4, unlike usual buildpout bootstrap scripts.*')
parser.add_option("-d", "--distribute",
action="store_true", dest="distribute", default=False,
help="Use Disribute rather than Setuptools.")
parser.add_option("-c", None, action="store", dest="config_file",
help=("Specify the path to the buildout configuration "
"file to be used."))
options, args = parser.parse_args()
# if -c was provided, we push it back into args for buildout' main function
if options.config_file is not None:
args += ['-c', options.config_file]
if options.version is not None:
VERSION = '==%s' % options.version
else:
VERSION = ''
USE_DISTRIBUTE = options.distribute
args = args + ['bootstrap']
to_reload = False
try:
import pkg_resources
if not hasattr(pkg_resources, '_distribute'):
to_reload = True
raise ImportError
except ImportError:
ez = {}
if USE_DISTRIBUTE:
exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py'
).read() in ez
ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True)
else:
exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
).read() in ez
ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
if to_reload:
reload(pkg_resources)
else:
import pkg_resources
if sys.platform == 'win32':
def quote(c):
if ' ' in c:
return '"%s"' % c # work around spawn lamosity on windows
else:
return c
else:
def quote (c):
return c
ws = pkg_resources.working_set
if USE_DISTRIBUTE:
requirement = 'distribute'
else:
requirement = 'setuptools'
env = dict(os.environ,
PYTHONPATH=
ws.find(pkg_resources.Requirement.parse(requirement)).location
)
cmd = [quote(sys.executable),
'-c',
quote('from setuptools.command.easy_install import main; main()'),
'-mqNxd',
quote(tmpeggs)]
if 'bootstrap-testing-find-links' in os.environ:
cmd.extend(['-f', os.environ['bootstrap-testing-find-links']])
cmd.append('zc.buildout' + VERSION)
if is_jython:
import subprocess
exitcode = subprocess.Popen(cmd, env=env).wait()
else: # Windows prefers this, apparently; otherwise we would prefer subprocess
exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
assert exitcode == 0
ws.add_entry(tmpeggs)
ws.require('zc.buildout' + VERSION)
import zc.buildout.buildout
zc.buildout.buildout.main(args)
shutil.rmtree(tmpeggs)
zope.formlib-4.2.0/buildout.cfg 0000644 0000765 0000024 00000000717 12055134250 016526 0 ustar sweh staff 0000000 0000000 [buildout]
develop = .
parts = test python coverage-test coverage-report
[test]
recipe = zc.recipe.testrunner
eggs = zope.formlib [test]
[python]
recipe = zc.recipe.egg
eggs = zope.formlib
interpreter = python
[coverage-test]
recipe = zc.recipe.testrunner
eggs = ${test:eggs}
defaults = ['--coverage', '../../coverage']
[coverage-report]
recipe = zc.recipe.egg
eggs = z3c.coverage
scripts = coverage=coverage-report
arguments = ('coverage', 'coverage/report') zope.formlib-4.2.0/CHANGES.txt 0000644 0000765 0000024 00000010745 12055136470 016037 0 ustar sweh staff 0000000 0000000 =======
Changes
=======
4.2.0 (2012-11-27)
==================
- LP #1017884: Add redirect status codes (303, 307) to the set which prevent
form rendering.
- Replaced deprecated ``zope.component.adapts`` usage with equivalent
``zope.component.adapter`` decorator.
- Replaced deprecated ``zope.interface.implements`` usage with equivalent
``zope.interface.implementer`` decorator.
- Dropped support for Python 2.5.
- Make separator of ``SourceSequenceDisplayWidget`` configurable.
4.1.1 (2012-03-16)
==================
- Added `ignoreContext` attribute to form classes to control whether
`checkInvariants` takes the context of the form into account when
checking interface invariants.
By default `ignoreContext` is set to ``False``. On the `AddForm` it is
``True`` by default because the context of this form is naturally not
suitable as context for the interface invariant.
4.1.0 (2012-03-15)
==================
- `checkInvariants` now takes the context of the form into account when
checking interface invariants.
- Tests are no longer compatible with Python 2.4.
4.0.6 (2011-08-20)
==================
- Fixed bug in ``orderedSelectionList.pt`` template.
4.0.5 (2010-09-16)
==================
- Fixed Action name parameter handling, since 4.0.3 all passed names were
lowercased.
4.0.4 (2010-07-06)
==================
- Fixed tests to pass under Python 2.7.
- Fix validation of "multiple" attributes in orderedSelectionList.pt.
4.0.3 (2010-05-06)
==================
- Keep Actions from raising exceptions when passed Unicode lables [LP:528468].
- Improve display of the "nothing selected" case for optional Choice fields
[LP:269782].
- Improve truth testing for ItemDisplayWidget [LP:159232].
- Don't blow up if TypeError raised during token conversion [LP:98491].
4.0.2 (2010-03-07)
==================
- Adapted tests for Python 2.4 (enforce sorting for short pprint output)
4.0.1 (2010-02-21)
==================
- Documentation uploaded to PyPI now contains widget documentation.
- Escape MultiCheckBoxWidget content [LP:302427].
4.0 (2010-01-08)
================
- Widget implementation and all widgets from zope.app.form have been
moved into zope.formlib, breaking zope.formlib's dependency on
zope.app.form (instead zope.app.form now depends on zope.formlib).
Widgets can all be imported from ``zope.formlib.widgets``.
Widget base classes and render functionality is in
``zope.formlib.widget``.
All relevant widget interfaces are now in ``zope.formlib.interfaces``.
3.10.0 (2009-12-22)
===================
- Use named template from zope.browserpage in favor of zope.app.pagetemplate.
3.9.0 (2009-12-22)
==================
- Use ViewPageTemplateFile from zope.browserpage.
3.8.0 (2009-12-22)
==================
- Adjusted test output to new zope.schema release.
3.7.0 (2009-12-18)
==================
- Rid ourselves from zope.app test dependencies.
- Fix: Button label needs escaping
3.6.0 (2009-05-18)
==================
- Remove deprecated imports.
- Remove dependency on zope.app.container (use ``IAdding`` from
``zope.browser.interfaces``) instead. Depend on
``zope.browser>=1.1`` (the version with ``IAdding``).
- Moved ``namedtemplate`` to ``zope.app.pagetemplate``, to cut some
dependencies on ``zope.formlib`` when using this feature. Left BBB
imports here.
3.5.2 (2009-02-21)
==================
- Adapt tests for Python 2.5 output.
3.5.1 (2009-01-31)
==================
- Adapt tests to upcoming zope.schema release 3.5.1.
3.5.0 (2009-01-26)
==================
New Features
------------
- Test dependencies are declared in a `test` extra now.
- Introduced ``zope.formlib.form.applyData`` which works like
``applyChanges`` but returns a dictionary with information about
which attribute of which schema changed. This information is then
sent along with the ``IObjectModifiedEvent``.
This fixes https://bugs.launchpad.net/zope3/+bug/98483.
Bugs Fixed
----------
- Actions that cause a redirect (301, 302) do not cause the `render` method to
be called anymore.
- The zope.formlib.form.Action class didn't fully implement
zope.formlib.interfaces.IAction.
- zope.formlib.form.setupWidgets and zope.formlib.form.setupEditWidgets did
not check for write access on the adapter but on context. This fixes
https://bugs.launchpad.net/zope3/+bug/219948
3.4.0 (2007-09-28)
==================
No further changes since 3.4.0a1.
3.4.0a1 (2007-04-22)
====================
Initial release as a separate project, corresponds to zope.formlib
from Zope 3.4.0a1
zope.formlib-4.2.0/COPYRIGHT.txt 0000644 0000765 0000024 00000000040 12055134250 016314 0 ustar sweh staff 0000000 0000000 Zope Foundation and Contributors zope.formlib-4.2.0/LICENSE.txt 0000644 0000765 0000024 00000004026 12055134250 016036 0 ustar sweh staff 0000000 0000000 Zope Public License (ZPL) Version 2.1
A copyright notice accompanies this license document that identifies the
copyright holders.
This license has been certified as open source. It has also been designated as
GPL compatible by the Free Software Foundation (FSF).
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions in source code must retain the accompanying copyright
notice, this list of conditions, and the following disclaimer.
2. Redistributions in binary form must reproduce the accompanying copyright
notice, this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Names of the copyright holders must not be used to endorse or promote
products derived from this software without prior written permission from the
copyright holders.
4. The right to distribute this software or to use it for any purpose does not
give you the right to use Servicemarks (sm) or Trademarks (tm) of the
copyright
holders. Use of them is covered by separate agreement with the copyright
holders.
5. If any files are modified, you must cause the modified files to carry
prominent notices stating that you changed the files and the date of any
change.
Disclaimer
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
zope.formlib-4.2.0/PKG-INFO 0000644 0000765 0000024 00000313654 12055141032 015316 0 ustar sweh staff 0000000 0000000 Metadata-Version: 1.0
Name: zope.formlib
Version: 4.2.0
Summary: Form generation and validation library for Zope
Home-page: http://pypi.python.org/pypi/zope.formlib
Author: Zope Foundation and Contributors
Author-email: zope-dev@zope.org
License: ZPL 2.1
Description: .. contents::
========
Overview
========
Forms are web components that use widgets to display and input data.
Typically a template displays the widgets by accessing an attribute or
method on an underlying class.
=====
Forms
=====
Forms are web components that use widgets to display and input data.
Typically a template displays the widgets by accessing an attribute or
method on an underlying class.
This document describes some tools to assist in form development. In
the examples, we will show "forms" that are generated with simple
print statements to keep the examples simpler. Most forms will use
templates in practice.
This document starts with low-level APIs. We eventually build up to
higher-level APIs that allow forms to be defined with just a little bit
of meta data. Impatient readers may wish to skip to the later
sections, especially the section on `Helpful base classes`_. :)
A form class can define ordered collections of "form fields" using
the `Fields` constructor. Form fields are distinct from and build on
schema fields. A schema field specified attribute values. Form
fields specify how a schema field should be used in a form. The
simplest way to define a collection of form fields is by passing a
schema to the `Fields` constructor:
>>> from zope import interface, schema
>>> class IOrder(interface.Interface):
... identifier = schema.Int(title=u"Identifier", readonly=True)
... name = schema.TextLine(title=u"Name")
... min_size = schema.Float(title=u"Minimum size")
... max_size = schema.Float(title=u"Maximum size")
... color = schema.TextLine(title=u"Color", required=False)
... now = schema.Datetime(title=u"Now", readonly=True)
>>> from zope.formlib import form
>>> class MyForm:
... form_fields = form.Fields(IOrder)
This sets up a set of form fields from the interface, IOrder.
>>> len(MyForm.form_fields)
6
>>> [w.__name__ for w in MyForm.form_fields]
['identifier', 'name', 'min_size', 'max_size', 'color', 'now']
We can access individual form fields by name:
>>> MyForm.form_fields['name'].__name__
'name'
We can also select and order subsets using the select method of form fields:
>>> [w.__name__ for w in MyForm.form_fields.select('name', 'identifier')]
['name', 'identifier']
or by omitting fields:
>>> [w.__name__ for w in MyForm.form_fields.omit('now', 'identifier')]
['name', 'min_size', 'max_size', 'color']
We can omit read-only fields using the omit_readonly option when
setting up the fields:
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
>>> [w.__name__ for w in MyForm.form_fields]
['name', 'min_size', 'max_size', 'color']
Getting HTML
============
Having defined form fields, we can use them to generate HTML
forms. Typically, this is done at run time by form class
instances. Let's look at an example that displays some input widgets:
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self, ignore_request=False):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request,
... ignore_request=ignore_request)
... return '\n'.join([w() for w in widgets])
Here we used ``form.setUpWidgets`` to create widget instances from our
form-field specifications. The second argument to ``setUpWidgets`` is a
form prefix. All of the widgets on this form are given the same
prefix. This allows multiple forms to be used within a single form
tag, assuming that each form uses a different form prefix.
Now, we can display the form:
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
If the request contains any form data, that will be reflected in the
output:
>>> request.form['form.name'] = u'bob'
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Sometimes we don't want this behavior: we want to ignore the request values,
particularly after a form has been processed and before it is drawn again.
This can be accomplished with the 'ignore_request' argument in
setUpWidgets.
>>> print MyForm(None, request)(ignore_request=True)
... # doctest: +NORMALIZE_WHITESPACE
Reading data
============
Of course, we don't just want to display inputs. We want to get the
input data. We can use getWidgetsData for that:
>>> from pprint import pprint
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... if 'submit' in self.request:
... data = {}
... errors = form.getWidgetsData(widgets, 'form', data)
... if errors:
... print 'There were errors:'
... for error in errors:
... print error
... else:
... data = None
...
... for w in widgets:
... print w()
... error = w.error()
... if error:
... print error
...
... return data
We check for a 'submit' variable in the form and, if we see it, we try
to get the data, and errors. We call `getWidgetsData`, passing:
- Our widgets
- The form prefix, and
- A data dictionary to contain input values found
The keys in the data dictionary have the form prefix stripped off.
If there are errors, we print them. When we display the widgets, we
also check for errors and show them if present. Let's add a submit
variable:
>>> request.form['form.min_size'] = u''
>>> request.form['form.max_size'] = u''
>>> request.form['submit'] = u'Submit'
>>> MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
There were errors:
('min_size', u'Minimum size', RequiredMissing('min_size'))
('max_size', u'Maximum size', RequiredMissing('max_size'))
Required input is missing.Required input is missing.
{'name': u'bob'}
Note that we got an error because we omitted the values for min_size
and max size. If we provide an invalid value, we'll get an error too:
>>> request.form['form.min_size'] = u'bob'
>>> MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
There were errors:
(u'Invalid floating point data',
)
('max_size', u'Maximum size', RequiredMissing('max_size'))
Invalid floating point dataRequired input is missing.
{'name': u'bob'}
If we provide valid data, we'll get the data back:
>>> request.form['form.min_size'] = u'42'
>>> request.form['form.max_size'] = u'142'
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
It's up to the form to decide what to do with the information.
Invariants
==========
The `getWidgetsData` function checks individual field constraints.
Interfaces can also provide invariants that we may also want to check.
The `checkInvariants` function can be used to do that.
In our order example, it makes sense to require that the maximum is
greater than or equal to the minimum:
>>> class IOrder(interface.Interface):
... identifier = schema.Int(title=u"Identifier", readonly=True)
... name = schema.TextLine(title=u"Name")
... min_size = schema.Float(title=u"Minimum size")
... max_size = schema.Float(title=u"Maximum size")
... now = schema.Datetime(title=u"Now", readonly=True)
...
... @interface.invariant
... def maxGreaterThanMin(order):
... if order.max_size < order.min_size:
... raise interface.Invalid("Maximum is less than Minimum")
We can update our form to check the invariant using 'checkInvariants':
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... if 'submit' in self.request:
... data = {}
... errors = form.getWidgetsData(widgets, 'form', data)
... invariant_errors = form.checkInvariants(
... self.form_fields, data, self.context)
... if errors:
... print 'There were field errors:'
... for error in errors:
... print error
...
... if invariant_errors:
... print 'There were invariant errors:'
... for error in invariant_errors:
... print error
... else:
... data = None
...
... for w in widgets:
... print w()
... error = w.error()
... if error:
... print error
...
... return data
If we display the form again, we'll get the same result:
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
But if we reduce the maximum below the minimum, we'll get an invariant
error:
>>> request.form['form.min_size'] = u'42'
>>> request.form['form.max_size'] = u'14'
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
There were invariant errors:
Maximum is less than Minimum
{'max_size': 14.0,
'min_size': 42.0,
'name': u'bob'}
We can have field errors and invariant errors:
>>> request.form['form.name'] = u''
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
There were field errors:
('name', u'Name', RequiredMissing('name'))
There were invariant errors:
Maximum is less than Minimum
Required input is missing.
{'max_size': 14.0,
'min_size': 42.0}
If the inputs for some fields tested by invariants are missing, the
invariants are ignored:
>>> request.form['form.max_size'] = u''
>>> pprint(MyForm(None, request)()) # doctest: +NORMALIZE_WHITESPACE
There were field errors:
('name', u'Name', RequiredMissing('name'))
('max_size', u'Maximum size', RequiredMissing('max_size'))
Required input is missing.Required input is missing.
{'min_size': 42.0}
Edit Forms
==========
A common application of forms is edit forms. Edit forms are special
in 2 ways:
- We want to get the initial data for widgets from the object being
edited.
- If there are no errors, we want to apply the changes back to the
object being edited.
The form package provides some functions to assist with creating edit
forms. When we set up our form_fields, we use the `render_context`
option, which uses data from the context passed to setUpWidgets.
Let's create a content class that provides `IOrder` and a simple form
that uses it:
>>> import datetime
>>> @interface.implementer(IOrder)
... class Order:
...
... def __init__(self, identifier):
... self.identifier = identifier
... self.name = 'unknown'
... self.min_size = 0.0
... self.max_size = 0.0
...
... now = property(lambda self: datetime.datetime.now())
>>> order = Order(1)
>>> class MyForm:
... form_fields = form.Fields(
... IOrder, omit_readonly=True, render_context=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self, ignore_request=False):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request,
... ignore_request=ignore_request)
...
... return '\n'.join([w() for w in widgets])
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
Note that, in this case, we got the values from the request, because
we used an old request. If we want to redraw the form after processing a
request, it is safest to pass ignore_request = True to setUpWidgets so that
the form is redrawn with the values as found in the object, not on the request.
>>> print MyForm(order, request)(ignore_request=True)
... # doctest: +NORMALIZE_WHITESPACE
If we use a new request, we will of course get the same result:
>>> request = TestRequest()
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
If we include read-only fields in an edit form, they will get display widgets:
>>> class MyForm:
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... return '\n'.join([w() for w in widgets])
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
1
When the form is submitted, we need to apply the changes back to the
object. We can use the `applyChanges` function for that:
>>> class MyForm:
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... if 'submit' in self.request:
... data = {}
... errors = form.getWidgetsData(widgets, 'form', data)
... invariant_errors = form.checkInvariants(
... self.form_fields, data, self.context)
... if errors:
... print 'There were field errors:'
... for error in errors:
... print error
...
... if invariant_errors:
... print 'There were invariant errors:'
... for error in invariant_errors:
... print error
...
... if not errors and not invariant_errors:
... changed = form.applyChanges(
... self.context, self.form_fields, data)
...
... else:
... data = changed = None
...
... for w in widgets:
... print w()
... error = w.error()
... if error:
... print error
...
... if changed:
... print 'Object updated'
... else:
... print 'No changes'
...
... return data
Now, if we submit the form with some data:
>>> request.form['form.name'] = u'bob'
>>> request.form['form.min_size'] = u'42'
>>> request.form['form.max_size'] = u'142'
>>> request.form['submit'] = u''
>>> pprint(MyForm(order, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
1
Object updated
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
>>> order.name
u'bob'
>>> order.max_size
142.0
>>> order.min_size
42.0
Note, however, that if we submit the same request, we'll see that no
changes were applied:
>>> pprint(MyForm(order, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
1
No changes
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
because the new and old values are the same.
The code we included in `MyForm` above is generic: it applies to any
edit form.
Actions
=======
Our commit logic is a little complicated. It would be far more
complicated if there were multiple submit buttons.
We can use action objects to provide some distribution of application logic.
An action is an object that represents a handler for a submit button.
In the most common case, an action accepts a label and zero or more options
provided as keyword parameters:
condition
A callable or name of a method to call to test whether the action is
applicable. if the value is a method name, then the method will be
passed the action when called, otherwise, the callable will be
passed the form and the action.
validator
A callable or name of a method to call to validate and collect
inputs. This is called only if the action was submitted and if the
action either has no condition, or the condition evaluates to a true
value. If the validator is provided as a method name, the method
will be called with the action and a dictionary in which to save data.
If the validator is provided as a callable, the callable will be
called with the form, the action, and a dictionary in which to save data.
The validator normally returns a (usually empty) list of widget
input errors. It may also return None to behave as if the action
wasn't submitted.
success
A handler, called when the the action was submitted and there are no
validation errors. The handler may be provided as either a callable
or a method name. If the handler is provided as a method name, the
method will be called with the action and a dictionary containing the
form data. If the success handler is provided as a callable, the
callable will be called with the form, the action, and a dictionary
containing the data. The handler may return a form result
(e.g. page), or may return None to indicate that the form should
generate it's own output.
failure
A handler, called when the the action was submitted and there are
validation errors. The handler may be provided as either a callable
or a method name. If the handler is provided as a method name, the
method will be called with the action, a dictionary containing the form
data, and a list of errors. If the failure handler is provided as a
callable, the callable will be called with the form, the action, a
dictionary containing the data, and a list of errors. The handler
may return a form result (e.g. page), or may return None to indicate
that the form should generate it's own output.
prefix
A form prefix for the action. When generating submit actions, the
prefix should be combined with the action name, separating the two
with a dot. The default prefix is "actions"form.
name
The action name, without a prefix. If the label is a valid Python
identifier, then the lower-case label will be used, otherwise, a hex encoding
of the label will be used. If for some strange reason the labels in
a set of actions with the same prefix is not unique, a name will
have to be given for some actions to get unique names.
data
A bag of extra information that can be used by handlers, validators,
or conditions.
Let's update our edit form to use an action. We are also going to
rearrange our form quite a bit to make things more modular:
- We've created a separate `validation` method to validate inputs and
compute errors.
- We've created a `handle_edit_action` method for applying changes.
- We've created a template method for displaying the form.
Normally, this would be a ZPT template, but we just provide a Python
version here.
- We've created a call method that is described below
- We've defined a number of instance attributes for passing
information between the various methods:
- `status` is a string that, if set, is displayed at the top of the
form.
- `errors` is the set of errors found when validating.
- `widgets` is a list of set-up widgets
Here's the new version:
>>> class MyForm:
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... status = errors = None
... prefix = 'form'
...
... actions = form.Actions(
... form.Action('Edit', success='handle_edit_action'),
... )
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def validate(self, action, data):
... return (form.getWidgetsData(self.widgets, self.prefix, data) +
... form.checkInvariants(
... self.form_fields, data, self.context))
...
... def handle_edit_action(self, action, data):
... if form.applyChanges(self.context, self.form_fields, data):
... self.status = 'Object updated'
... else:
... self.status = 'No changes'
...
... def template(self):
... if self.status:
... print self.status
...
... result = []
...
... if self.errors:
... result.append('There were errors:')
... for error in self.errors:
... result.append(str(error))
...
... for w in self.widgets:
... result.append(w())
... error = w.error()
... if error:
... result.append(str(error))
...
... for action in self.actions:
... result.append(action.render())
...
... return '\n'.join(result)
...
... def __call__(self):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request)
...
... data = {}
... errors, action = form.handleSubmit(
... self.actions, data, self.validate)
... self.errors = errors
...
... if errors:
... result = action.failure(data, errors)
... elif errors is not None:
... result = action.success(data)
... else:
... result = None
...
... if result is None:
... result = self.template()
...
... return result
Lets walk through the `__call__` method.
- We set up our widgets as before.
- We use `form.handleSubmit` to validate our data. We pass the form,
actions, prefix, and `validate` method. For each action,
`form.handleSubmit` checks to see if the action was submitted. If the
action was submitted, it checks to see if it has a validator. If
the action has a validator, the action's validator is called,
otherwise the validator passed is called. The validator result (a
list of widget input errors) and the action are returned. If no
action was submitted, then `None` is returned for the errors and the
action.
- If a action was submitted and there were no errors, we call the
success method on the action. If the action has a handler defined,
it will be called and the return value is returned, otherwise None
is returned. A return value of None indicates that the form should
generate it's own result.
- If a action was submitted but there were errors, we call the
action's failure method. If the action has a failure handler
defined, it will be called and the return value is returned,
otherwise None is returned. A return value of None indicates that
the form should generate it's own result.
- No action was submitted, the result is set to None.
- If we don't have a result, we generate one with our template.
Let's try the new version of our form:
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
1
In this case, we didn't get any output about changes because the
request form data didn't include a submit action that matched our
action definition. Let's add one and try again:
>>> request.form['form.actions.edit'] = u''
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
No changes
1
This time, we got a status message indicating that there weren't any
changes.
Let's try changing some data:
>>> request.form['form.max_size'] = u'10/0'
>>> print MyForm(order, request)()
... # doctest: +NORMALIZE_WHITESPACE
There were errors:
(u'Invalid floating point data', )
1
Invalid floating point data
Oops, we had a typo, let's fix it:
>>> request.form['form.max_size'] = u'10.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
There were errors:
Maximum is less than Minimum
1
Oh yeah, we need to reduce the minimum too: :)
>>> request.form['form.min_size'] = u'1.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
Object updated
1
Ah, much better. And our order has been updated:
>>> order.max_size
10.0
>>> order.min_size
1.0
Helpful base classes
====================
Our form has a lot of repetitive code. A number of helpful base
classes provide standard form implementation.
Form
----
The `Form` base class provides a number of common attribute definitions.
It provides:
`__init__`
A constructor
`validate`
A default validation method
`__call__`
To render the form
`template`
A default template. Note that this is a NamedTemplate named "default",
so the template may also be overridden by registering an alternate
default template.
`prefix`
A string added to all widget and action names.
`setPrefix`
method for changing the prefix
`availableActions`
method for getting available actions
`adapters`
Dictionary of objects implementing each given schema
Subclasses need to:
- Provide a form_fields variable containing a list of form fields
- a actions attribute containing a list of action definitions
Subclasses may:
- Provide a label function or message id to produce
a form label.
- Override the setUpWidgets method to control how widgets are
set up. This is fairly rarely needed.
- Override the template. The form defines variables:
status
providing a short summary of the operation performed.
widgets
A collection of widgets, which can be accessed through iteration
or by name
errors
A (possibly empty) list of errors
Let's update our example to use the base class:
>>> class MyForm(form.Form):
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... @form.action("Edit", failure='handle_edit_action_failure')
... def handle_edit_action(self, action, data):
... if form.applyChanges(self.context, self.form_fields, data):
... self.status = 'Object updated'
... else:
... self.status = 'No changes'
...
... def handle_edit_action_failure(self, action, data, errors):
... self.status = 'There were %d errors.' % len(errors)
We inherited most of our behavior from the base class.
We also used the `action` decorator. The action decorator:
- creates an `actions` variable if one isn't already created,
- defines an action with the given label and any other arguments, and
- appends the action to the `actions` list.
The `action` decorator accepts the same arguments as the `Action`
class with the exception of the `success` option.
The creation of the `actions` is a bit magic, but provides
simplification in common cases.
Now we can try out our form:
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
No changes
1
>>> request.form['form.min_size'] = u'20.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
There were 1 errors.
Invalid: Maximum is less than Minimum
1
>>> request.form['form.max_size'] = u'30.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
Object updated
1
>>> order.max_size
30.0
>>> order.min_size
20.0
EditForm
--------
Our `handle_edit_action` action is common to edit forms. An
`EditForm` base class captures this commonality. It also sets up
widget widgets a bit differently. The `EditForm` base class sets up
widgets as if the form fields had been set up with the `render_context`
option.
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder)
... form_fields = form_fields.omit('now')
>>> request.form['form.actions.apply'] = u''
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
No changes
1
>>> request.form['form.min_size'] = u'40.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
There were errors
Invalid: Maximum is less than Minimum
1
>>> request.form['form.max_size'] = u'50.0'
>>> print MyForm(order, request)()
... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Updated on ... ... ... ...:...:...
1
>>> order.max_size
50.0
>>> order.min_size
40.0
Note that `EditForm` shows the date and time when content are
modified.
Multiple Schemas and Adapters
=============================
Forms can use fields from multiple schemas. This can be done in a
number of ways. For example, multiple schemas can be passed to
`form.Fields`:
>>> class IDescriptive(interface.Interface):
... title = schema.TextLine(title=u"Title")
... description = schema.TextLine(title=u"Description")
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder, IDescriptive)
... form_fields = form_fields.omit('now')
In addition, if the the object being edited doesn't provide any of the
schemas, it will be adapted to the schemas it doesn't provide.
Suppose we have a generic adapter for storing descriptive information
on objects:
>>> from zope import component
>>> @component.adapter(interface.Interface)
... @interface.implementer(IDescriptive)
... class Descriptive(object):
... def __init__(self, context):
... self.context = context
...
... def title():
... def get(self):
... try:
... return self.context.__title
... except AttributeError:
... return ''
... def set(self, v):
... self.context.__title = v
... return property(get, set)
... title = title()
...
... def description():
... def get(self):
... try:
... return self.context.__description
... except AttributeError:
... return ''
... def set(self, v):
... self.context.__description = v
... return property(get, set)
... description = description()
>>> component.provideAdapter(Descriptive)
Now, we can use a single form to edit both the regular order data and
the descriptive data:
>>> request = TestRequest()
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
1
>>> request.form['form.name'] = u'bob'
>>> request.form['form.min_size'] = u'10.0'
>>> request.form['form.max_size'] = u'20.0'
>>> request.form['form.title'] = u'Widgets'
>>> request.form['form.description'] = u'Need more widgets'
>>> request.form['form.actions.apply'] = u''
>>> myform = MyForm(order, request)
>>> print myform()
... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Updated on ... ... ... ...:...:...
1
>>> order.min_size
10.0
>>> order.title
Traceback (most recent call last):
...
AttributeError: Order instance has no attribute 'title'
>>> Descriptive(order).title
u'Widgets'
Often, we'd like to get at the adapters used. If `EditForm` is used,
the adapters are available in the adapters attribute, which is a
dictionary that allows adapters to be looked up by by schema or schema
name:
>>> myform.adapters[IOrder].__class__.__name__
'Order'
>>> myform.adapters['IOrder'].__class__.__name__
'Order'
>>> myform.adapters[IDescriptive].__class__.__name__
'Descriptive'
>>> myform.adapters['IDescriptive'].__class__.__name__
'Descriptive'
If you aren't using `EditForm`, you can get a dictionary populated in
the same way by `setUpWidgets` by passing the dictionary as an
`adapters` keyword argument.
Named Widget Access
===================
The value returned from `setUpWidgets` supports named-based lookup as well as
iteration:
>>> myform.widgets['name'].__class__.__name__
'TextWidget'
>>> myform.widgets['name'].name
'form.name'
>>> myform.widgets['title'].__class__.__name__
'TextWidget'
>>> myform.widgets['title'].name
'form.title'
Form-field manipulations
========================
The form-field constructor is very flexible. We've already seen that
we can supply multiple schemas. Here are some other things you can
do.
Specifying individual fields
----------------------------
You can specify individual fields for a form. Here, we'll create a
form that collects just the name from `IOrder` and the title from
`IDescriptive`:
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder['name'],
... IDescriptive['title'])
... actions = ()
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
You can also use stand-alone fields:
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(
... schema.TextLine(__name__='name', title=u"Who?"),
... IDescriptive['title'],
... )
... actions = ()
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
But make sure the fields have a '__name__', as was done above.
Concatenating field collections
-------------------------------
It is sometimes convenient to combine multiple field collections.
Field collections support concatenation. For example, we may want to
combine field definitions:
>>> class MyExpandedForm(form.Form):
... form_fields = (
... MyForm.form_fields
... +
... form.Fields(IDescriptive['description'])
... )
... actions = ()
>>> print MyExpandedForm(order, TestRequest())()
... # doctest: +NORMALIZE_WHITESPACE
Using fields for display
------------------------
Normally, any writable fields get input widgets. We may want to
indicate that some fields should be used for display only. We can do
this using the `for_display` option when setting up form_fields:
>>> class MyForm(form.EditForm):
... form_fields = (
... form.Fields(IOrder, for_display=True).select('name')
... +
... form.Fields(IOrder).select('min_size', 'max_size')
... )
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
bob
Note that if all of the fields in an edit form are for display:
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder, for_display=True
... ).select('name', 'min_size', 'max_size')
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
bob
10.0
20.0
we don't get an edit action. This is because the edit action defined
by `EditForm` has a condition to prevent it's use when there are no
input widgets. Check it out for an example of using action conditions.
Using fields for input
----------------------
We may want to indicate that some fields should be used for input even
if the underlying schema field is read-only. We can do this using the
`for_input` option when setting up form_fields:
>>> class MyForm(form.Form):
... form_fields = form.Fields(IOrder, for_input=True,
... render_context=True)
... form_fields = form_fields.omit('now')
...
... actions = ()
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
Displaying or editing raw data
==============================
Sometimes, you want to display or edit data that doesn't come from an
object. One way to do this is to pass the data to setUpWidgets.
Lets look at an example:
>>> class MyForm(form.Form):
...
... form_fields = form.Fields(IOrder)
... form_fields = form_fields.omit('now')
...
... actions = ()
...
... def setUpWidgets(self, ignore_request=False):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request,
... data=dict(identifier=42, name=u'sally'),
... ignore_request=ignore_request
... )
In this case, we supplied initial data for the identifier and the
name. Now if we display the form, we'll see our data and defaults for
the fields we didn't supply data for:
>>> print MyForm(None, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
42
If data are passed in the request, they override initial data for
input fields:
>>> request = TestRequest()
>>> request.form['form.name'] = u'fred'
>>> request.form['form.identifier'] = u'0'
>>> request.form['form.max_size'] = u'100'
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
42
We'll get display fields if we ask for display fields when setting up
our form fields:
>>> class MyForm(form.Form):
...
... form_fields = form.Fields(IOrder, for_display=True)
... form_fields = form_fields.omit('now')
...
... actions = ()
...
... def setUpWidgets(self, ignore_request=False):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request,
... data=dict(identifier=42, name=u'sally'),
... ignore_request=ignore_request
... )
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
42
sally
Note that we didn't get data from the request because we are using all
display widgets.
Passing `ignore_request=True` to the `setUpWidgets` function ignores
the request for all values passed in the data dictionary, in order to
help with redrawing a form after a successful action handler. We'll
fake that quickly by forcing ignore_request to be `True`.
>>> class MyForm(form.Form):
...
... form_fields = form.Fields(IOrder)
... form_fields = form_fields.omit('now')
...
... actions = ()
...
... def setUpWidgets(self, ignore_request=False):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request,
... data=dict(identifier=42, name=u'sally'),
... ignore_request=True # =ignore_request
... )
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
42
Specifying Custom Widgets
=========================
It is possible to use custom widgets for specific fields. This can be
done for a variety of reasons, but the provided mechanism should work
for any of them.
Custom widgets are specified by providing a widget factory that should
be used instead of the registered field view. The factory will be
called in the same way as any other field view factory, with the bound
field and the request as arguments.
Let's create a simple custom widget to use in our demonstration::
>>> import zope.formlib.widget
>>> class ISODisplayWidget(zope.formlib.widget.DisplayWidget):
...
... def __call__(self):
... return '2005-05-04'
To set the custom widget factory for a field, assign to the
`custom_widget` attribute of the form field object::
>>> class MyForm(form.Form):
... actions = ()
...
... form_fields = form.Fields(IOrder).select("now")
...
... # Here we set the custom widget:
...
... form_fields["now"].custom_widget = ISODisplayWidget
>>> print MyForm(None, request)()
2005-05-04
Specifying Fields individually
------------------------------
All of the previous examples set up fields as collections. We can
also set up forms individually and pass them to the Fields
constructor. This is especially useful for passing options that
really only apply to a single field. The previous example can be
written more simply as:
>>> class MyForm(form.Form):
... actions = ()
...
... form_fields = form.Fields(
... form.Field(IOrder['now'], custom_widget=ISODisplayWidget),
... )
>>> print MyForm(None, request)()
2005-05-04
Computing default values
------------------------
We saw earlier that we could provide initial widget data by passing a
dictionary to setUpWidgets. We can also supply a function or method
name when we set up form fields.
We might like to include the `now` field in our forms. We can provide
a function for getting the needed initial value:
>>> import datetime
>>> class MyForm(form.Form):
... actions = ()
...
... def now(self):
... return datetime.datetime(2002, 12, 2, 12, 30)
...
... form_fields = form.Fields(
... form.Fields(IOrder).omit('now'),
... form.Field(IOrder['now'], get_rendered=now),
... )
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
2002 12 2 12:30:00
Now try the same with the AddFormBase which uses a setUpInputWidget:
>>> class MyAddForm(form.AddFormBase):
... actions = ()
...
... def now(self):
... return datetime.datetime(2002, 12, 2, 12, 30)
...
... form_fields = form.Fields(
... form.Fields(IOrder).omit('now'),
... form.Field(IOrder['now'], get_rendered=now),
... )
...
... def setUpWidgets(self, ignore_request=True):
... super(MyAddForm, self).setUpWidgets(ignore_request)
>>> print MyAddForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Note that a EditForm can't make use of a get_rendered method. The get_rendered
method does only set initial values.
Note that the function passed must take a form as an argument. The
`setUpWidgets` function takes an optional 'form' argument, which
**must** be passed if any fields use the get_rendered option. The
form base classes always pass the form to `setUpWidgets`.
Advanced Usage Hints
====================
This section documents patterns for advanced usage of the formlib package.
Multiple button groups
----------------------
Multiple button groups can be accomplished many ways, but the way we've found
that reuses the most code is the following:
>>> class MyForm(form.Form):
... form_fields = form.Fields(IOrder)
... primary_actions = form.Actions()
... secondary_actions = form.Actions()
... # can use @zope.cachedescriptors.property.Lazy for performance
... def actions(self):
... return list(self.primary_actions) + list(self.secondary_actions)
... @form.action(u'Edit', primary_actions)
... def handle_edit_action(self, action, data):
... if form.applyChanges(self.context, self.form_fields, data):
... self.status = 'Object updated'
... else:
... self.status = 'No changes'
... @form.action(u'Submit for review...', secondary_actions)
... def handle_review_action(self, action, data):
... print "do something here"
...
The template then can render the button groups separately--something like the
following, for instance:
and
But the form machinery can still find the correct button. # TODO: demo
Dividing display of widget errors and invariant errors
------------------------------------------------------
Even though the form machinery only has a single errors attribute, if designers
wish to render widget errors differently than invariant errors, they can be
separated reasonably easily. The separation takes advantage of the fact that
all widget errors should implement zope.formlib.interfaces.IWidgetInputError,
and invariant errors shouldn't, because they don't come from a widget.
Therefore, a simple division such as the following should suffice.
# TODO
Omitting the form prefix
------------------------
For certain use cases (e.g. forms that post data to a different server whose
software you do not control) it is important to be able to generate forms
*without* a prefix. Using an empty string for the prefix omits it entirely.
>>> form_fields = form.Fields(IOrder).select('name')
>>> request = TestRequest()
>>> widgets = form.setUpWidgets(form_fields, '', None, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
Of course, getting the widget data still works.
>>> request.form['name'] = 'foo'
>>> widgets = form.setUpWidgets(form_fields, '', None, request)
>>> data = {}
>>> form.getWidgetsData(widgets, '', data)
[]
>>> data
{'name': u'foo'}
And the value from the request is also visible in the rendered form.
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
The same is true when using the other setup*Widgets helpers.
>>> widgets = form.setUpInputWidgets(form_fields, '', None, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
>>> order = Order(42)
>>> widgets = form.setUpEditWidgets(form_fields, '', order, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
>>> widgets = form.setUpDataWidgets(form_fields, '', None, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
Form actions have their own prefix in addition to the form prefix. This can be
suppressed for each action by passing the empty string as the 'prefix'
argument.
>>> class MyForm(form.Form):
...
... prefix = ''
... form_fields = form.Fields()
...
... @form.action('Button 1', name='button1')
... def handle_button1(self, action, data):
... self.status = 'Button 1 detected'
...
... @form.action('Button 2', prefix='', name='button2')
... def handle_button2(self, action, data):
... self.status = 'Button 2 detected'
...
>>> request = TestRequest()
>>> request.form['actions.button1'] = ''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Button 1 detected
>>> request = TestRequest()
>>> request.form['button2'] = ''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Button 2 detected
It is also possible to keep the form prefix and just suppress the 'actions' prefix.
>>> class MyForm(form.Form):
...
... form_fields = form.Fields()
...
... @form.action('Button', prefix='', name='button')
... def handle_button(self, action, data):
... self.status = 'Button detected'
...
>>> request = TestRequest()
>>> request.form['form.button'] = ''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Button detected
Additional Cases
================
Automatic Context Adaptation
----------------------------
As you may know already, the formlib will automatically adapt the context to
find a widget and data for a particular field. In an early version of
``zope.formlib``, it simply used ``field.interface`` to get the interface to
adapt to. Unfortunately, this call returns the interface the field has been
defined in and not the interface you got the field from. The following lines
demonstrate the correct behavior:
>>> import zope.interface
>>> import zope.schema
>>> class IFoo(zope.interface.Interface):
... title = zope.schema.TextLine()
>>> class IFooBar(IFoo):
... pass
Here is the unexpected behavior that caused formlib to do the wrong thing:
>>> IFooBar['title'].interface
Note: If this behavior ever changes, the formlib can be simplified again.
>>> @zope.interface.implementer(IFooBar)
... class FooBar(object):
... title = u'initial'
>>> foobar = FooBar()
>>> class Blah(object):
... def __conform__(self, iface):
... if iface is IFooBar:
... return foobar
>>> blah = Blah()
Let's now generate the form fields and instantiate the widgets:
>>> from zope.formlib import form
>>> form_fields = form.FormFields(IFooBar)
>>> request = TestRequest()
>>> widgets = form.setUpEditWidgets(form_fields, 'form', blah, request)
>>> print widgets.get('title')()
Here are some more places where the behavior was incorrect:
>>> widgets = form.setUpWidgets(form_fields, 'form', blah, request)
>>> print widgets.get('title')()
>>> form.checkInvariants(form_fields, {'title': 'new'}, blah)
[]
>>> form.applyChanges(blah, form_fields, {'title': 'new'})
True
Event descriptions
------------------
The ObjectModifiedEvent can be annotated with descriptions about the involved
schemas and fields. The formlib provides these annotations with the help of the
applyData function, which returns a list of modification descriptions:
>>> form.applyData(blah, form_fields, {'title': 'modified'})
{: ['title']}
The events are annotated with these descriptions. We need a subscriber to log these
infos:
>>> def eventLog(event):
... desc = event.descriptions[0]
... print 'Modified:', desc.interface.__identifier__, desc.attributes
>>> zope.event.subscribers.append(eventLog)
>>> class MyForm(form.EditForm):
... form_fields = form.FormFields(IFooBar)
>>> request = TestRequest()
>>> request.form['form.title'] = u'again modified'
>>> request.form['form.actions.apply'] = u''
>>> MyForm(FooBar(), request)()
Modified: __builtin__.IFooBar ('title',)
...
Cleanup:
>>> zope.event.subscribers.remove(eventLog)
Actions that cause a redirect
-----------------------------
When an action causes a redirect, the following `render` phase is omitted as
the result will not be displayed anyway. This is both a performance
improvement and for avoiding application bugs with one-time session
information.
>>> class MyForm(form.Form):
... form_fields = form.FormFields(IFooBar)
... @form.action("Redirect")
... def redirect(self, action, data):
... print 'Action: redirect'
... self.request.response.redirect('foo')
... @form.action("Stay")
... def redirect(self, action, data):
... print 'Action: stay'
... pass
... def render(self):
... print 'render was called'
... return ''
>>> request = TestRequest()
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
render was called
>>> request.form['form.actions.redirect'] = u''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Action: redirect
>>> request = TestRequest()
>>> request.form['form.actions.stay'] = u''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Action: stay
render was called
===============
Browser Widgets
===============
Formlib defines widgets: views on bound schema fields. Many of these
are straightforward. For instance, see the `TextWidget` in
textwidgets.py, which is a subclass of BrowserWidget in widget.py. It
is registered as an `IBrowserRequest` view of an `ITextLine` schema
field, providing the `IInputWidget` interface::
The widget then receives the field and the request as arguments to the factory
(i.e., the `TextWidget` class).
Some widgets in formlib extend this pattern. The widget registration
is extended for `Choice` fields and for the `collection` fields.
Default Choice Field Widget Registration and Lookup
===================================================
All field widgets are obtained by looking up a browser `IInputWidget`
or `IDisplayWidget` view for the field object. For `Choice` fields,
the default registered widget defers all of its behavior to the result
of another lookup: a browser widget view for the field *and* the
Choice field's vocabulary.
This allows registration of Choice widgets that differ on the basis of the
vocabulary type. For example, a widget for a vocabulary of images might have
a significantly different user interface than a widget for a vocabulary of
words. A dynamic vocabulary might implement `IIterableVocabulary` if its
contents are below a certain length, but not implement the marker "iterable"
interface if the number of possible values is above the threshhold.
This also means that choice widget factories are called with with an additional
argument. Rather than being called with the field and the request as
arguments, choice widgets receive the field, vocabulary, and request as
arguments.
Some `Choice` widgets may also need to provide a source interface,
particularly if the vocabulary is too big to iterate over.
Default Collection Field Widget Registration and Lookup
=======================================================
The default configured lookup for collection fields -- List, Tuple, and Set, for
instance -- begins with the usual lookup for a browser widget view for the
field object. This widget defers its display to the result of another lookup:
a browser widget view registered for the field and the field's `value_type`
(the type of the contained values). This allows registrations for collection
widgets that differ on the basis of the members -- a widget for entering a list
of text strings might differ significantly from a widget for entering a list of
dates...or even a list of choices, as discussed below.
This registration pattern has three implications that should be highlighted.
* First, collection fields that do not specify a `value_type` probably cannot
have a reasonable widget.
* Second, collection widgets that wish to be the default widget for a
collection with any `value_type` should be registered for the collection
field and a generic value_type: the `IField` interface. Do not register the
generic widget for the collection field only or you will break the lookup
behavior as described here.
* Third, like choice widget factories, sequence widget factories (classes or
functions) take three arguments. Typical sequence widgets receive the
field, the `value_type`, and the request as arguments.
Collections of Choices
----------------------
If a collection field's `value_type` is a `Choice` field, the second widget
again defers its behavior, this time to a third lookup based on the collection
field and the choice's vocabulary. This means that a widget for a list of
large image choices can be different than a widget for a list of small image
choices (with a different vocabulary interface), different from a widget for a
list of keyword choices, and different from a set of keyword choices.
Some advanced applications may wish to do a further lookup on the basis of the
unique attribute of the collection field--perhaps looking up a named view with
a "unique" or "lenient" token depending on the field's value, but this is not
enabled in the default Zope 3 configuration.
Registering Widgets for a New Collection Field Type
---------------------------------------------------
Because of this lookup pattern, basic widget registrations for new field types
must follow a recipe. For example, a developer may introduce a new Bag field
type for simple shopping cart functionality and wishes to add widgets for it
within the default Zope 3 collection widget registration. The bag widgets
should be registered something like this.
The only hard requirement is that the developer must register the bag + choice
widget: the widget is just the factory for the third dispatch as described
above, so the developer can use the already implemented widgets listed below::
Beyond this, the developer may also have a generic bag widget she wishes to
register. This might look something like this, assuming there's a
`BagSequenceWidget` available in this package::
Then any widgets for the bag and a vocabulary would be registered according to
this general pattern, in which `IIterableVocabulary` would be the interface of
any appropriate vocabulary and `BagWidget` is some appropriate widget::
Choice widgets and the missing value
====================================
Choice widgets for a non-required field include a "no value" item to allow for
not selecting any value at all. This value used to be omitted for required
fields on the assumption that the widget should avoid invalid input from the
start.
However, if the context object doesn't yet have a field value set and there's
no default value, a dropdown widget would have to select an arbitrary value
due to the way it is displayed in the browser. This way, the field would
always validate, but possibly with a value the user never chose consciously.
Starting with version zope.app.form 3.6.0, dropdown widgets for
required fields display a "no value" item even for required fields if
an arbitrary value would have to be selected by the widget otherwise.
To switch the old behaviour back on for backwards compatibility, do
zope.formlib.itemswidgets.EXPLICIT_EMPTY_SELECTION = False
during application start-up.
==============
Error handling
==============
These are a couple of functional tests that were written on-the-go ... In the
future this might become more extensive ...
Displaying invalidation errors
==============================
Validation errors, e.g. cause by invariants, are converted into readable text
by adapting them to IWidgetInputErrorView:
>>> from zope.publisher.browser import TestRequest
>>> from zope.interface.exceptions import Invalid
>>> from zope.component import getMultiAdapter
>>> from zope.formlib.interfaces import IWidgetInputErrorView
>>> error = Invalid("You are wrong!")
>>> message = getMultiAdapter((error, TestRequest()),
... IWidgetInputErrorView).snippet()
>>> message
u'You are wrong!'
Interface invariant methods raise zope.interface.Invalid exception. Test if
this exception gets handled by the error_views.
>>> myError = Invalid('My error message')
>>> import zope.formlib.form
>>> mybase = zope.formlib.form.FormBase(None, TestRequest())
>>> mybase.errors = (myError,)
>>> save = mybase.error_views()
>>> save.next()
u'My error message'
Now we need to set up the translation framework:
>>> from zope import component, interface
>>> from zope.i18n.interfaces import INegotiator
>>> @interface.implementer(INegotiator)
... class Negotiator:
... def getLanguage(*ignored): return 'test'
>>> component.provideUtility(Negotiator())
>>> from zope.i18n.testmessagecatalog import TestMessageFallbackDomain
>>> component.provideUtility(TestMessageFallbackDomain)
And yes, we can even handle an i18n message in an Invalid exception:
>>> from zope.i18nmessageid import MessageFactory
>>> _ = MessageFactory('my.domain')
>>> myError = Invalid(_('My i18n error message'))
>>> mybase = zope.formlib.form.FormBase(None, TestRequest())
>>> mybase.errors = (myError,)
>>> save = mybase.error_views()
>>> save.next()
u'[[my.domain][My i18n error message]]'
Displaying widget input errors
==============================
WidgetInputError exceptions also work with i18n messages:
>>> from zope.formlib.interfaces import WidgetInputError
>>> myError = WidgetInputError(
... field_name='summary',
... widget_title=_(u'Summary'),
... errors=_(u'Foo'))
>>> mybase = zope.formlib.form.FormBase(None, TestRequest())
>>> mybase.errors = (myError,)
>>> save = mybase.error_views()
>>> save.next()
u'[[my.domain][Summary]]: [[my.domain][Foo]]'
=======
Changes
=======
4.2.0 (2012-11-27)
==================
- LP #1017884: Add redirect status codes (303, 307) to the set which prevent
form rendering.
- Replaced deprecated ``zope.component.adapts`` usage with equivalent
``zope.component.adapter`` decorator.
- Replaced deprecated ``zope.interface.implements`` usage with equivalent
``zope.interface.implementer`` decorator.
- Dropped support for Python 2.5.
- Make separator of ``SourceSequenceDisplayWidget`` configurable.
4.1.1 (2012-03-16)
==================
- Added `ignoreContext` attribute to form classes to control whether
`checkInvariants` takes the context of the form into account when
checking interface invariants.
By default `ignoreContext` is set to ``False``. On the `AddForm` it is
``True`` by default because the context of this form is naturally not
suitable as context for the interface invariant.
4.1.0 (2012-03-15)
==================
- `checkInvariants` now takes the context of the form into account when
checking interface invariants.
- Tests are no longer compatible with Python 2.4.
4.0.6 (2011-08-20)
==================
- Fixed bug in ``orderedSelectionList.pt`` template.
4.0.5 (2010-09-16)
==================
- Fixed Action name parameter handling, since 4.0.3 all passed names were
lowercased.
4.0.4 (2010-07-06)
==================
- Fixed tests to pass under Python 2.7.
- Fix validation of "multiple" attributes in orderedSelectionList.pt.
4.0.3 (2010-05-06)
==================
- Keep Actions from raising exceptions when passed Unicode lables [LP:528468].
- Improve display of the "nothing selected" case for optional Choice fields
[LP:269782].
- Improve truth testing for ItemDisplayWidget [LP:159232].
- Don't blow up if TypeError raised during token conversion [LP:98491].
4.0.2 (2010-03-07)
==================
- Adapted tests for Python 2.4 (enforce sorting for short pprint output)
4.0.1 (2010-02-21)
==================
- Documentation uploaded to PyPI now contains widget documentation.
- Escape MultiCheckBoxWidget content [LP:302427].
4.0 (2010-01-08)
================
- Widget implementation and all widgets from zope.app.form have been
moved into zope.formlib, breaking zope.formlib's dependency on
zope.app.form (instead zope.app.form now depends on zope.formlib).
Widgets can all be imported from ``zope.formlib.widgets``.
Widget base classes and render functionality is in
``zope.formlib.widget``.
All relevant widget interfaces are now in ``zope.formlib.interfaces``.
3.10.0 (2009-12-22)
===================
- Use named template from zope.browserpage in favor of zope.app.pagetemplate.
3.9.0 (2009-12-22)
==================
- Use ViewPageTemplateFile from zope.browserpage.
3.8.0 (2009-12-22)
==================
- Adjusted test output to new zope.schema release.
3.7.0 (2009-12-18)
==================
- Rid ourselves from zope.app test dependencies.
- Fix: Button label needs escaping
3.6.0 (2009-05-18)
==================
- Remove deprecated imports.
- Remove dependency on zope.app.container (use ``IAdding`` from
``zope.browser.interfaces``) instead. Depend on
``zope.browser>=1.1`` (the version with ``IAdding``).
- Moved ``namedtemplate`` to ``zope.app.pagetemplate``, to cut some
dependencies on ``zope.formlib`` when using this feature. Left BBB
imports here.
3.5.2 (2009-02-21)
==================
- Adapt tests for Python 2.5 output.
3.5.1 (2009-01-31)
==================
- Adapt tests to upcoming zope.schema release 3.5.1.
3.5.0 (2009-01-26)
==================
New Features
------------
- Test dependencies are declared in a `test` extra now.
- Introduced ``zope.formlib.form.applyData`` which works like
``applyChanges`` but returns a dictionary with information about
which attribute of which schema changed. This information is then
sent along with the ``IObjectModifiedEvent``.
This fixes https://bugs.launchpad.net/zope3/+bug/98483.
Bugs Fixed
----------
- Actions that cause a redirect (301, 302) do not cause the `render` method to
be called anymore.
- The zope.formlib.form.Action class didn't fully implement
zope.formlib.interfaces.IAction.
- zope.formlib.form.setupWidgets and zope.formlib.form.setupEditWidgets did
not check for write access on the adapter but on context. This fixes
https://bugs.launchpad.net/zope3/+bug/219948
3.4.0 (2007-09-28)
==================
No further changes since 3.4.0a1.
3.4.0a1 (2007-04-22)
====================
Initial release as a separate project, corresponds to zope.formlib
from Zope 3.4.0a1
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Zope3
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Zope Public License
Classifier: License :: OSI Approved
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python
Classifier: Topic :: Internet :: WWW/HTTP
zope.formlib-4.2.0/README.txt 0000644 0000765 0000024 00000000326 12055134251 015711 0 ustar sweh staff 0000000 0000000 .. contents::
========
Overview
========
Forms are web components that use widgets to display and input data.
Typically a template displays the widgets by accessing an attribute or
method on an underlying class.
zope.formlib-4.2.0/setup.cfg 0000644 0000765 0000024 00000000073 12055141032 016026 0 ustar sweh staff 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
zope.formlib-4.2.0/setup.py 0000644 0000765 0000024 00000006632 12055136470 015740 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2006-2009 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
# This package is developed by the Zope Toolkit project, documented here:
# http://docs.zope.org/zopetoolkit
# When developing and releasing this package, please follow the documented
# Zope Toolkit policies as described by this documentation.
##############################################################################
"""Setup for zope.formlib package
"""
import os
from setuptools import setup, find_packages
def read(*rnames):
return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
version = '4.2.0'
setup(name='zope.formlib',
version=version,
url='http://pypi.python.org/pypi/zope.formlib',
license='ZPL 2.1',
description='Form generation and validation library for Zope',
author='Zope Foundation and Contributors',
author_email='zope-dev@zope.org',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Zope3',
'Intended Audience :: Developers',
'License :: OSI Approved :: Zope Public License',
'License :: OSI Approved',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP',
],
long_description=(read('README.txt')
+ '\n\n' +
read('src', 'zope', 'formlib', 'form.txt')
+ '\n\n' +
read('src', 'zope', 'formlib', 'widgets.txt')
+ '\n\n' +
read('src', 'zope', 'formlib', 'errors.txt')
+ '\n\n' +
read('CHANGES.txt')
),
packages=find_packages('src'),
package_dir = {'': 'src'},
namespace_packages=['zope',],
extras_require=dict(
test=['zope.configuration',
'zope.testing',
]
),
install_requires=['setuptools',
'pytz',
'zope.browser>=1.1',
'zope.browserpage>=3.11.0',
'zope.component',
'zope.event',
'zope.i18n',
'zope.i18nmessageid',
'zope.interface',
'zope.lifecycleevent',
'zope.publisher',
'zope.schema>=3.5.1',
'zope.security',
'zope.traversing',
"zope.datetime",
],
include_package_data = True,
zip_safe = False,
)
zope.formlib-4.2.0/src/ 0000755 0000765 0000024 00000000000 12055141032 014774 5 ustar sweh staff 0000000 0000000 zope.formlib-4.2.0/src/zope/ 0000755 0000765 0000024 00000000000 12055141032 015751 5 ustar sweh staff 0000000 0000000 zope.formlib-4.2.0/src/zope/__init__.py 0000644 0000765 0000024 00000000070 12055134250 020063 0 ustar sweh staff 0000000 0000000 __import__('pkg_resources').declare_namespace(__name__)
zope.formlib-4.2.0/src/zope/formlib/ 0000755 0000765 0000024 00000000000 12055141032 017403 5 ustar sweh staff 0000000 0000000 zope.formlib-4.2.0/src/zope/formlib/__init__.py 0000644 0000765 0000024 00000001145 12055134250 021521 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
zope.formlib-4.2.0/src/zope/formlib/boolwidgets.py 0000644 0000765 0000024 00000010244 12055134251 022305 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Browser widgets for items
"""
__docformat__ = 'restructuredtext'
from zope.schema.vocabulary import SimpleVocabulary
from zope.formlib.widget import SimpleInputWidget, renderElement
from zope.formlib.widget import DisplayWidget
from zope.formlib.i18n import _
from zope.formlib.itemswidgets import RadioWidget
from zope.formlib.itemswidgets import SelectWidget, DropdownWidget
class CheckBoxWidget(SimpleInputWidget):
"""A checkbox widget used to display Bool fields.
For more detailed documentation, including sample code, see
``tests/test_checkboxwidget.py``.
"""
type = 'checkbox'
default = 0
extra = ''
def __init__(self, context, request):
super(CheckBoxWidget, self).__init__(context, request)
self.required = False
def __call__(self):
"""Render the widget to HTML."""
value = self._getFormValue()
if value == 'on':
kw = {'checked': 'checked'}
else:
kw = {}
return "%s %s" % (
renderElement(self.tag,
type='hidden',
name=self.name+".used",
id=self.name+".used",
value=""
),
renderElement(self.tag,
type=self.type,
name=self.name,
id=self.name,
cssClass=self.cssClass,
extra=self.extra,
value="on",
**kw),
)
def _toFieldValue(self, input):
"""Convert from HTML presentation to Python bool."""
return input == 'on'
def _toFormValue(self, value):
"""Convert from Python bool to HTML representation."""
return value and 'on' or ''
def hasInput(self):
"""Check whether the field is represented in the form."""
return self.name + ".used" in self.request.form or \
super(CheckBoxWidget, self).hasInput()
def _getFormInput(self):
"""Returns the form input used by `_toFieldValue`.
Return values:
``'on'`` checkbox is checked
``''`` checkbox is not checked
``None`` form input was not provided
"""
if self.request.get(self.name) == 'on':
return 'on'
elif self.name + '.used' in self.request:
return ''
else:
return None
def BooleanRadioWidget(field, request, true=_('on'), false=_('off')):
vocabulary = SimpleVocabulary.fromItems( ((true, True), (false, False)) )
widget = RadioWidget(field, vocabulary, request)
widget.required = False
return widget
def BooleanSelectWidget(field, request, true=_('on'), false=_('off')):
vocabulary = SimpleVocabulary.fromItems( ((true, True), (false, False)) )
widget = SelectWidget(field, vocabulary, request)
widget.size = 2
widget.required = False
return widget
def BooleanDropdownWidget(field, request, true=_('on'), false=_('off')):
vocabulary = SimpleVocabulary.fromItems( ((true, True), (false, False)) )
widget = DropdownWidget(field, vocabulary, request)
widget.required = False
return widget
_msg_true = _("True")
_msg_false = _("False")
class BooleanDisplayWidget(DisplayWidget):
def __call__(self):
if self._renderedValueSet():
value = self._data
else:
value = self.context.default
if value:
return _msg_true
else:
return _msg_false
zope.formlib-4.2.0/src/zope/formlib/bugs.txt 0000644 0000765 0000024 00000003717 12055134250 021120 0 ustar sweh staff 0000000 0000000 Functional tests to verify bugs are gone
========================================
`setupWidgets` and `DISPLAY_UNWRITEABLE`
++++++++++++++++++++++++++++++++++++++++
This is to verify that bug #219948 is gone: setupWidgets doesn't check for
write access on the adapter.
Create a form containg two interfaces:
>>> import zope.formlib.tests
>>> class MyFormBase(object):
... form_fields = zope.formlib.form.FormFields(
... zope.formlib.tests.IOrder, zope.formlib.tests.IDescriptive,
... render_context=zope.formlib.interfaces.DISPLAY_UNWRITEABLE).omit(
... 'now')
>>> class MyEditForm(MyFormBase, zope.formlib.form.EditForm):
... pass
Instanciate the context objects and the form:
>>> import zope.publisher.browser
>>> request = zope.publisher.browser.TestRequest()
>>> order = zope.formlib.tests.Order()
>>> form = MyEditForm(order, request)
When we render the form, the fields of IDescriptive are read only because we
have no write access (this is configured in ftesting.zcml), the others are
writable[#needsinteraction]_:
>>> form.setUpWidgets()
>>> form.widgets['title']
>>> form.widgets['name']
Make sure we have the same behaviour for non-edit forms:
>>> class MyForm(MyFormBase, zope.formlib.form.Form):
... pass
>>> import zope.publisher.browser
>>> request = zope.publisher.browser.TestRequest()
>>> order = zope.formlib.tests.Order()
>>> form = MyForm(order, request)
>>> form.setUpWidgets()
>>> form.widgets['title']
>>> form.widgets['name']
Clean up:
>>> zope.security.management.endInteraction()
.. [#needsinteraction]
>>> import zope.security.management
>>> import zope.security.testing
>>> request.setPrincipal(zope.security.management.system_user)
>>> zope.security.management.newInteraction(request)
zope.formlib-4.2.0/src/zope/formlib/configure.zcml 0000644 0000765 0000024 00000046662 12055134250 022275 0 ustar sweh staff 0000000 0000000
zope.formlib-4.2.0/src/zope/formlib/errors.py 0000644 0000765 0000024 00000003345 12055134251 021303 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2006 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Error related things.
"""
__docformat__ = 'restructuredtext'
from cgi import escape
from zope.component import adapter
from zope.interface import implementer
from zope.interface import Invalid
from zope.i18n import Message
from zope.i18n import translate
from zope.formlib.interfaces import IWidgetInputErrorView
from zope.publisher.interfaces.browser import IBrowserRequest
@implementer(IWidgetInputErrorView)
@adapter(Invalid, IBrowserRequest)
class InvalidErrorView(object):
"""Display a validation error as a snippet of text."""
def __init__(self, context, request):
self.context = context
self.request = request
def snippet(self):
"""Convert a widget input error to an html snippet
>>> from zope.interface.exceptions import Invalid
>>> error = Invalid("You made an error!")
>>> InvalidErrorView(error, None).snippet()
u'You made an error!'
"""
msg = self.context.args[0]
if isinstance(msg, Message):
msg = translate(msg, context=self.request)
return u'%s' % escape(msg)
zope.formlib-4.2.0/src/zope/formlib/errors.txt 0000644 0000765 0000024 00000005150 12055134250 021465 0 ustar sweh staff 0000000 0000000 ==============
Error handling
==============
These are a couple of functional tests that were written on-the-go ... In the
future this might become more extensive ...
Displaying invalidation errors
==============================
Validation errors, e.g. cause by invariants, are converted into readable text
by adapting them to IWidgetInputErrorView:
>>> from zope.publisher.browser import TestRequest
>>> from zope.interface.exceptions import Invalid
>>> from zope.component import getMultiAdapter
>>> from zope.formlib.interfaces import IWidgetInputErrorView
>>> error = Invalid("You are wrong!")
>>> message = getMultiAdapter((error, TestRequest()),
... IWidgetInputErrorView).snippet()
>>> message
u'You are wrong!'
Interface invariant methods raise zope.interface.Invalid exception. Test if
this exception gets handled by the error_views.
>>> myError = Invalid('My error message')
>>> import zope.formlib.form
>>> mybase = zope.formlib.form.FormBase(None, TestRequest())
>>> mybase.errors = (myError,)
>>> save = mybase.error_views()
>>> save.next()
u'My error message'
Now we need to set up the translation framework:
>>> from zope import component, interface
>>> from zope.i18n.interfaces import INegotiator
>>> @interface.implementer(INegotiator)
... class Negotiator:
... def getLanguage(*ignored): return 'test'
>>> component.provideUtility(Negotiator())
>>> from zope.i18n.testmessagecatalog import TestMessageFallbackDomain
>>> component.provideUtility(TestMessageFallbackDomain)
And yes, we can even handle an i18n message in an Invalid exception:
>>> from zope.i18nmessageid import MessageFactory
>>> _ = MessageFactory('my.domain')
>>> myError = Invalid(_('My i18n error message'))
>>> mybase = zope.formlib.form.FormBase(None, TestRequest())
>>> mybase.errors = (myError,)
>>> save = mybase.error_views()
>>> save.next()
u'[[my.domain][My i18n error message]]'
Displaying widget input errors
==============================
WidgetInputError exceptions also work with i18n messages:
>>> from zope.formlib.interfaces import WidgetInputError
>>> myError = WidgetInputError(
... field_name='summary',
... widget_title=_(u'Summary'),
... errors=_(u'Foo'))
>>> mybase = zope.formlib.form.FormBase(None, TestRequest())
>>> mybase.errors = (myError,)
>>> save = mybase.error_views()
>>> save.next()
u'[[my.domain][Summary]]: [[my.domain][Foo]]'
zope.formlib-4.2.0/src/zope/formlib/exception.py 0000644 0000765 0000024 00000004304 12055134250 021760 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2003 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Form-related exception views
"""
__docformat__ = 'restructuredtext'
from cgi import escape
from zope.interface import implementer
from zope.i18n import translate
from zope.formlib.interfaces import IWidgetInputError, IWidgetInputErrorView
@implementer(IWidgetInputErrorView)
class WidgetInputErrorView(object):
"""Display an input error as a snippet of text."""
__used_for__ = IWidgetInputError
def __init__(self, context, request):
self.context, self.request = context, request
def snippet(self):
"""Convert a widget input error to an html snippet
>>> from zope.formlib.interfaces import WidgetInputError
>>> class TooSmallError(object):
... def doc(self):
... return "Foo input < 1"
>>> err = WidgetInputError("foo", "Foo", TooSmallError())
>>> view = WidgetInputErrorView(err, None)
>>> view.snippet()
u'Foo input < 1'
The only method that IWidgetInputError promises to implement is
`doc()`. Therefore, other implementations of the interface should also
work.
>>> from zope.formlib.interfaces import ConversionError
>>> err = ConversionError('Could not convert to float.')
>>> view = WidgetInputErrorView(err, None)
>>> view.snippet()
u'Could not convert to float.'
"""
message = self.context.doc()
translated = translate(message, context=self.request, default=message)
return u'%s' % escape(translated)
zope.formlib-4.2.0/src/zope/formlib/form.py 0000644 0000765 0000024 00000076174 12055134250 020743 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Forms
"""
import datetime
import re
import sys
import pytz
from cgi import escape
import zope.browser.interfaces
import zope.event
import zope.i18n
import zope.i18nmessageid
import zope.security
import zope.interface.interfaces
import zope.publisher.browser
import zope.publisher.interfaces.browser
from zope import component, interface, schema
from zope.browserpage import ViewPageTemplateFile
from zope.interface.common import idatetime
from zope.interface.interface import InterfaceClass
from zope.schema.interfaces import IField
from zope.schema.interfaces import ValidationError
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
from zope.lifecycleevent import Attributes
from zope.browserpage import namedtemplate
from zope.formlib.interfaces import IWidgetInputErrorView
from zope.formlib.interfaces import IInputWidget, IDisplayWidget
from zope.formlib.interfaces import WidgetsError, MissingInputError
from zope.formlib.interfaces import InputErrors, WidgetInputError
from zope.formlib import interfaces
from zope.i18nmessageid import MessageFactory
_ = MessageFactory("zope")
interface.moduleProvides(interfaces.IFormAPI)
def expandPrefix(prefix):
"""Expand prefix string by adding a trailing period if needed.
expandPrefix(p) should be used instead of p+'.' in most contexts.
"""
if prefix and not prefix.endswith('.'):
return prefix + '.'
return prefix
@interface.implementer(interfaces.IFormField)
class FormField:
def __init__(self, field, name=None, prefix='',
for_display=None, for_input=None, custom_widget=None,
render_context=False, get_rendered=None, interface=None
):
self.field = field
if name is None:
name = field.__name__
assert name
self.__name__ = expandPrefix(prefix) + name
self.prefix = prefix
if interface is None:
interface = field.interface
self.interface = interface
self.for_display = for_display
self.for_input = for_input
self.custom_widget = custom_widget
self.render_context = render_context
self.get_rendered = get_rendered
Field = FormField
def _initkw(keep_readonly=(), omit_readonly=False, **defaults):
return keep_readonly, omit_readonly, defaults
@interface.implementer(interfaces.IFormFields)
class FormFields(object):
def __init__(self, *args, **kw):
keep_readonly, omit_readonly, defaults = _initkw(**kw)
fields = []
for arg in args:
if isinstance(arg, InterfaceClass):
for name, field in schema.getFieldsInOrder(arg):
fields.append((name, field, arg))
elif IField.providedBy(arg):
name = arg.__name__
if not name:
raise ValueError(
"Field has no name")
fields.append((name, arg, arg.interface))
elif isinstance(arg, FormFields):
for form_field in arg:
fields.append(
(form_field.__name__,
form_field,
form_field.interface)
)
elif isinstance(arg, FormField):
fields.append((arg.__name__, arg, arg.interface))
else:
raise TypeError("Unrecognized argument type", arg)
seq = []
byname = {}
for name, field, iface in fields:
if isinstance(field, FormField):
form_field = field
else:
if field.readonly:
if omit_readonly and (name not in keep_readonly):
continue
form_field = FormField(field, interface=iface, **defaults)
name = form_field.__name__
if name in byname:
raise ValueError("Duplicate name", name)
seq.append(form_field)
byname[name] = form_field
self.__FormFields_seq__ = seq
self.__FormFields_byname__ = byname
def __len__(self):
return len(self.__FormFields_seq__)
def __iter__(self):
return iter(self.__FormFields_seq__)
def __getitem__(self, name):
return self.__FormFields_byname__[name]
def get(self, name, default=None):
return self.__FormFields_byname__.get(name, default)
def __add__(self, other):
if not isinstance(other, FormFields):
return NotImplemented
return self.__class__(self, other)
def select(self, *names):
"""Return a modified instance with an ordered subset of fields."""
return self.__class__(*[self[name] for name in names])
def omit(self, *names):
"""Return a modified instance omitting given fields."""
return self.__class__(*[ff for ff in self if ff.__name__ not in names])
Fields = FormFields
def fields_initkw(keep_all_readonly=False, **other):
return keep_all_readonly, other
# Backward compat
def fields(*args, **kw):
keep_all_readonly, other = fields_initkw(**kw)
other['omit_readonly'] = not keep_all_readonly
return FormFields(*args, **other)
@interface.implementer(interfaces.IWidgets)
class Widgets(object):
def __init__(self, widgets, prefix_length=None, prefix=None):
self.__Widgets_widgets_items__ = widgets
self.__Widgets_widgets_list__ = [w for (i, w) in widgets]
if prefix is None:
# BBB Allow old code using the prefix_length argument.
if prefix_length is None:
raise TypeError(
"One of 'prefix_length' and 'prefix' is required."
)
self.__Widgets_widgets_dict__ = dict(
[(w.name[prefix_length:], w) for (i, w) in widgets]
)
else:
prefix = expandPrefix(prefix)
self.__Widgets_widgets_dict__ = dict(
[(_widgetKey(w, prefix), w) for (i, w) in widgets]
)
def __iter__(self):
return iter(self.__Widgets_widgets_list__)
def __getitem__(self, name):
return self.__Widgets_widgets_dict__[name]
# TODO need test
def get(self, name):
return self.__Widgets_widgets_dict__.get(name)
def __iter_input_and_widget__(self):
return iter(self.__Widgets_widgets_items__)
# TODO need test
def __add__(self, other):
widgets = self.__class__([], 0)
widgets.__Widgets_widgets_items__ = (
self.__Widgets_widgets_items__ + other.__Widgets_widgets_items__)
widgets.__Widgets_widgets_list__ = (
self.__Widgets_widgets_list__ + other.__Widgets_widgets_list__)
widgets.__Widgets_widgets_dict__ = self.__Widgets_widgets_dict__.copy()
widgets.__Widgets_widgets_dict__.update(other.__Widgets_widgets_dict__)
return widgets
def canWrite(context, field):
writer = getattr(field, 'writer', None)
if writer is not None:
return zope.security.canAccess(context, writer.__name__)
return zope.security.canWrite(context, field.__name__)
def setUpWidgets(form_fields,
form_prefix=None, context=None, request=None, form=None,
data=(), adapters=None, ignore_request=False):
if request is None:
request = form.request
if context is None and form is not None:
context = form.context
if form_prefix is None:
form_prefix = form.prefix
widgets = []
adapter = None
for form_field in form_fields:
field = form_field.field
if form_field.render_context:
if adapters is None:
adapters = {}
# Adapt context, if necessary
interface = form_field.interface
adapter = adapters.get(interface)
if adapter is None:
if interface is None:
adapter = context
else:
adapter = interface(context)
adapters[interface] = adapter
if interface is not None:
adapters[interface.__name__] = adapter
field = field.bind(adapter)
else:
field = field.bind(context)
readonly = form_field.for_display
readonly = readonly or (field.readonly and not form_field.for_input)
readonly = readonly or (
(form_field.render_context & interfaces.DISPLAY_UNWRITEABLE)
and not canWrite(adapter, field)
)
if form_field.custom_widget is not None:
widget = form_field.custom_widget(field, request)
else:
if readonly:
widget = component.getMultiAdapter((field, request),
IDisplayWidget)
else:
widget = component.getMultiAdapter((field, request),
IInputWidget)
prefix = form_prefix
if form_field.prefix:
prefix = expandPrefix(prefix) + form_field.prefix
widget.setPrefix(prefix)
if ignore_request or readonly or not widget.hasInput():
# Get the value to render
if form_field.__name__ in data:
widget.setRenderedValue(data[form_field.__name__])
elif form_field.get_rendered is not None:
widget.setRenderedValue(form_field.get_rendered(form))
elif form_field.render_context:
widget.setRenderedValue(field.get(adapter))
else:
widget.setRenderedValue(field.default)
widgets.append((not readonly, widget))
return Widgets(widgets, prefix=form_prefix)
def setUpInputWidgets(form_fields, form_prefix, context, request,
form=None, ignore_request=False):
widgets = []
for form_field in form_fields:
field = form_field.field.bind(context)
widget = _createWidget(form_field, field, request, IInputWidget)
prefix = form_prefix
if form_field.prefix:
prefix = expandPrefix(prefix) + form_field.prefix
widget.setPrefix(prefix)
if ignore_request:
if form_field.get_rendered is not None:
value = form_field.get_rendered(form)
else:
value = field.default
widget.setRenderedValue(value)
widgets.append((True, widget))
return Widgets(widgets, prefix=form_prefix)
def _createWidget(form_field, field, request, iface):
if form_field.custom_widget is None:
return component.getMultiAdapter((field, request), iface)
else:
return form_field.custom_widget(field, request)
def getWidgetsData(widgets, form_prefix, data):
errors = []
form_prefix = expandPrefix(form_prefix)
for input, widget in widgets.__iter_input_and_widget__():
if input and IInputWidget.providedBy(widget):
name = _widgetKey(widget, form_prefix)
if not widget.hasInput():
continue
try:
data[name] = widget.getInputValue()
except ValidationError, error:
# convert field ValidationError to WidgetInputError
error = WidgetInputError(widget.name, widget.label, error)
errors.append(error)
except InputErrors, error:
errors.append(error)
return errors
def _widgetKey(widget, form_prefix):
name = widget.name
if name.startswith(form_prefix):
name = name[len(form_prefix):]
else:
raise ValueError("Name does not match prefix", name, form_prefix)
return name
def setUpEditWidgets(form_fields, form_prefix, context, request,
adapters=None, for_display=False,
ignore_request=False):
if adapters is None:
adapters = {}
widgets = []
for form_field in form_fields:
field = form_field.field
# Adapt context, if necessary
interface = form_field.interface
adapter = adapters.get(interface)
if adapter is None:
if interface is None:
adapter = context
else:
adapter = interface(context)
adapters[interface] = adapter
if interface is not None:
adapters[interface.__name__] = adapter
field = field.bind(adapter)
readonly = form_field.for_display
readonly = readonly or (field.readonly and not form_field.for_input)
readonly = readonly or (
(form_field.render_context & interfaces.DISPLAY_UNWRITEABLE)
and not canWrite(adapter, field)
)
readonly = readonly or for_display
if readonly:
iface = IDisplayWidget
else:
iface = IInputWidget
widget = _createWidget(form_field, field, request, iface)
prefix = form_prefix
if form_field.prefix:
prefix = expandPrefix(prefix) + form_field.prefix
widget.setPrefix(prefix)
if ignore_request or readonly or not widget.hasInput():
# Get the value to render
value = field.get(adapter)
widget.setRenderedValue(value)
widgets.append((not readonly, widget))
return Widgets(widgets, prefix=form_prefix)
def setUpDataWidgets(form_fields, form_prefix, context, request, data=(),
for_display=False, ignore_request=False):
widgets = []
for form_field in form_fields:
field = form_field.field.bind(context)
readonly = for_display or field.readonly or form_field.for_display
if readonly:
iface = IDisplayWidget
else:
iface = IInputWidget
widget = _createWidget(form_field, field, request, iface)
prefix = form_prefix
if form_field.prefix:
prefix = expandPrefix(prefix) + form_field.prefix
widget.setPrefix(prefix)
if ((form_field.__name__ in data)
and (ignore_request or readonly or not widget.hasInput())
):
widget.setRenderedValue(data[form_field.__name__])
widgets.append((not readonly, widget))
return Widgets(widgets, prefix=form_prefix)
class NoInputData(interface.Invalid):
"""There was no input data because:
- It wasn't asked for
- It wasn't entered by the user
- It was entered by the user, but the value entered was invalid
This exception is part of the internal implementation of checkInvariants.
"""
class FormData:
def __init__(self, schema, data, context):
self._FormData_data___ = data
self._FormData_schema___ = schema
self._FormData_context___ = context
def __getattr__(self, name):
schema = self._FormData_schema___
data = self._FormData_data___
context = self._FormData_context___
try:
field = schema[name]
except KeyError:
raise AttributeError(name)
else:
value = data.get(name, data)
if value is data:
if context is None:
raise NoInputData(name)
# The value is not in the form look it up on the context:
field = schema[name]
adapted_context = schema(context)
if IField.providedBy(field):
value = field.get(adapted_context)
elif (zope.interface.interfaces.IAttribute.providedBy(field)
and
not zope.interface.interfaces.IMethod.providedBy(field)):
# Fallback for non-field schema contents:
value = getattr(adapted_context, name)
else:
# Don't know how to extract value
raise NoInputData(name)
if zope.interface.interfaces.IMethod.providedBy(field):
if not IField.providedBy(field):
raise RuntimeError(
"Data value is not a schema field", name)
v = lambda: value
else:
v = value
setattr(self, name, v)
return v
raise AttributeError(name)
def checkInvariants(form_fields, form_data, context):
# First, collect the data for the various schemas
schema_data = {}
for form_field in form_fields:
schema = form_field.interface
if schema is None:
continue
data = schema_data.get(schema)
if data is None:
data = schema_data[schema] = {}
if form_field.__name__ in form_data:
data[form_field.field.__name__] = form_data[form_field.__name__]
# Now validate the individual schemas
errors = []
for schema, data in schema_data.items():
try:
schema.validateInvariants(FormData(schema, data, context), errors)
except interface.Invalid:
pass # Just collect the errors
return [error for error in errors if not isinstance(error, NoInputData)]
def applyData(context, form_fields, data, adapters=None):
if adapters is None:
adapters = {}
descriptions = {}
for form_field in form_fields:
field = form_field.field
# Adapt context, if necessary
interface = form_field.interface
adapter = adapters.get(interface)
if adapter is None:
if interface is None:
adapter = context
else:
adapter = interface(context)
adapters[interface] = adapter
name = form_field.__name__
newvalue = data.get(name, form_field) # using form_field as marker
if (newvalue is not form_field) \
and (field.get(adapter) != newvalue):
descriptions.setdefault(interface, []).append(field.__name__)
field.set(adapter, newvalue)
return descriptions
def applyChanges(context, form_fields, data, adapters=None):
return bool(applyData(context, form_fields, data, adapters))
def _callify(meth):
"""Return method if it is callable,
otherwise return the form's method of the name"""
if callable(meth):
return meth
elif isinstance(meth, str):
return lambda form, *args: getattr(form, meth)(*args)
@interface.implementer(interfaces.IAction)
class Action(object):
_identifier = re.compile('[A-Za-z][a-zA-Z0-9_]*$')
def __init__(self, label, success=None, failure=None,
condition=None, validator=None, prefix='actions',
name=None, data=None):
self.label = label
self.setPrefix(prefix)
self.setName(name)
self.bindMethods(success_handler=success,
failure_handler=failure,
condition=condition,
validator=validator)
if data is None:
data = {}
self.data = data
def bindMethods(self, **methods):
"""Bind methods to the action"""
for k, v in methods.items():
setattr(self, k, _callify(v))
def setName(self, name):
"""Make sure name is ASCIIfiable.
Use action label if name is None
"""
if name is None:
name = self.label
if self._identifier.match(name):
name = name.lower()
else:
if isinstance(name, unicode):
name = name.encode("utf-8")
name = name.encode('hex')
self.name = name
self.__name__ = self.prefix + name
def setPrefix(self, prefix):
"""Set prefix"""
self.prefix = expandPrefix(prefix)
def __get__(self, form, class_=None):
if form is None:
return self
result = self.__class__.__new__(self.__class__)
result.__dict__.update(self.__dict__)
result.form = form
result.__name__ = expandPrefix(form.prefix) + result.__name__
interface.alsoProvides(result, interfaces.IBoundAction)
return result
def available(self):
condition = self.condition
return (condition is None) or condition(self.form, self)
def validate(self, data):
if self.validator is not None:
return self.validator(self.form, self, data)
def success(self, data):
if self.success_handler is not None:
return self.success_handler(self.form, self, data)
def failure(self, data, errors):
if self.failure_handler is not None:
return self.failure_handler(self.form, self, data, errors)
def submitted(self):
return (self.__name__ in self.form.request.form) and self.available()
def update(self):
pass
render = namedtemplate.NamedTemplate('render')
@namedtemplate.implementation(interfaces.IAction)
def render_submit_button(self):
if not self.available():
return ''
label = self.label
if isinstance(label, zope.i18nmessageid.Message):
label = zope.i18n.translate(self.label, context=self.form.request)
return ('' %
(self.__name__, self.__name__, escape(label, quote=True))
)
class action:
def __init__(self, label, actions=None, **options):
caller_locals = sys._getframe(1).f_locals
if actions is None:
actions = caller_locals.get('actions')
if actions is None:
actions = caller_locals['actions'] = Actions()
self.actions = actions
self.label = label
self.options = options
def __call__(self, success):
action = Action(self.label, success=success, **self.options)
self.actions.append(action)
return action
@interface.implementer(interfaces.IActions)
class Actions(object):
def __init__(self, *actions):
self.actions = actions
self.byname = dict([(a.__name__, a) for a in actions])
def __iter__(self):
return iter(self.actions)
def __getitem__(self, name):
try:
return self.byname[name]
except TypeError:
if isinstance(name, slice):
return self.__class__(
*self.actions[name.start:name.stop:name.step]
)
def append(self, action):
self.actions += (action, )
self.byname[action.__name__] = action
# TODO need test
def __add__(self, other):
return self.__class__(*(self.actions + other.actions))
def copy(self):
return self.__class__(*self.actions)
def __get__(self, inst, class_):
if inst is None:
return self
return self.__class__(*[a.__get__(inst) for a in self.actions])
def handleSubmit(actions, data, default_validate=None):
for action in actions:
if action.submitted():
errors = action.validate(data)
if errors is None and default_validate is not None:
errors = default_validate(action, data)
return errors, action
return None, None
# TODO need test for this
def availableActions(form, actions):
result = []
for action in actions:
condition = action.condition
if (condition is None) or condition(form, action):
result.append(action)
return result
@interface.implementer(interfaces.IForm)
class FormBase(zope.publisher.browser.BrowserPage):
label = u''
prefix = 'form'
status = ''
errors = ()
ignoreContext = False
def setPrefix(self, prefix):
self.prefix = prefix
def setUpWidgets(self, ignore_request=False):
self.adapters = {}
self.widgets = setUpWidgets(
self.form_fields, self.prefix, self.context, self.request,
form=self, adapters=self.adapters, ignore_request=ignore_request)
def validate(self, action, data):
if self.ignoreContext:
context = None
else:
context = self.context
return (getWidgetsData(self.widgets, self.prefix, data)
+ checkInvariants(self.form_fields, data, context))
template = namedtemplate.NamedTemplate('default')
# TODO also need to be able to show disabled actions
def availableActions(self):
return availableActions(self, self.actions)
def resetForm(self):
self.setUpWidgets(ignore_request=True)
form_result = None
form_reset = True
def update(self):
self.setUpWidgets()
self.form_reset = False
data = {}
errors, action = handleSubmit(self.actions, data, self.validate)
# the following part will make sure that previous error not
# get overriden by new errors. This is usefull for subforms. (ri)
if self.errors is None:
self.errors = errors
else:
if errors is not None:
self.errors += tuple(errors)
if errors:
self.status = _('There were errors')
result = action.failure(data, errors)
elif errors is not None:
self.form_reset = True
result = action.success(data)
else:
result = None
self.form_result = result
def render(self):
# if the form has been updated, it will already have a result
if self.form_result is None:
if self.form_reset:
# we reset, in case data has changed in a way that
# causes the widgets to have different data
self.resetForm()
self.form_reset = False
self.form_result = self.template()
return self.form_result
def __call__(self):
self.update()
if self.request.response.getStatus() in [301, 302, 303, 307]:
# Avoid rendering if the action caused a redirect.
result = self.form_result or ''
else:
result = self.render()
return result
def error_views(self):
for error in self.errors:
if isinstance(error, basestring):
yield error
else:
view = component.getMultiAdapter(
(error, self.request),
IWidgetInputErrorView)
title = getattr(error, 'widget_title', None) # duck typing
if title:
if isinstance(title, zope.i18n.Message):
title = zope.i18n.translate(title, context=self.request)
yield '%s: %s' % (title, view.snippet())
else:
yield view.snippet()
def haveInputWidgets(form, action):
for input, widget in form.widgets.__iter_input_and_widget__():
if input:
return True
else:
return False
class EditFormBase(FormBase):
def setUpWidgets(self, ignore_request=False):
self.adapters = {}
self.widgets = setUpEditWidgets(
self.form_fields, self.prefix, self.context, self.request,
adapters=self.adapters, ignore_request=ignore_request
)
@action(_("Apply"), condition=haveInputWidgets)
def handle_edit_action(self, action, data):
descriptions = applyData(self.context, self.form_fields, data,
self.adapters)
if descriptions:
descriptions = [Attributes(interface, *tuple(keys))
for interface, keys in descriptions.items()]
zope.event.notify(ObjectModifiedEvent(self.context, *descriptions))
formatter = self.request.locale.dates.getFormatter(
'dateTime', 'medium')
try:
time_zone = idatetime.ITZInfo(self.request)
except TypeError:
time_zone = pytz.UTC
status = _("Updated on ${date_time}",
mapping={'date_time':
formatter.format(
datetime.datetime.now(time_zone)
)
}
)
self.status = status
else:
self.status = _('No changes')
class DisplayFormBase(FormBase):
def setUpWidgets(self, ignore_request=False):
self.adapters = {}
self.widgets = setUpEditWidgets(
self.form_fields, self.prefix, self.context, self.request,
adapters=self.adapters, for_display=True,
ignore_request=ignore_request
)
actions = ()
@interface.implementer(interfaces.IAddFormCustomization,
zope.component.interfaces.IFactory)
@component.adapter(zope.browser.interfaces.IAdding,
zope.publisher.interfaces.browser.IBrowserRequest)
class AddFormBase(FormBase):
ignoreContext = True
def __init__(self, context, request):
self.__parent__ = context
super(AddFormBase, self).__init__(context, request)
def setUpWidgets(self, ignore_request=False):
self.widgets = setUpInputWidgets(
self.form_fields, self.prefix, self.context, self.request,
ignore_request=ignore_request,
)
@action(_("Add"), condition=haveInputWidgets)
def handle_add(self, action, data):
self.createAndAdd(data)
# zope.formlib.interfaces.IAddFormCustomization
def createAndAdd(self, data):
ob = self.create(data)
zope.event.notify(ObjectCreatedEvent(ob))
return self.add(ob)
def create(self, data):
raise NotImplementedError(
"concrete classes must implement create() or createAndAdd()")
_finished_add = False
def add(self, object):
ob = self.context.add(object)
self._finished_add = True
return ob
def render(self):
if self._finished_add:
self.request.response.redirect(self.nextURL())
return ""
return super(AddFormBase, self).render()
def nextURL(self):
return self.context.nextURL()
default_page_template = namedtemplate.NamedTemplateImplementation(
ViewPageTemplateFile('pageform.pt'), interfaces.IPageForm)
default_subpage_template = namedtemplate.NamedTemplateImplementation(
ViewPageTemplateFile('subpageform.pt'), interfaces.ISubPageForm)
@interface.implementer(interfaces.IPageForm)
class PageForm(FormBase):
pass
Form = PageForm
@interface.implementer(interfaces.IPageForm)
class PageEditForm(EditFormBase):
pass
EditForm = PageEditForm
@interface.implementer(interfaces.IPageForm)
class PageDisplayForm(DisplayFormBase):
pass
DisplayForm = PageDisplayForm
@interface.implementer(interfaces.IPageForm)
class PageAddForm(AddFormBase):
pass
AddForm = PageAddForm
@interface.implementer(interfaces.ISubPageForm)
class SubPageForm(FormBase):
pass
@interface.implementer(interfaces.ISubPageForm)
class SubPageEditForm(EditFormBase):
pass
@interface.implementer(interfaces.ISubPageForm)
class SubPageDisplayForm(DisplayFormBase):
pass
zope.formlib-4.2.0/src/zope/formlib/form.txt 0000644 0000765 0000024 00000210447 12055134250 021123 0 ustar sweh staff 0000000 0000000 =====
Forms
=====
Forms are web components that use widgets to display and input data.
Typically a template displays the widgets by accessing an attribute or
method on an underlying class.
This document describes some tools to assist in form development. In
the examples, we will show "forms" that are generated with simple
print statements to keep the examples simpler. Most forms will use
templates in practice.
This document starts with low-level APIs. We eventually build up to
higher-level APIs that allow forms to be defined with just a little bit
of meta data. Impatient readers may wish to skip to the later
sections, especially the section on `Helpful base classes`_. :)
A form class can define ordered collections of "form fields" using
the `Fields` constructor. Form fields are distinct from and build on
schema fields. A schema field specified attribute values. Form
fields specify how a schema field should be used in a form. The
simplest way to define a collection of form fields is by passing a
schema to the `Fields` constructor:
>>> from zope import interface, schema
>>> class IOrder(interface.Interface):
... identifier = schema.Int(title=u"Identifier", readonly=True)
... name = schema.TextLine(title=u"Name")
... min_size = schema.Float(title=u"Minimum size")
... max_size = schema.Float(title=u"Maximum size")
... color = schema.TextLine(title=u"Color", required=False)
... now = schema.Datetime(title=u"Now", readonly=True)
>>> from zope.formlib import form
>>> class MyForm:
... form_fields = form.Fields(IOrder)
This sets up a set of form fields from the interface, IOrder.
>>> len(MyForm.form_fields)
6
>>> [w.__name__ for w in MyForm.form_fields]
['identifier', 'name', 'min_size', 'max_size', 'color', 'now']
We can access individual form fields by name:
>>> MyForm.form_fields['name'].__name__
'name'
We can also select and order subsets using the select method of form fields:
>>> [w.__name__ for w in MyForm.form_fields.select('name', 'identifier')]
['name', 'identifier']
or by omitting fields:
>>> [w.__name__ for w in MyForm.form_fields.omit('now', 'identifier')]
['name', 'min_size', 'max_size', 'color']
We can omit read-only fields using the omit_readonly option when
setting up the fields:
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
>>> [w.__name__ for w in MyForm.form_fields]
['name', 'min_size', 'max_size', 'color']
Getting HTML
============
Having defined form fields, we can use them to generate HTML
forms. Typically, this is done at run time by form class
instances. Let's look at an example that displays some input widgets:
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self, ignore_request=False):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request,
... ignore_request=ignore_request)
... return '\n'.join([w() for w in widgets])
Here we used ``form.setUpWidgets`` to create widget instances from our
form-field specifications. The second argument to ``setUpWidgets`` is a
form prefix. All of the widgets on this form are given the same
prefix. This allows multiple forms to be used within a single form
tag, assuming that each form uses a different form prefix.
Now, we can display the form:
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
If the request contains any form data, that will be reflected in the
output:
>>> request.form['form.name'] = u'bob'
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Sometimes we don't want this behavior: we want to ignore the request values,
particularly after a form has been processed and before it is drawn again.
This can be accomplished with the 'ignore_request' argument in
setUpWidgets.
>>> print MyForm(None, request)(ignore_request=True)
... # doctest: +NORMALIZE_WHITESPACE
Reading data
============
Of course, we don't just want to display inputs. We want to get the
input data. We can use getWidgetsData for that:
>>> from pprint import pprint
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... if 'submit' in self.request:
... data = {}
... errors = form.getWidgetsData(widgets, 'form', data)
... if errors:
... print 'There were errors:'
... for error in errors:
... print error
... else:
... data = None
...
... for w in widgets:
... print w()
... error = w.error()
... if error:
... print error
...
... return data
We check for a 'submit' variable in the form and, if we see it, we try
to get the data, and errors. We call `getWidgetsData`, passing:
- Our widgets
- The form prefix, and
- A data dictionary to contain input values found
The keys in the data dictionary have the form prefix stripped off.
If there are errors, we print them. When we display the widgets, we
also check for errors and show them if present. Let's add a submit
variable:
>>> request.form['form.min_size'] = u''
>>> request.form['form.max_size'] = u''
>>> request.form['submit'] = u'Submit'
>>> MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
There were errors:
('min_size', u'Minimum size', RequiredMissing('min_size'))
('max_size', u'Maximum size', RequiredMissing('max_size'))
Required input is missing.Required input is missing.
{'name': u'bob'}
Note that we got an error because we omitted the values for min_size
and max size. If we provide an invalid value, we'll get an error too:
>>> request.form['form.min_size'] = u'bob'
>>> MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
There were errors:
(u'Invalid floating point data',
)
('max_size', u'Maximum size', RequiredMissing('max_size'))
Invalid floating point dataRequired input is missing.
{'name': u'bob'}
If we provide valid data, we'll get the data back:
>>> request.form['form.min_size'] = u'42'
>>> request.form['form.max_size'] = u'142'
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
It's up to the form to decide what to do with the information.
Invariants
==========
The `getWidgetsData` function checks individual field constraints.
Interfaces can also provide invariants that we may also want to check.
The `checkInvariants` function can be used to do that.
In our order example, it makes sense to require that the maximum is
greater than or equal to the minimum:
>>> class IOrder(interface.Interface):
... identifier = schema.Int(title=u"Identifier", readonly=True)
... name = schema.TextLine(title=u"Name")
... min_size = schema.Float(title=u"Minimum size")
... max_size = schema.Float(title=u"Maximum size")
... now = schema.Datetime(title=u"Now", readonly=True)
...
... @interface.invariant
... def maxGreaterThanMin(order):
... if order.max_size < order.min_size:
... raise interface.Invalid("Maximum is less than Minimum")
We can update our form to check the invariant using 'checkInvariants':
>>> class MyForm:
... form_fields = form.Fields(IOrder, omit_readonly=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... if 'submit' in self.request:
... data = {}
... errors = form.getWidgetsData(widgets, 'form', data)
... invariant_errors = form.checkInvariants(
... self.form_fields, data, self.context)
... if errors:
... print 'There were field errors:'
... for error in errors:
... print error
...
... if invariant_errors:
... print 'There were invariant errors:'
... for error in invariant_errors:
... print error
... else:
... data = None
...
... for w in widgets:
... print w()
... error = w.error()
... if error:
... print error
...
... return data
If we display the form again, we'll get the same result:
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
But if we reduce the maximum below the minimum, we'll get an invariant
error:
>>> request.form['form.min_size'] = u'42'
>>> request.form['form.max_size'] = u'14'
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
There were invariant errors:
Maximum is less than Minimum
{'max_size': 14.0,
'min_size': 42.0,
'name': u'bob'}
We can have field errors and invariant errors:
>>> request.form['form.name'] = u''
>>> pprint(MyForm(None, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
There were field errors:
('name', u'Name', RequiredMissing('name'))
There were invariant errors:
Maximum is less than Minimum
Required input is missing.
{'max_size': 14.0,
'min_size': 42.0}
If the inputs for some fields tested by invariants are missing, the
invariants are ignored:
>>> request.form['form.max_size'] = u''
>>> pprint(MyForm(None, request)()) # doctest: +NORMALIZE_WHITESPACE
There were field errors:
('name', u'Name', RequiredMissing('name'))
('max_size', u'Maximum size', RequiredMissing('max_size'))
Required input is missing.Required input is missing.
{'min_size': 42.0}
Edit Forms
==========
A common application of forms is edit forms. Edit forms are special
in 2 ways:
- We want to get the initial data for widgets from the object being
edited.
- If there are no errors, we want to apply the changes back to the
object being edited.
The form package provides some functions to assist with creating edit
forms. When we set up our form_fields, we use the `render_context`
option, which uses data from the context passed to setUpWidgets.
Let's create a content class that provides `IOrder` and a simple form
that uses it:
>>> import datetime
>>> @interface.implementer(IOrder)
... class Order:
...
... def __init__(self, identifier):
... self.identifier = identifier
... self.name = 'unknown'
... self.min_size = 0.0
... self.max_size = 0.0
...
... now = property(lambda self: datetime.datetime.now())
>>> order = Order(1)
>>> class MyForm:
... form_fields = form.Fields(
... IOrder, omit_readonly=True, render_context=True)
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self, ignore_request=False):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request,
... ignore_request=ignore_request)
...
... return '\n'.join([w() for w in widgets])
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
Note that, in this case, we got the values from the request, because
we used an old request. If we want to redraw the form after processing a
request, it is safest to pass ignore_request = True to setUpWidgets so that
the form is redrawn with the values as found in the object, not on the request.
>>> print MyForm(order, request)(ignore_request=True)
... # doctest: +NORMALIZE_WHITESPACE
If we use a new request, we will of course get the same result:
>>> request = TestRequest()
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
If we include read-only fields in an edit form, they will get display widgets:
>>> class MyForm:
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... return '\n'.join([w() for w in widgets])
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
1
When the form is submitted, we need to apply the changes back to the
object. We can use the `applyChanges` function for that:
>>> class MyForm:
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def __call__(self):
... widgets = form.setUpWidgets(
... self.form_fields, 'form', self.context, self.request)
...
... if 'submit' in self.request:
... data = {}
... errors = form.getWidgetsData(widgets, 'form', data)
... invariant_errors = form.checkInvariants(
... self.form_fields, data, self.context)
... if errors:
... print 'There were field errors:'
... for error in errors:
... print error
...
... if invariant_errors:
... print 'There were invariant errors:'
... for error in invariant_errors:
... print error
...
... if not errors and not invariant_errors:
... changed = form.applyChanges(
... self.context, self.form_fields, data)
...
... else:
... data = changed = None
...
... for w in widgets:
... print w()
... error = w.error()
... if error:
... print error
...
... if changed:
... print 'Object updated'
... else:
... print 'No changes'
...
... return data
Now, if we submit the form with some data:
>>> request.form['form.name'] = u'bob'
>>> request.form['form.min_size'] = u'42'
>>> request.form['form.max_size'] = u'142'
>>> request.form['submit'] = u''
>>> pprint(MyForm(order, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
1
Object updated
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
>>> order.name
u'bob'
>>> order.max_size
142.0
>>> order.min_size
42.0
Note, however, that if we submit the same request, we'll see that no
changes were applied:
>>> pprint(MyForm(order, request)(), width=1)
... # doctest: +NORMALIZE_WHITESPACE
1
No changes
{'max_size': 142.0,
'min_size': 42.0,
'name': u'bob'}
because the new and old values are the same.
The code we included in `MyForm` above is generic: it applies to any
edit form.
Actions
=======
Our commit logic is a little complicated. It would be far more
complicated if there were multiple submit buttons.
We can use action objects to provide some distribution of application logic.
An action is an object that represents a handler for a submit button.
In the most common case, an action accepts a label and zero or more options
provided as keyword parameters:
condition
A callable or name of a method to call to test whether the action is
applicable. if the value is a method name, then the method will be
passed the action when called, otherwise, the callable will be
passed the form and the action.
validator
A callable or name of a method to call to validate and collect
inputs. This is called only if the action was submitted and if the
action either has no condition, or the condition evaluates to a true
value. If the validator is provided as a method name, the method
will be called with the action and a dictionary in which to save data.
If the validator is provided as a callable, the callable will be
called with the form, the action, and a dictionary in which to save data.
The validator normally returns a (usually empty) list of widget
input errors. It may also return None to behave as if the action
wasn't submitted.
success
A handler, called when the the action was submitted and there are no
validation errors. The handler may be provided as either a callable
or a method name. If the handler is provided as a method name, the
method will be called with the action and a dictionary containing the
form data. If the success handler is provided as a callable, the
callable will be called with the form, the action, and a dictionary
containing the data. The handler may return a form result
(e.g. page), or may return None to indicate that the form should
generate it's own output.
failure
A handler, called when the the action was submitted and there are
validation errors. The handler may be provided as either a callable
or a method name. If the handler is provided as a method name, the
method will be called with the action, a dictionary containing the form
data, and a list of errors. If the failure handler is provided as a
callable, the callable will be called with the form, the action, a
dictionary containing the data, and a list of errors. The handler
may return a form result (e.g. page), or may return None to indicate
that the form should generate it's own output.
prefix
A form prefix for the action. When generating submit actions, the
prefix should be combined with the action name, separating the two
with a dot. The default prefix is "actions"form.
name
The action name, without a prefix. If the label is a valid Python
identifier, then the lower-case label will be used, otherwise, a hex encoding
of the label will be used. If for some strange reason the labels in
a set of actions with the same prefix is not unique, a name will
have to be given for some actions to get unique names.
data
A bag of extra information that can be used by handlers, validators,
or conditions.
Let's update our edit form to use an action. We are also going to
rearrange our form quite a bit to make things more modular:
- We've created a separate `validation` method to validate inputs and
compute errors.
- We've created a `handle_edit_action` method for applying changes.
- We've created a template method for displaying the form.
Normally, this would be a ZPT template, but we just provide a Python
version here.
- We've created a call method that is described below
- We've defined a number of instance attributes for passing
information between the various methods:
- `status` is a string that, if set, is displayed at the top of the
form.
- `errors` is the set of errors found when validating.
- `widgets` is a list of set-up widgets
Here's the new version:
>>> class MyForm:
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... status = errors = None
... prefix = 'form'
...
... actions = form.Actions(
... form.Action('Edit', success='handle_edit_action'),
... )
...
... def __init__(self, context, request):
... self.context, self.request = context, request
...
... def validate(self, action, data):
... return (form.getWidgetsData(self.widgets, self.prefix, data) +
... form.checkInvariants(
... self.form_fields, data, self.context))
...
... def handle_edit_action(self, action, data):
... if form.applyChanges(self.context, self.form_fields, data):
... self.status = 'Object updated'
... else:
... self.status = 'No changes'
...
... def template(self):
... if self.status:
... print self.status
...
... result = []
...
... if self.errors:
... result.append('There were errors:')
... for error in self.errors:
... result.append(str(error))
...
... for w in self.widgets:
... result.append(w())
... error = w.error()
... if error:
... result.append(str(error))
...
... for action in self.actions:
... result.append(action.render())
...
... return '\n'.join(result)
...
... def __call__(self):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request)
...
... data = {}
... errors, action = form.handleSubmit(
... self.actions, data, self.validate)
... self.errors = errors
...
... if errors:
... result = action.failure(data, errors)
... elif errors is not None:
... result = action.success(data)
... else:
... result = None
...
... if result is None:
... result = self.template()
...
... return result
Lets walk through the `__call__` method.
- We set up our widgets as before.
- We use `form.handleSubmit` to validate our data. We pass the form,
actions, prefix, and `validate` method. For each action,
`form.handleSubmit` checks to see if the action was submitted. If the
action was submitted, it checks to see if it has a validator. If
the action has a validator, the action's validator is called,
otherwise the validator passed is called. The validator result (a
list of widget input errors) and the action are returned. If no
action was submitted, then `None` is returned for the errors and the
action.
- If a action was submitted and there were no errors, we call the
success method on the action. If the action has a handler defined,
it will be called and the return value is returned, otherwise None
is returned. A return value of None indicates that the form should
generate it's own result.
- If a action was submitted but there were errors, we call the
action's failure method. If the action has a failure handler
defined, it will be called and the return value is returned,
otherwise None is returned. A return value of None indicates that
the form should generate it's own result.
- No action was submitted, the result is set to None.
- If we don't have a result, we generate one with our template.
Let's try the new version of our form:
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
1
In this case, we didn't get any output about changes because the
request form data didn't include a submit action that matched our
action definition. Let's add one and try again:
>>> request.form['form.actions.edit'] = u''
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
No changes
1
This time, we got a status message indicating that there weren't any
changes.
Let's try changing some data:
>>> request.form['form.max_size'] = u'10/0'
>>> print MyForm(order, request)()
... # doctest: +NORMALIZE_WHITESPACE
There were errors:
(u'Invalid floating point data', )
1
Invalid floating point data
Oops, we had a typo, let's fix it:
>>> request.form['form.max_size'] = u'10.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
There were errors:
Maximum is less than Minimum
1
Oh yeah, we need to reduce the minimum too: :)
>>> request.form['form.min_size'] = u'1.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
Object updated
1
Ah, much better. And our order has been updated:
>>> order.max_size
10.0
>>> order.min_size
1.0
Helpful base classes
====================
Our form has a lot of repetitive code. A number of helpful base
classes provide standard form implementation.
Form
----
The `Form` base class provides a number of common attribute definitions.
It provides:
`__init__`
A constructor
`validate`
A default validation method
`__call__`
To render the form
`template`
A default template. Note that this is a NamedTemplate named "default",
so the template may also be overridden by registering an alternate
default template.
`prefix`
A string added to all widget and action names.
`setPrefix`
method for changing the prefix
`availableActions`
method for getting available actions
`adapters`
Dictionary of objects implementing each given schema
Subclasses need to:
- Provide a form_fields variable containing a list of form fields
- a actions attribute containing a list of action definitions
Subclasses may:
- Provide a label function or message id to produce
a form label.
- Override the setUpWidgets method to control how widgets are
set up. This is fairly rarely needed.
- Override the template. The form defines variables:
status
providing a short summary of the operation performed.
widgets
A collection of widgets, which can be accessed through iteration
or by name
errors
A (possibly empty) list of errors
Let's update our example to use the base class:
>>> class MyForm(form.Form):
... form_fields = form.Fields(IOrder, render_context=True)
... form_fields = form_fields.omit('now')
...
... @form.action("Edit", failure='handle_edit_action_failure')
... def handle_edit_action(self, action, data):
... if form.applyChanges(self.context, self.form_fields, data):
... self.status = 'Object updated'
... else:
... self.status = 'No changes'
...
... def handle_edit_action_failure(self, action, data, errors):
... self.status = 'There were %d errors.' % len(errors)
We inherited most of our behavior from the base class.
We also used the `action` decorator. The action decorator:
- creates an `actions` variable if one isn't already created,
- defines an action with the given label and any other arguments, and
- appends the action to the `actions` list.
The `action` decorator accepts the same arguments as the `Action`
class with the exception of the `success` option.
The creation of the `actions` is a bit magic, but provides
simplification in common cases.
Now we can try out our form:
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
No changes
1
>>> request.form['form.min_size'] = u'20.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
There were 1 errors.
Invalid: Maximum is less than Minimum
1
>>> request.form['form.max_size'] = u'30.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
Object updated
1
>>> order.max_size
30.0
>>> order.min_size
20.0
EditForm
--------
Our `handle_edit_action` action is common to edit forms. An
`EditForm` base class captures this commonality. It also sets up
widget widgets a bit differently. The `EditForm` base class sets up
widgets as if the form fields had been set up with the `render_context`
option.
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder)
... form_fields = form_fields.omit('now')
>>> request.form['form.actions.apply'] = u''
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
No changes
1
>>> request.form['form.min_size'] = u'40.0'
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
There were errors
Invalid: Maximum is less than Minimum
1
>>> request.form['form.max_size'] = u'50.0'
>>> print MyForm(order, request)()
... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Updated on ... ... ... ...:...:...
1
>>> order.max_size
50.0
>>> order.min_size
40.0
Note that `EditForm` shows the date and time when content are
modified.
Multiple Schemas and Adapters
=============================
Forms can use fields from multiple schemas. This can be done in a
number of ways. For example, multiple schemas can be passed to
`form.Fields`:
>>> class IDescriptive(interface.Interface):
... title = schema.TextLine(title=u"Title")
... description = schema.TextLine(title=u"Description")
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder, IDescriptive)
... form_fields = form_fields.omit('now')
In addition, if the the object being edited doesn't provide any of the
schemas, it will be adapted to the schemas it doesn't provide.
Suppose we have a generic adapter for storing descriptive information
on objects:
>>> from zope import component
>>> @component.adapter(interface.Interface)
... @interface.implementer(IDescriptive)
... class Descriptive(object):
... def __init__(self, context):
... self.context = context
...
... def title():
... def get(self):
... try:
... return self.context.__title
... except AttributeError:
... return ''
... def set(self, v):
... self.context.__title = v
... return property(get, set)
... title = title()
...
... def description():
... def get(self):
... try:
... return self.context.__description
... except AttributeError:
... return ''
... def set(self, v):
... self.context.__description = v
... return property(get, set)
... description = description()
>>> component.provideAdapter(Descriptive)
Now, we can use a single form to edit both the regular order data and
the descriptive data:
>>> request = TestRequest()
>>> print MyForm(order, request)() # doctest: +NORMALIZE_WHITESPACE
1
>>> request.form['form.name'] = u'bob'
>>> request.form['form.min_size'] = u'10.0'
>>> request.form['form.max_size'] = u'20.0'
>>> request.form['form.title'] = u'Widgets'
>>> request.form['form.description'] = u'Need more widgets'
>>> request.form['form.actions.apply'] = u''
>>> myform = MyForm(order, request)
>>> print myform()
... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
Updated on ... ... ... ...:...:...
1
>>> order.min_size
10.0
>>> order.title
Traceback (most recent call last):
...
AttributeError: Order instance has no attribute 'title'
>>> Descriptive(order).title
u'Widgets'
Often, we'd like to get at the adapters used. If `EditForm` is used,
the adapters are available in the adapters attribute, which is a
dictionary that allows adapters to be looked up by by schema or schema
name:
>>> myform.adapters[IOrder].__class__.__name__
'Order'
>>> myform.adapters['IOrder'].__class__.__name__
'Order'
>>> myform.adapters[IDescriptive].__class__.__name__
'Descriptive'
>>> myform.adapters['IDescriptive'].__class__.__name__
'Descriptive'
If you aren't using `EditForm`, you can get a dictionary populated in
the same way by `setUpWidgets` by passing the dictionary as an
`adapters` keyword argument.
Named Widget Access
===================
The value returned from `setUpWidgets` supports named-based lookup as well as
iteration:
>>> myform.widgets['name'].__class__.__name__
'TextWidget'
>>> myform.widgets['name'].name
'form.name'
>>> myform.widgets['title'].__class__.__name__
'TextWidget'
>>> myform.widgets['title'].name
'form.title'
Form-field manipulations
========================
The form-field constructor is very flexible. We've already seen that
we can supply multiple schemas. Here are some other things you can
do.
Specifying individual fields
----------------------------
You can specify individual fields for a form. Here, we'll create a
form that collects just the name from `IOrder` and the title from
`IDescriptive`:
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder['name'],
... IDescriptive['title'])
... actions = ()
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
You can also use stand-alone fields:
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(
... schema.TextLine(__name__='name', title=u"Who?"),
... IDescriptive['title'],
... )
... actions = ()
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
But make sure the fields have a '__name__', as was done above.
Concatenating field collections
-------------------------------
It is sometimes convenient to combine multiple field collections.
Field collections support concatenation. For example, we may want to
combine field definitions:
>>> class MyExpandedForm(form.Form):
... form_fields = (
... MyForm.form_fields
... +
... form.Fields(IDescriptive['description'])
... )
... actions = ()
>>> print MyExpandedForm(order, TestRequest())()
... # doctest: +NORMALIZE_WHITESPACE
Using fields for display
------------------------
Normally, any writable fields get input widgets. We may want to
indicate that some fields should be used for display only. We can do
this using the `for_display` option when setting up form_fields:
>>> class MyForm(form.EditForm):
... form_fields = (
... form.Fields(IOrder, for_display=True).select('name')
... +
... form.Fields(IOrder).select('min_size', 'max_size')
... )
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
bob
Note that if all of the fields in an edit form are for display:
>>> class MyForm(form.EditForm):
... form_fields = form.Fields(IOrder, for_display=True
... ).select('name', 'min_size', 'max_size')
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
bob
10.0
20.0
we don't get an edit action. This is because the edit action defined
by `EditForm` has a condition to prevent it's use when there are no
input widgets. Check it out for an example of using action conditions.
Using fields for input
----------------------
We may want to indicate that some fields should be used for input even
if the underlying schema field is read-only. We can do this using the
`for_input` option when setting up form_fields:
>>> class MyForm(form.Form):
... form_fields = form.Fields(IOrder, for_input=True,
... render_context=True)
... form_fields = form_fields.omit('now')
...
... actions = ()
>>> print MyForm(order, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
Displaying or editing raw data
==============================
Sometimes, you want to display or edit data that doesn't come from an
object. One way to do this is to pass the data to setUpWidgets.
Lets look at an example:
>>> class MyForm(form.Form):
...
... form_fields = form.Fields(IOrder)
... form_fields = form_fields.omit('now')
...
... actions = ()
...
... def setUpWidgets(self, ignore_request=False):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request,
... data=dict(identifier=42, name=u'sally'),
... ignore_request=ignore_request
... )
In this case, we supplied initial data for the identifier and the
name. Now if we display the form, we'll see our data and defaults for
the fields we didn't supply data for:
>>> print MyForm(None, TestRequest())() # doctest: +NORMALIZE_WHITESPACE
42
If data are passed in the request, they override initial data for
input fields:
>>> request = TestRequest()
>>> request.form['form.name'] = u'fred'
>>> request.form['form.identifier'] = u'0'
>>> request.form['form.max_size'] = u'100'
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
42
We'll get display fields if we ask for display fields when setting up
our form fields:
>>> class MyForm(form.Form):
...
... form_fields = form.Fields(IOrder, for_display=True)
... form_fields = form_fields.omit('now')
...
... actions = ()
...
... def setUpWidgets(self, ignore_request=False):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request,
... data=dict(identifier=42, name=u'sally'),
... ignore_request=ignore_request
... )
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
42
sally
Note that we didn't get data from the request because we are using all
display widgets.
Passing `ignore_request=True` to the `setUpWidgets` function ignores
the request for all values passed in the data dictionary, in order to
help with redrawing a form after a successful action handler. We'll
fake that quickly by forcing ignore_request to be `True`.
>>> class MyForm(form.Form):
...
... form_fields = form.Fields(IOrder)
... form_fields = form_fields.omit('now')
...
... actions = ()
...
... def setUpWidgets(self, ignore_request=False):
... self.widgets = form.setUpWidgets(
... self.form_fields, self.prefix, self.context, self.request,
... data=dict(identifier=42, name=u'sally'),
... ignore_request=True # =ignore_request
... )
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
42
Specifying Custom Widgets
=========================
It is possible to use custom widgets for specific fields. This can be
done for a variety of reasons, but the provided mechanism should work
for any of them.
Custom widgets are specified by providing a widget factory that should
be used instead of the registered field view. The factory will be
called in the same way as any other field view factory, with the bound
field and the request as arguments.
Let's create a simple custom widget to use in our demonstration::
>>> import zope.formlib.widget
>>> class ISODisplayWidget(zope.formlib.widget.DisplayWidget):
...
... def __call__(self):
... return '2005-05-04'
To set the custom widget factory for a field, assign to the
`custom_widget` attribute of the form field object::
>>> class MyForm(form.Form):
... actions = ()
...
... form_fields = form.Fields(IOrder).select("now")
...
... # Here we set the custom widget:
...
... form_fields["now"].custom_widget = ISODisplayWidget
>>> print MyForm(None, request)()
2005-05-04
Specifying Fields individually
------------------------------
All of the previous examples set up fields as collections. We can
also set up forms individually and pass them to the Fields
constructor. This is especially useful for passing options that
really only apply to a single field. The previous example can be
written more simply as:
>>> class MyForm(form.Form):
... actions = ()
...
... form_fields = form.Fields(
... form.Field(IOrder['now'], custom_widget=ISODisplayWidget),
... )
>>> print MyForm(None, request)()
2005-05-04
Computing default values
------------------------
We saw earlier that we could provide initial widget data by passing a
dictionary to setUpWidgets. We can also supply a function or method
name when we set up form fields.
We might like to include the `now` field in our forms. We can provide
a function for getting the needed initial value:
>>> import datetime
>>> class MyForm(form.Form):
... actions = ()
...
... def now(self):
... return datetime.datetime(2002, 12, 2, 12, 30)
...
... form_fields = form.Fields(
... form.Fields(IOrder).omit('now'),
... form.Field(IOrder['now'], get_rendered=now),
... )
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
2002 12 2 12:30:00
Now try the same with the AddFormBase which uses a setUpInputWidget:
>>> class MyAddForm(form.AddFormBase):
... actions = ()
...
... def now(self):
... return datetime.datetime(2002, 12, 2, 12, 30)
...
... form_fields = form.Fields(
... form.Fields(IOrder).omit('now'),
... form.Field(IOrder['now'], get_rendered=now),
... )
...
... def setUpWidgets(self, ignore_request=True):
... super(MyAddForm, self).setUpWidgets(ignore_request)
>>> print MyAddForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Note that a EditForm can't make use of a get_rendered method. The get_rendered
method does only set initial values.
Note that the function passed must take a form as an argument. The
`setUpWidgets` function takes an optional 'form' argument, which
**must** be passed if any fields use the get_rendered option. The
form base classes always pass the form to `setUpWidgets`.
Advanced Usage Hints
====================
This section documents patterns for advanced usage of the formlib package.
Multiple button groups
----------------------
Multiple button groups can be accomplished many ways, but the way we've found
that reuses the most code is the following:
>>> class MyForm(form.Form):
... form_fields = form.Fields(IOrder)
... primary_actions = form.Actions()
... secondary_actions = form.Actions()
... # can use @zope.cachedescriptors.property.Lazy for performance
... def actions(self):
... return list(self.primary_actions) + list(self.secondary_actions)
... @form.action(u'Edit', primary_actions)
... def handle_edit_action(self, action, data):
... if form.applyChanges(self.context, self.form_fields, data):
... self.status = 'Object updated'
... else:
... self.status = 'No changes'
... @form.action(u'Submit for review...', secondary_actions)
... def handle_review_action(self, action, data):
... print "do something here"
...
The template then can render the button groups separately--something like the
following, for instance:
and
But the form machinery can still find the correct button. # TODO: demo
Dividing display of widget errors and invariant errors
------------------------------------------------------
Even though the form machinery only has a single errors attribute, if designers
wish to render widget errors differently than invariant errors, they can be
separated reasonably easily. The separation takes advantage of the fact that
all widget errors should implement zope.formlib.interfaces.IWidgetInputError,
and invariant errors shouldn't, because they don't come from a widget.
Therefore, a simple division such as the following should suffice.
# TODO
Omitting the form prefix
------------------------
For certain use cases (e.g. forms that post data to a different server whose
software you do not control) it is important to be able to generate forms
*without* a prefix. Using an empty string for the prefix omits it entirely.
>>> form_fields = form.Fields(IOrder).select('name')
>>> request = TestRequest()
>>> widgets = form.setUpWidgets(form_fields, '', None, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
Of course, getting the widget data still works.
>>> request.form['name'] = 'foo'
>>> widgets = form.setUpWidgets(form_fields, '', None, request)
>>> data = {}
>>> form.getWidgetsData(widgets, '', data)
[]
>>> data
{'name': u'foo'}
And the value from the request is also visible in the rendered form.
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
The same is true when using the other setup*Widgets helpers.
>>> widgets = form.setUpInputWidgets(form_fields, '', None, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
>>> order = Order(42)
>>> widgets = form.setUpEditWidgets(form_fields, '', order, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
>>> widgets = form.setUpDataWidgets(form_fields, '', None, request)
>>> print widgets['name']() # doctest: +NORMALIZE_WHITESPACE
Form actions have their own prefix in addition to the form prefix. This can be
suppressed for each action by passing the empty string as the 'prefix'
argument.
>>> class MyForm(form.Form):
...
... prefix = ''
... form_fields = form.Fields()
...
... @form.action('Button 1', name='button1')
... def handle_button1(self, action, data):
... self.status = 'Button 1 detected'
...
... @form.action('Button 2', prefix='', name='button2')
... def handle_button2(self, action, data):
... self.status = 'Button 2 detected'
...
>>> request = TestRequest()
>>> request.form['actions.button1'] = ''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Button 1 detected
>>> request = TestRequest()
>>> request.form['button2'] = ''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Button 2 detected
It is also possible to keep the form prefix and just suppress the 'actions' prefix.
>>> class MyForm(form.Form):
...
... form_fields = form.Fields()
...
... @form.action('Button', prefix='', name='button')
... def handle_button(self, action, data):
... self.status = 'Button detected'
...
>>> request = TestRequest()
>>> request.form['form.button'] = ''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Button detected
Additional Cases
================
Automatic Context Adaptation
----------------------------
As you may know already, the formlib will automatically adapt the context to
find a widget and data for a particular field. In an early version of
``zope.formlib``, it simply used ``field.interface`` to get the interface to
adapt to. Unfortunately, this call returns the interface the field has been
defined in and not the interface you got the field from. The following lines
demonstrate the correct behavior:
>>> import zope.interface
>>> import zope.schema
>>> class IFoo(zope.interface.Interface):
... title = zope.schema.TextLine()
>>> class IFooBar(IFoo):
... pass
Here is the unexpected behavior that caused formlib to do the wrong thing:
>>> IFooBar['title'].interface
Note: If this behavior ever changes, the formlib can be simplified again.
>>> @zope.interface.implementer(IFooBar)
... class FooBar(object):
... title = u'initial'
>>> foobar = FooBar()
>>> class Blah(object):
... def __conform__(self, iface):
... if iface is IFooBar:
... return foobar
>>> blah = Blah()
Let's now generate the form fields and instantiate the widgets:
>>> from zope.formlib import form
>>> form_fields = form.FormFields(IFooBar)
>>> request = TestRequest()
>>> widgets = form.setUpEditWidgets(form_fields, 'form', blah, request)
>>> print widgets.get('title')()
Here are some more places where the behavior was incorrect:
>>> widgets = form.setUpWidgets(form_fields, 'form', blah, request)
>>> print widgets.get('title')()
>>> form.checkInvariants(form_fields, {'title': 'new'}, blah)
[]
>>> form.applyChanges(blah, form_fields, {'title': 'new'})
True
Event descriptions
------------------
The ObjectModifiedEvent can be annotated with descriptions about the involved
schemas and fields. The formlib provides these annotations with the help of the
applyData function, which returns a list of modification descriptions:
>>> form.applyData(blah, form_fields, {'title': 'modified'})
{: ['title']}
The events are annotated with these descriptions. We need a subscriber to log these
infos:
>>> def eventLog(event):
... desc = event.descriptions[0]
... print 'Modified:', desc.interface.__identifier__, desc.attributes
>>> zope.event.subscribers.append(eventLog)
>>> class MyForm(form.EditForm):
... form_fields = form.FormFields(IFooBar)
>>> request = TestRequest()
>>> request.form['form.title'] = u'again modified'
>>> request.form['form.actions.apply'] = u''
>>> MyForm(FooBar(), request)()
Modified: __builtin__.IFooBar ('title',)
...
Cleanup:
>>> zope.event.subscribers.remove(eventLog)
Actions that cause a redirect
-----------------------------
When an action causes a redirect, the following `render` phase is omitted as
the result will not be displayed anyway. This is both a performance
improvement and for avoiding application bugs with one-time session
information.
>>> class MyForm(form.Form):
... form_fields = form.FormFields(IFooBar)
... @form.action("Redirect")
... def redirect(self, action, data):
... print 'Action: redirect'
... self.request.response.redirect('foo')
... @form.action("Stay")
... def redirect(self, action, data):
... print 'Action: stay'
... pass
... def render(self):
... print 'render was called'
... return ''
>>> request = TestRequest()
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
render was called
>>> request.form['form.actions.redirect'] = u''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Action: redirect
>>> request = TestRequest()
>>> request.form['form.actions.stay'] = u''
>>> print MyForm(None, request)() # doctest: +NORMALIZE_WHITESPACE
Action: stay
render was called
zope.formlib-4.2.0/src/zope/formlib/ftesting.zcml 0000644 0000765 0000024 00000001230 12055134250 022115 0 ustar sweh staff 0000000 0000000
zope.formlib-4.2.0/src/zope/formlib/i18n.py 0000644 0000765 0000024 00000000225 12055134251 020540 0 ustar sweh staff 0000000 0000000 """\
I18N support for zope.formlib
"""
__docformat__ = "reStructuredText"
import zope.i18nmessageid
_ = zope.i18nmessageid.MessageFactory("zope")
zope.formlib-4.2.0/src/zope/formlib/interfaces.py 0000644 0000765 0000024 00000104363 12055134250 022113 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Form interfaces
"""
import re
from zope import schema
from zope.publisher.interfaces.browser import IBrowserPage
from zope.schema.interfaces import ValidationError
from zope.publisher.interfaces import IView
from zope.interface import Attribute, Interface, implementer, Invalid
from zope.schema import Bool
from zope.exceptions.interfaces import UserError
class IWidgetInputError(Interface):
"""Placeholder for a snippet View"""
def doc():
"""Returns a string that represents the error message."""
@implementer(IWidgetInputError)
class WidgetInputError(UserError):
"""One or more user input errors occurred."""
def __init__(self, field_name, widget_title, errors=None):
"""Initialize Error
`errors` is a ``ValidationError`` or a list of ValidationError objects
"""
UserError.__init__(self, field_name, widget_title, errors)
self.field_name = field_name
self.widget_title = widget_title
self.errors = errors
def doc(self):
# TODO this duck typing is to get the code working. See
# collector issue 372
if isinstance(self.errors, basestring):
return self.errors
elif getattr(self.errors, 'doc', None) is not None:
return self.errors.doc()
return ''
class MissingInputError(WidgetInputError):
"""Required data was not supplied."""
@implementer(IWidgetInputError)
class ConversionError(Exception):
"""A conversion error occurred."""
def __init__(self, error_name, original_exception=None):
Exception.__init__(self, error_name, original_exception)
self.error_name = error_name
self.original_exception = original_exception
def doc(self):
return self.error_name
InputErrors = WidgetInputError, ValidationError, ConversionError
class ErrorContainer(Exception):
"""A base error class for collecting multiple errors."""
def append(self, error):
self.args += (error, )
def __len__(self):
return len(self.args)
def __iter__(self):
return iter(self.args)
def __getitem__(self, i):
return self.args[i]
def __str__(self):
return "\n".join(
["%s: %s" % (error.__class__.__name__, error)
for error in self.args]
)
__repr__ = __str__
class WidgetsError(ErrorContainer):
"""A collection of errors from widget processing.
widgetValues is a map containing the list of values that were obtained
from the widgets, keyed by field name.
"""
def __init__(self, errors, widgetsData={}):
ErrorContainer.__init__(self, *errors)
self.widgetsData = widgetsData
class IWidget(IView):
"""Generically describes the behavior of a widget.
Note that this level must be still presentation independent.
"""
name = Attribute(
"""The unique widget name
This must be unique within a set of widgets.""")
label = Attribute(
"""The widget label.
Label may be translated for the request.
The attribute may be implemented as either a read-write or read-only
property, depending on the requirements for a specific implementation.
""")
hint = Attribute(
"""A hint regarding the use of the widget.
Hints are traditionally rendered using tooltips in GUIs, but may be
rendered differently depending on the UI implementation.
Hint may be translated for the request.
The attribute may be implemented as either a read-write or read-only
property, depending on the requirements for a specific implementation.
""")
visible = Attribute(
"""A flag indicating whether or not the widget is visible.""")
def setRenderedValue(value):
"""Set the value to be rendered by the widget.
Calling this method will override any values provided by the user.
For input widgets (`IInputWidget` implementations), calling
this sets the value that will be rendered even if there is
already user input.
"""
def setPrefix(prefix):
"""Set the name prefix used for the widget
The widget name is used to identify the widget's data within
input data. For example, for HTTP forms, the widget name is
used for the form key.
It is acceptable to *reset* the prefix: set it once to read
values from the request, and again to redraw with a different
prefix but maintained state.
"""
class IInputWidget(IWidget):
"""A widget for editing a field value."""
required = Bool(
title=u"Required",
description=u"""If True, widget should be displayed as requiring input.
By default, this value is the field's 'required' attribute. This
field can be set to False for widgets that always provide input (e.g.
a checkbox) to avoid unnecessary 'required' UI notations.
""")
def getInputValue():
"""Return value suitable for the widget's field.
The widget must return a value that can be legally assigned to
its bound field or otherwise raise ``WidgetInputError``.
The return value is not affected by `setRenderedValue()`.
"""
def applyChanges(content):
"""Validate the user input data and apply it to the content.
Return a boolean indicating whether a change was actually applied.
This raises an error if there is no user input.
"""
def hasInput():
"""Returns ``True`` if the widget has input.
Input is used by the widget to calculate an 'input value', which is
a value that can be legally assigned to a field.
Note that the widget may return ``True``, indicating it has input, but
still be unable to return a value from `getInputValue`. Use
`hasValidInput` to determine whether or not `getInputValue` will return
a valid value.
A widget that does not have input should generally not be used
to update its bound field. Values set using
`setRenderedValue()` do not count as user input.
A widget that has been rendered into a form which has been
submitted must report that it has input. If the form
containing the widget has not been submitted, the widget
shall report that it has no input.
"""
def hasValidInput():
"""Returns ``True`` is the widget has valid input.
This method is similar to `hasInput` but it also confirms that the
input provided by the user can be converted to a valid field value
based on the field constraints.
"""
class IDisplayWidget(IWidget):
"""A widget for displaying a field value."""
required = Bool(
title=u"Required",
description=u"""If True, widget should be displayed as requiring input.
Display widgets should never be required.
""")
class IWidgetFactory(Interface):
"""A factory that creates the widget"""
def __call__(context, request):
"""Return a widget"""
class FormError(Exception):
"""There was an error in managing the form
"""
class IBrowserWidget(IWidget):
"""A widget for use in a web browser UI."""
def __call__():
"""Render the widget."""
def hidden():
"""Render the widget as a hidden field."""
def error():
"""Render the validation error for the widget, or return
an empty string if no error"""
class ISimpleInputWidget(IBrowserWidget, IInputWidget):
"""A widget that uses a single HTML element to collect user input."""
tag = schema.TextLine(
title=u'Tag',
description=u'The widget HTML element.')
type = schema.TextLine(
title=u'Type',
description=u'The element type attribute',
required=False)
cssClass = schema.TextLine(
title=u'CSS Class',
description=u'The element class attribute.',
required=False)
extra = schema.TextLine(
title=u'Extra',
description=u'The element extra attribute.',
required=False)
class ITextBrowserWidget(ISimpleInputWidget):
convert_missing_value = schema.Bool(
title=u'Translate Input Value',
description=
u'If True, an empty string is converted to field.missing_value.',
default=True)
# XXX this seems to be buggy (since at least 2005)
# it returns None, and prefix_re is never defined.
def reConstraint(pat, explanation):
pat = re.compile(pat)
def constraint(value):
if prefix_re.match(value):
return True
raise Invalid(value, explanation)
class IWidgetInputErrorView(Interface):
"""Display an input error as a snippet of text."""
def snippet():
"""Convert a widget input error to an html snippet."""
class ISourceQueryView(Interface):
"""View support for querying non-iterable sources
"""
def render(name):
"""Return a rendering of the search form elements
The query view should use `name` as the prefix for its widgets.
"""
def results(name):
"""Return the results of the query
The query view should use `name` as the prefix for its widgets.
The value returned is an iterable.
None may be returned to indicate that there are no results.
"""
class ISubPage(Interface):
"""A component that computes part of a page
"""
def update():
"""Update content ot view information based on user input
"""
def render():
"""Render the sub page, returning a unicode string
"""
prefix = schema.ASCII(
constraint=reConstraint(
'[a-zA-Z][a-zA-Z0-9_]*([.][a-zA-Z][a-zA-Z0-9_]*)*',
"Must be a sequence of not-separated identifiers"),
description=u"""Page-element prefix
All named or identified page elements in a subpage should have
names and identifiers that begin with a subpage prefix
followed by a dot.
""",
readonly=True,
)
def setPrefix(prefix):
"""Update the subpage prefix
"""
class IFormAPI(Interface):
"""API to facilitate creating forms, provided by zope.formlib.form
"""
def Field(schema_field, **options):
"""Define a form field from a schema field and usage options
The following options are supported:
name
Provide a name to use for the field.
prefix
The form-field prefix.
for_display
A flag indicating whether the form-field is to be used
for display. See IFormField.
for_input
A flag indicating whether the form-field is to be used
for input. See IFormField.
custom_widget
Factory to use for widget construction. See IFormField.
render_context
A flag indicating whether the default value to render
should come from the form context. See IFormField.
get_rendered
A callable or form method name to be used to get a default
rendered value. See IFormField.
"""
def Fields(*arguments, **options):
"""Create form-fields collection (IFormFields)
Creates a form-field collection from a collection of:
- Schemas
- Schema fields
- form fields (IFormField)
- form-field collections (IFormFields)
An IFormFields is returned.
The following options are supported:
name
Provide a name to use for the field.
prefix
The form-field prefix for new form-fields created.
When form-field collections are passed, their contents keep
their existing prefixes are retained.
for_display
A flag indicating whether the form-fiellds are to be used
for display. This value is used for for_display attributes
of all created form fields. This option does not effect
input from form-field collections.
for_input
A flag indicating whether the form-fiellds are to be used
for input. This value is used for for_input attributes
of all created form fields. This option does not effect
input from form-field collections.
render_context
A flag indicating whether the default values to render
should come from the form context. See IFormField.
"""
def setUpInputWidgets(form_fields, form_prefix, context, request,
ignore_request=False):
"""Set up widgets for input
An IWidgets is returned based on the give form fields.
All of the resulting widgets will be input widgets, regardless
of whether the form fields are for display or whether the
underlying schema fields are read only. This is so that one
can easily build an input form, such as an add form from an
existing schema.
The widgets will have prefixes that combine the given form
prefix and any form-field prefixes.
A context argument is provided to allow field binding.
If ignore_request passed a true value, then the widgets will
not initialize their values from the request.
"""
def setUpEditWidgets(form_fields, form_prefix, context, request,
adapters=None, for_display=False,
ignore_request=False):
"""Set up widgets for editing or displaying content
An IWidgets is returned based on the give form fields.
The resulting widgets will be input widgets unless:
- the corresponding form field was defined with the
for_display option,
- the underlying field is read only, or
- the for_display opetion to setUpEditWidgets was passed a
true value.
The widgets fields are bound to the context after it is
adapted to the field schema. A mapping object can be passed
to setUpEditWidgets to capture the adapters created. The
adapters are placed in the mapping using both interfaces and
interface names as keys.
If the ignore_request option is passed a true value, then
widget's rendered data will be set from the context, and user
inputs will be ignored.
"""
def setUpDataWidgets(form_fields, form_prefix, context, request, data=(),
for_display=False, ignore_request=False):
"""Set up widgets for input or display
An IWidgets is returned based on the give form fields.
The resulting widgets will be input widgets unless:
- the corresponding form field was defined with the
for_display option,
- the underlying field is read only, or
- the for_display opetion to setUpEditWidgets was passed a
true value.
A data mapping argument can be passed to provide initial
data.
If the ignore_request option is passed a true value, then
widget's rendered data will be set from the passed data or
from field defaults, and user inputs will be ignored.
"""
def getWidgetsData(widgets, form_prefix, data):
"""Get input data and input errors
A sequence of input errors are returned. Any data available
are added to the data argument, which must be a mapping
argument. The keys in the output mapping are
widget/form-field names without the form prefix.
"""
def checkInvariants(form_fields, form_data):
"""Check schema invariants for input data
For each schema that was used to define the form fields and
that had invariants relevent to the fields, the invariants are
checked. Invariants that refer to fields not included in the
form fields are ignored.
A list of errors is returned.
"""
def applyChanges(context, form_fields, data, adapters=None):
"""Apply form data to an object
For each form field that has data, the data are applied to the
context argument. The context is adapter to the schema for
each field. If an adapters mapping is passed, it will be used
as a cache. Typically, it would be a mapping object populated
when setUpEditWidgets was called.
"""
def Action(label, **options):
"""Define a submit action
options:
condition
A callable or name of a method to call to test whether
the action is applicable. if the value is a method
name, then the method will be passed the action when
called, otherwise, the callables will be passed the form
and the action.
validator
A callable or name of a method to call to validate and
collect inputs. This is called only if the action was
submitted and if the action either has no condition, or the
condition evaluates to a true value. If the validator is
provided as a method name, the method will be called the
action and a dictionary in which to save data. If the
validator is provided as a callable, the callable will be
called the form, the action, and a dictionary in which to
save data. The validator normally returns a (usually empty)
list of widget input errors. It may also return None to behave
as if the action wasn't submitted.
success
A handler, called when the the action was submitted and
there are no validation errors. The handler may be provided
as either a callable or a method name. If the handler is
provided as a method name, the method will be called the
action and a dictionary containing the form data. If the
success handler is provided as a callable, the callable will
be called the form, the action, and a dictionary containing
the data. The handler may return a form result (e.g. page),
or may return None to indicate that the form should generate
it's own output.
failure
A handler, called when the the action was submitted and
there are validation errors. The handler may be provided as
either a callable or a method name. If the handler is
provided as a method name, the method will be called the
action, a dictionary containing the form data, and a list of
errors. If the failure handler is provided as a callable,
the callable will be called the form, the action, a
dictionary containing the data, and a list of errors. The
handler may return a form result (e.g. page), or may return
None to indicate that the form should generate it's own
output.
prefix
A form prefix for the action. When generating submit
actions, the prefix should be combined with the action
name, separating the two with a dot. The default prefix
is "actions"form.
name
The action name, without a prefix. If the label is a
valid Python identifier, then the lowe-case label will
be used, otherwise, a hex encoding of the label will be
used. If for some strange reason the labels in a set of
actions with the same prefix is not unique, a name will
have to be given for some actions to get unique names.
css_class
The CSS class for the action. The class defaults to
"action"form.
data
A bag of extra information that can be used by handlers,
validators, or conditions.
"""
def action(label, **options):
"""Create an action factory
This function creates a factory for creating an action from a
function, using the function as the action success handler.
The options are the same as for the Action constructor except
that the options don't include the success option.
The function is designed to be used as a decorator (Python 2.4
and later), as in:
@action("Edit")
def handle_edit(self, action, data):
...
"""
def validate(form, actions, form_prefix, data, default_validate=None):
"""Process a submitted action, if any
Check each of the given actions to see if any were submitted.
If an action was submitted, then validate the input. The input
is called by calling the action's validator, ir it has one, or
by calling the default_validate passed in.
If the input is validated successfully, and the action has one
success handler, then the success handler is called.
If the input was validated and there were errors, then the
action's failure handler will be called, if it has one.
If an action was submitted, then the function returns the
result of validation and the action. The result of validation
is normally a boolean, but may be None if no validator was
provided.
If no action was submitted, then None is returned for both the
result of validation and the action.
"""
FormBase = Attribute("""Base class for creating forms
The FormBase class provides reuasable implementation for creating
forms. It implements ISubPage, IBrowserPage, and IFormBaseCustomization.
Subclasses will override or use attributes defined by
IFormBaseCustomization.
""")
class IFormBaseCustomization(ISubPage, IBrowserPage):
"""Attributes provided by the Form base class
These attributes may be used or overridden.
Note that the update and render methods are designed to to work
together. If you override one, you probably need to override the
other, unless you use original versions in your override.
"""
label = Attribute("A label to display at the top of a form")
status = Attribute(
"""An update status message
This is normally generated by success or failure handlers.
""")
errors = Attribute(
"""Sequence of errors encountered during validation
""")
form_result = Attribute(
"""Return from action result method
""")
form_reset = Attribute(
"""Boolean indicating whether the form needs to be reset
""")
form_fields = Attribute(
"""The form's form field definitions
This attribute is used by many of the default methods.
""")
widgets = Attribute(
"""The form's widgets
- set by setUpWidgets
- used by validate
""")
def setUpWidgets(ignore_request=False):
"""Set up the form's widgets.
The default implementation uses the form definitions in the
form_fields attribute and setUpInputWidgets.
The function should set the widgets attribute.
"""
def validate(action, data):
"""The default form validator
If an action is submitted and the action doesn't have it's own
validator then this function will be called.
"""
template = Attribute(
"""Template used to display the form
This can be overridden in 2 ways:
1. You can override the attribute in a subclass
2. You can register an alternate named template, named
"default" for your form.
""")
def resetForm():
"""Reset any cached data because underlying content may have changed
"""
def error_views():
"""Return views of any errors.
The errors are returned as an iterable.
"""
class IFormFields(Interface):
"""A colection of form fields (IFormField objects)
"""
def __len__():
"""Get the number of fields
"""
def __iter__():
"""Iterate over the form fields
"""
def __getitem__(name):
"""Return the form field with the given name
If the desired firld has a prefix, then the given name should
be the prefix, a dot, and the unprefixed name. Otherwise, the
given name is just the field name.
Raise a KeyError if a field can't be found for the given name.
"""
def get(name, default=None):
"""Return the form field with the given name
If the desired firld has a prefix, then the given name should
be the prefix, a dot, and the unprefixed name. Otherwise, the
given name is just the field name.
Return the default if a field can't be found for the given name.
"""
def __add__(form_fields):
"""Add two form fields collections (IFormFields)
Return a new IFormFields that is the concatination of the two
IFormFields.
"""
def select(*names):
"""Select fields with given names in order
Return a new IFormFields that is a selection from the original
IFormFields that has the named fields in the specified order.
"""
def omit(*names):
"""Omit fields with given names
"""
SKIP_UNAUTHORIZED = 2
DISPLAY_UNWRITEABLE = 4
class IFormField(Interface):
"""Definition of a field to be included in a form
This should not be confused with a schema field.
"""
__name__ = schema.ASCII(
constraint=reConstraint('[a-zA-Z][a-zA-Z0-9_]*',
"Must be an identifier"),
title = u"Field name",
description=u"""\
This is the name, without any proefix, used for the field.
It is usually the same as the name of the for field's schem field.
"""
)
field = Attribute(
"""Schema field that defines the data of the form field
"""
)
prefix = schema.ASCII(
constraint=reConstraint('[a-zA-Z][a-zA-Z0-9_]*',
"Must be an identifier"),
title=u"Prefix",
description=u"""\
Form-field prefix. The form-field prefix is used to
disambiguate fields with the same name (e.g. from different
schema) within a collection of form fields.
""",
default="",
)
for_display = schema.Bool(
title=u"Is the form field for display only?",
description=u"""\
If this attribute has a true value, then a display widget will be
used for the field even if it is writable.
"""
)
for_input = schema.Bool(
title=u"Is the form field for input?",
description=u"""\
If this attribute has a true value, then an input widget will be
used for the field even if it is readonly.
"""
)
custom_widget = Attribute(
"""Factory to use for widget construction.
If not set, normal view lookup will be used.
"""
)
render_context = schema.Choice(
title=u"Should the rendered value come from the form context?",
description=u"""\
If this attribute has a true value, and there is no other
source of rendered data, then use data from the form context
to set the rendered value for the widget. This attribute is
ignored if:
- There is user input and user input is not being ignored, or
- Data for the value is passed to setUpWidgets.
If the value is true, then it is evaluated as a collection of bit
flags with the flags:
DISPLAY_UNWRITEABLE
If the field isn't writable, then use a display widget
TODO untested
SKIP_UNAUTHORIZED
If the user is not priviledges to perform the requested
operation, then omit a widget.
TODO unimplemented
""",
vocabulary=schema.vocabulary.SimpleVocabulary.fromValues((
False, True,
DISPLAY_UNWRITEABLE,
SKIP_UNAUTHORIZED,
DISPLAY_UNWRITEABLE | SKIP_UNAUTHORIZED,
)),
default=False,
missing_value=False,
)
get_rendered = Attribute(
"""Object to call to get a rendered value
This attribute may be set to a callable object or to
a form method name to call to get a value to be rendered in a
widget.
This attribute is ignored if:
- There is user input and user input is not being ignored, or
- Data for the value is passed to setUpWidgets.
"""
)
class IWidgets(Interface):
"""A widget collection
IWidgets provide ordered collections of widgets that also support:
- Name-based lookup
- Keeping track of whether a widget is being used for input or
display
"""
def __iter__():
"""Return an interator in the widgets, in order
"""
def __getitem__(name):
"""Get the widget with the given name
Widgets are computed from form fields (IFormField). If the
form field used to create a widget has a prefix, then that
should be reflected in the name passed.
"""
def __iter_input_and_widget__():
"""Return an iterator of flag/widget pairs
The flags indicate whether the corresponding widgets are used
for input. This is necessary because there is currently no
way to introspect a widget to determine whether it is being
used for input.
"""
def __add__(widgets):
"""Add two widgets collections
The result has the widgets in the first collection followed by
the widgets in the second collection.
Widgets should have different names in the two collections.
The bahavior is undefined if the names overlap.
"""
class IForm(Interface):
"""Base type for forms
This exists primarily to provide something for which to register
form-related conponents.
"""
class ISubPageForm(IForm, ISubPage):
"""A component that displays a part of a page.
The rendered output must not have a form tag. It is the
responsibility of the surrounding page to supply a form tag.
"""
class IPageForm(IForm, IBrowserPage):
"""A component that displays a form as a page.
"""
class IAction(ISubPage):
"""Form submit actions
"""
label = schema.TextLine(title=u"Action label")
name = schema.TextLine(title=u"Action name")
__name__ = schema.TextLine(title=u"Action name with its prefix")
data = schema.Dict(title=u"Application data")
condition = Attribute(
"""Action condition
This is a callable object that will be passed a form and an
action and that returns a boolean to indicate whether the
action is available.
""")
validator = Attribute(
"""Action validator
This is a callable object that will be passed a form and an
action and that returns a (possibly empty) list of widget
input errors.
""")
def available():
"""Return a boolean indicating whether the action is available
"""
def submitted():
"""Return a boolean indicating whether the action was submitted
"""
def validate(data):
"""Validate inputs
If an action was submitted and has a custom validator, then
the validator result is returned. Otherwise, None is returned.
Validated inputs, if any, are placed into the mapping object
passed as an argument,
"""
def success(data):
"""Handle sucessful submition
This method is called when the action was submitted and the
submitted data was valid.
"""
def failure(data, errors):
"""Handle unsucessful submition
This method is called when the action was submitted and the
submitted data was not valid.
"""
def __get__(form, form_class=None):
"""Bind an action to a form
Note that the other methods defined in this interface are
valid only after the action has been bound to a form.
"""
class IActions(Interface):
"""An action collection
IActions provide ordered collections of actions that also support
name-based lookup.
"""
def __iter__():
"""Return an interator in the actions, in order
"""
def __getitem__(name):
"""Get the action with the given name
Actions are computed from form fields (IFormField). If the
form field used to create an action has a prefix, then that
should be reflected in the name passed.
"""
def __add__(actions):
"""Add two actions collections
The result has the actions in the first collection followed by
the actions in the second collection.
Actions should have different names in the two collections.
The bahavior is undefined if the names overlap.
"""
class IBoundAction(IAction):
"""An action that has been bound to a form
"""
form = Attribute("The form to which the action is bound")
class IAddFormCustomization(IFormBaseCustomization):
"""Form responsible for adding an object.
"""
def create(data):
"""Create and return an object to be added to the context.
The data argument is a dictionary with values supplied by the
form.
If any user errors occur, they should be collected into a list
and raised as a `WidgetsError`.
"""
def add(object):
"""Add an object to the context. Returns the added object.
"""
def createAndAdd(data):
"""Create and return an object that has been added to the context.
The data argument is a dictionary with values supplied by the
form.
If any user errors occur, they should be collected into a list
and raised as a `WidgetsError`.
This is normally expected to simply call the create() and
add() methods.
"""
def nextURL():
"""Return the URL to be displayed after the add operation.
This can be relative to the view's context.
The default implementation returns `self.context.nextURL()`,
i.e. it delegates to the `IAdding` view.
"""
zope.formlib-4.2.0/src/zope/formlib/itemswidgets.py 0000644 0000765 0000024 00000053324 12055134251 022501 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Browser widgets for items
"""
__docformat__ = 'restructuredtext'
from xml.sax.saxutils import escape
from zope import component
from zope.interface import implementer
from zope.i18n import translate
from zope.schema.interfaces import InvalidValue
from zope.schema.interfaces import ITitledTokenizedTerm
from zope.formlib.widget import SimpleInputWidget, renderElement
from zope.formlib.interfaces import IInputWidget, IDisplayWidget
from zope.formlib.interfaces import ConversionError
from zope.formlib.i18n import _
from zope.browserpage import ViewPageTemplateFile
# For choices, we want to make the widget a view of the field and vocabulary.
def ChoiceDisplayWidget(field, request):
return component.getMultiAdapter((field, field.vocabulary, request),
IDisplayWidget)
def ChoiceInputWidget(field, request):
return component.getMultiAdapter((field, field.vocabulary, request),
IInputWidget)
# for collections, we want to make the widget a view of the field and the
# value_type. If the value_type is None we may fall over. We may
# not be able to do any better than that.
def CollectionDisplayWidget(field, request):
return component.getMultiAdapter((field, field.value_type, request),
IDisplayWidget)
def CollectionInputWidget(field, request):
return component.getMultiAdapter((field, field.value_type, request),
IInputWidget)
# for collections of choices, we want to make the widget a view of the field,
# the value type, and the vocabulary.
def ChoiceCollectionDisplayWidget(field, value_type, request):
return component.getMultiAdapter((field, value_type.vocabulary, request),
IDisplayWidget)
def ChoiceCollectionInputWidget(field, value_type, request):
return component.getMultiAdapter((field, value_type.vocabulary, request),
IInputWidget)
class TranslationHook(object):
"""A mixin class that provides the translation capabilities."""
def translate(self, msgid):
return translate(msgid, context=self.request, default=msgid)
class ItemsWidgetBase(TranslationHook, SimpleInputWidget):
"""Convenience base class for widgets displaying items/choices."""
extra = ""
def __init__(self, field, vocabulary, request):
"""Initialize the widget."""
# only allow this to happen for a bound field
assert field.context is not None
self.vocabulary = vocabulary
super(ItemsWidgetBase, self).__init__(field, request)
self.empty_marker_name = self.name + "-empty-marker"
def setPrefix(self, prefix):
"""Set the prefixes for the field names of the form."""
super(ItemsWidgetBase, self).setPrefix(prefix)
# names for other information from the form
self.empty_marker_name = self.name + "-empty-marker"
def __call__(self):
"""Render the widget to HTML."""
raise NotImplementedError(
"__call__() must be implemented by a subclass; use _getFormValue()"
)
def textForValue(self, term):
"""Extract a string from the `term`.
The `term` must be a vocabulary tokenized term.
This can be overridden to support more complex `term`
objects. The token is returned here since it's the only thing
known to be a string, or str()able.
"""
titled = ITitledTokenizedTerm(term, None)
if titled is None:
return term.token
return self.translate(titled.title)
def convertTokensToValues(self, tokens):
"""Convert term tokens to the terms themselves.
Tokens are used in the HTML form to represent terms. This method takes
the form tokens and converts them back to terms.
"""
values = []
for token in tokens:
try:
term = self.vocabulary.getTermByToken(token)
except LookupError:
raise InvalidValue("token %r not found in vocabulary" % token)
else:
values.append(term.value)
return values
def _emptyMarker(self):
"""Mark the form so that empty selections are also valid."""
return '' % (
self.empty_marker_name)
def hasInput(self):
"""Check whether we have any input."""
return (self.name in self.request.form or
self.empty_marker_name in self.request.form)
def _toFieldValue(self, input):
"""See `SimpleInputWidget`"""
raise NotImplementedError(
"_toFieldValue(input) must be implemented by a subclass\n"
"It may be inherited from the mix-in classes SingleDataHelper\n"
"or MultiDataHelper")
class SingleDataHelper(object):
"""Mix-in helper class for getting the term from the HTML form.
This is used when we expect a single input, i.e. the Choice field.
"""
def _toFieldValue(self, input):
if input:
try:
return self.convertTokensToValues([input])[0]
except (InvalidValue, TypeError), e:
raise ConversionError(_("Invalid value"), e)
else:
return self.context.missing_value
def hidden(self):
#XXX: _getFormValue() should return a string value that can be
# used in a HTML form, but it doesn't. When
# http://www.zope.org/Collectors/Zope3-dev/584 gets fixed
# this hack should be reverted.
# -- Bjorn Tillenius, 2006-04-12
value = self._getFormValue()
if value == self._missing:
form_value = ''
else:
form_value = self.vocabulary.getTerm(value).token
return renderElement(u'input',
type='hidden',
name=self.name,
id=self.name,
value=form_value,
cssClass=self.cssClass,
extra=self.extra)
class MultiDataHelper(object):
"""Mix-in helper class for getting the term from the HTML form.
This is used when we expect a multiple inputs, i.e. Sequence fields with a
Choice field as value_type.
"""
def _toFieldValue(self, input):
"""See SimpleInputWidget"""
if input is None:
input = []
elif not isinstance(input, list):
input = [input]
try:
values = self.convertTokensToValues(input)
except InvalidValue, e:
raise ConversionError(_("Invalid value"), e)
# All AbstractCollection fields have a `_type` attribute specifying
# the type of collection. Use it to generate the correct type,
# otherwise return a list. TODO: this breaks encapsulation.
if hasattr(self.context, '_type'):
_type = self.context._type
if isinstance(_type, tuple):
_type = _type[0]
return _type(values)
else:
return values
def _getDefault(self):
# Return the default value for this widget;
# may be overridden by subclasses.
val = self.context.default
if val is None:
val = []
return val
## Display-Widgets for Items-related fields.
class ItemDisplayWidget(SingleDataHelper, ItemsWidgetBase):
"""Simple single-selection display that can be used in many cases."""
def __init__(self, *args, **kw):
ItemsWidgetBase.__init__(self, *args, **kw)
self.required = False
_messageNoValue = _("item-missing-single-value-for-display", "")
def __call__(self):
"""See IBrowserWidget."""
value = self._getFormValue()
if value is None or value == u'':
return self.translate(self._messageNoValue)
else:
term = self.vocabulary.getTerm(value)
return self.textForValue(term)
class ItemsMultiDisplayWidget(MultiDataHelper, ItemsWidgetBase):
"""Displays a sequence of items."""
def __init__(self, *args, **kw):
ItemsWidgetBase.__init__(self, *args, **kw)
self.required = False
_messageNoValue = _("vocabulary-missing-multiple-value-for-display", "")
itemTag = 'li'
tag = 'ol'
def __call__(self):
"""See IBrowserWidget."""
value = self._getFormValue()
if value:
rendered_items = self.renderItems(value)
return renderElement(self.tag,
id=self.name,
cssClass=self.cssClass,
contents="\n".join(rendered_items),
extra=self.extra)
else:
return self.translate(self._messageNoValue)
def renderItems(self, value):
"""Render items of sequence."""
items = []
cssClass = self.cssClass or ''
if cssClass:
cssClass += "-item"
tag = self.itemTag
for item in value:
term = self.vocabulary.getTerm(item)
items.append(renderElement(
tag,
cssClass=cssClass,
contents=escape(self.textForValue(term))))
return items
class ListDisplayWidget(ItemsMultiDisplayWidget):
"""Display widget for ordered multi-selection fields.
This can be used for both Sequence, List, and Tuple fields.
"""
tag = 'ol'
class SetDisplayWidget(ItemsMultiDisplayWidget):
"""Display widget for unordered multi-selection fields.
This can be used for both Set field.
"""
tag = 'ul'
## Edit-Widgets for Items-related fields.
# BBB Set to False to never display an item for the missing value if the field
# is required, which was the behaviour of versions up to and including 3.5.0.
EXPLICIT_EMPTY_SELECTION = True
@implementer(IInputWidget)
class ItemsEditWidgetBase(SingleDataHelper, ItemsWidgetBase):
"""Widget Base for rendering item-related fields.
These widgets work with Choice fields and Sequence fields that have Choice
as value_type.
"""
size = 5
tag = 'select'
_displayItemForMissingValue = True
# Whether an empty selection should always be made explicit, i.e. even
# if the field is required.
explicit_empty_selection = False
def __init__(self, field, vocabulary, request):
"""Initialize the widget."""
super(ItemsEditWidgetBase, self).__init__(field, vocabulary, request)
def setPrefix(self, prefix):
"""Set the prefix of the input name.
Once we set the prefix of input field, we use the name of the input
field and the postfix '-query' for the associated query view.
"""
super(ItemsEditWidgetBase, self).setPrefix(prefix)
def __call__(self):
"""See IBrowserWidget."""
value = self._getFormValue()
contents = []
contents.append(self._div('value', self.renderValue(value)))
contents.append(self._emptyMarker())
return self._div(self.cssClass, "\n".join(contents))
def _div(self, cssClass, contents, **kw):
"""Render a simple div tag."""
if contents:
return renderElement('div',
cssClass=cssClass,
contents="\n%s\n" % contents,
**kw)
return ""
def renderItemsWithValues(self, values):
"""Render the list of possible values, with those found in
`values` being marked as selected."""
cssClass = self.cssClass
# multiple items with the same value are not allowed from a
# vocabulary, so that need not be considered here
rendered_items = []
count = 0
# Handle case of missing value
missing = self._toFormValue(self.context.missing_value)
if self._displayItemForMissingValue and (
not self.context.required or
EXPLICIT_EMPTY_SELECTION and
self.explicit_empty_selection and
missing in values and
self.context.default is None):
if missing in values:
render = self.renderSelectedItem
else:
render = self.renderItem
missing_item = render(count,
self.translate(self._messageNoValue),
missing,
self.name,
cssClass)
rendered_items.append(missing_item)
count += 1
# Render normal values
for term in self.vocabulary:
item_text = self.textForValue(term)
if term.value in values:
render = self.renderSelectedItem
else:
render = self.renderItem
rendered_item = render(count,
item_text,
term.token,
self.name,
cssClass)
rendered_items.append(rendered_item)
count += 1
return rendered_items
def renderItem(self, index, text, value, name, cssClass):
"""Render an item for a particular `value`."""
return renderElement('option',
contents=escape(text),
value=value,
cssClass=cssClass)
def renderSelectedItem(self, index, text, value, name, cssClass):
"""Render an item for a particular `value` that is selected."""
return renderElement('option',
contents=escape(text),
value=value,
cssClass=cssClass,
selected='selected')
class SelectWidget(ItemsEditWidgetBase):
"""Provide a selection list for the item."""
_messageNoValue = _("vocabulary-missing-single-value-for-edit",
"(nothing selected)")
def renderValue(self, value):
rendered_items = self.renderItems(value)
contents = "\n%s\n" %"\n".join(rendered_items)
return renderElement('select',
name=self.name,
id=self.name,
contents=contents,
size=self.size,
extra=self.extra)
def renderItems(self, value):
return self.renderItemsWithValues([value])
class DropdownWidget(SelectWidget):
"""Variation of the SelectWidget that uses a drop-down list."""
size = 1
explicit_empty_selection = True
class RadioWidget(SelectWidget):
"""Radio widget for single item choices.
This widget can be used when the number of selections is going
to be small.
"""
orientation = "vertical"
_messageNoValue = _("vocabulary-missing-single-value-for-edit",
"(nothing selected)")
def renderItem(self, index, text, value, name, cssClass):
"""Render an item of the list."""
return self._renderItem(index, text, value, name, cssClass)
def renderSelectedItem(self, index, text, value, name, cssClass):
"""Render a selected item of the list."""
return self._renderItem(index, text, value, name, cssClass,
checked=True)
def _renderItem(self, index, text, value, name, cssClass, checked=False):
kw = {}
if checked:
kw['checked'] = 'checked'
id = '%s.%s' % (name, index)
elem = renderElement(u'input',
value=value,
name=name,
id=id,
cssClass=cssClass,
type='radio',
**kw)
return renderElement(u'label',
contents='%s %s' % (elem, text),
**{'for': id})
def renderValue(self, value):
rendered_items = self.renderItems(value)
if self.orientation == 'horizontal':
return " ".join(rendered_items)
else:
return " ".join(rendered_items)
class ItemsMultiEditWidgetBase(MultiDataHelper, ItemsEditWidgetBase):
"""Items widget supporting multiple selections."""
_messageNoValue = _("vocabulary-missing-multiple-value-for-edit",
"(nothing selected)")
_displayItemForMissingValue = False
def renderItems(self, value):
if value == self.context.missing_value:
values = []
else:
values = list(value)
return self.renderItemsWithValues(values)
def renderValue(self, value):
# All we really add here is the ':list' in the name argument
# and mutliple='multiple' to renderElement().
rendered_items = self.renderItems(value)
return renderElement(self.tag,
name=self.name + ':list',
id=self.name,
multiple='multiple',
size=self.size,
contents="\n".join(rendered_items),
extra=self.extra)
def hidden(self):
items = []
for item in self._getFormValue():
items.append(
renderElement(u'input',
type='hidden',
name=self.name+':list',
id=self.name,
value=self.vocabulary.getTerm(item).token,
cssClass=self.cssClass,
extra=self.extra))
return '\n'.join(items)
class MultiSelectWidget(ItemsMultiEditWidgetBase):
"""Provide a selection list for the list to be selected."""
class MultiSelectSetWidget(MultiSelectWidget):
"""Provide a selection list for the set to be selected."""
def _toFieldValue(self, input):
value = super(MultiSelectSetWidget, self)._toFieldValue(input)
if isinstance(value, list):
value = set(value)
return value
class MultiSelectFrozenSetWidget(MultiSelectWidget):
"""Provide a selection list for the set to be selected."""
def _toFieldValue(self, input):
value = super(MultiSelectFrozenSetWidget, self)._toFieldValue(input)
if isinstance(value, list):
value = frozenset(value)
return value
class OrderedMultiSelectWidget(ItemsMultiEditWidgetBase):
"""A multi-selection widget with ordering support."""
template = ViewPageTemplateFile('orderedSelectionList.pt')
def choices(self):
"""Return a set of tuples (text, value) that are available."""
# Not all content objects must necessarily support the attributes
if hasattr(self.context.context, self.context.__name__):
available_values = self.context.get(self.context.context)
else:
available_values = []
return [{'text': self.textForValue(term), 'value': term.token}
for term in self.vocabulary
if term.value not in available_values]
def selected(self):
"""Return a list of tuples (text, value) that are selected."""
# Get form values
values = self._getFormValue()
# Not all content objects must necessarily support the attributes
if hasattr(self.context.context, self.context.__name__):
# merge in values from content
for value in self.context.get(self.context.context):
if value not in values:
values.append(value)
terms = [self.vocabulary.getTerm(value)
for value in values]
return [{'text': self.textForValue(term), 'value': term.token}
for term in terms]
def __call__(self):
return self.template()
class MultiCheckBoxWidget(ItemsMultiEditWidgetBase):
"""Provide a list of checkboxes that provide the choice for the list."""
orientation = "vertical"
_joinButtonToMessageTemplate = u"%s %s"
def renderValue(self, value):
rendered_items = self.renderItems(value)
if self.orientation == 'horizontal':
return " ".join(rendered_items)
else:
return " ".join(rendered_items)
def renderItem(self, index, text, value, name, cssClass):
"""Render an item of the list."""
return self._renderItem(index, text, value, name, cssClass)
def renderSelectedItem(self, index, text, value, name, cssClass):
"""Render a selected item of the list."""
return self._renderItem(index, text, value, name, cssClass,
checked=True)
def _renderItem(self, index, text, value, name, cssClass, checked=False):
kw = {}
if checked:
kw['checked'] = 'checked'
id = '%s.%s' % (name, index)
elem = renderElement('input',
type="checkbox",
cssClass=cssClass,
name=name,
id=id,
value=value,
**kw)
contents = self._joinButtonToMessageTemplate % (elem, escape(text))
return renderElement(u'label',
contents=contents,
**{'for': id})
zope.formlib-4.2.0/src/zope/formlib/namedtemplate.py 0000644 0000765 0000024 00000001456 12055134251 022610 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2005-2009 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""BBB
"""
# BBB
from zope.browserpage.namedtemplate import (
INamedTemplate, NamedTemplateImplementation, implementation,
NamedTemplate, NamedTemplatePathAdapter)
zope.formlib-4.2.0/src/zope/formlib/objectwidget.pt 0000644 0000765 0000024 00000000370 12055134251 022427 0 ustar sweh staff 0000000 0000000
zope.formlib-4.2.0/src/zope/formlib/objectwidget.py 0000644 0000765 0000024 00000014050 12055134251 022434 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Browser widgets for text-like data
"""
__docformat__ = 'restructuredtext'
from zope import component
from zope.interface import implementer
from zope.schema import getFieldNamesInOrder
from zope.formlib.interfaces import IInputWidget
from zope.formlib.widget import InputWidget
from zope.formlib.widget import BrowserWidget
from zope.formlib.utility import setUpWidgets, applyWidgetsChanges
from zope.browserpage import ViewPageTemplateFile
from zope.formlib.interfaces import IWidgetInputErrorView
class ObjectWidgetView:
template = ViewPageTemplateFile('objectwidget.pt')
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self):
return self.template()
@implementer(IInputWidget)
class ObjectWidget(BrowserWidget, InputWidget):
"""A widget over an Interface that contains Fields.
``factory``
factory used to create content that this widget (field) represents
``*_widget``
Optional CustomWidgets used to generate widgets for the fields in this
widget
"""
_object = None # the object value (from setRenderedValue & request)
_request_parsed = False
def __init__(self, context, request, factory, **kw):
super(ObjectWidget, self).__init__(context, request)
# define view that renders the widget
self.view = ObjectWidgetView(self, request)
# factory used to create content that this widget (field)
# represents
self.factory = factory
# handle foo_widget specs being passed in
self.names = getFieldNamesInOrder(self.context.schema)
for k, v in kw.items():
if k.endswith('_widget'):
setattr(self, k, v)
# set up my subwidgets
self._setUpEditWidgets()
def setPrefix(self, prefix):
super(ObjectWidget, self).setPrefix(prefix)
self._setUpEditWidgets()
def _setUpEditWidgets(self):
# subwidgets need a new name
setUpWidgets(self, self.context.schema, IInputWidget,
prefix=self.name, names=self.names,
context=self.context)
def __call__(self):
return self.view()
def legendTitle(self):
return self.context.title or self.context.__name__
def getSubWidget(self, name):
return getattr(self, '%s_widget' % name)
def subwidgets(self):
return [self.getSubWidget(name) for name in self.names]
def hidden(self):
"""Render the object as hidden fields."""
result = []
for name in self.names:
result.append(self.getSubwidget(name).hidden())
return "".join(result)
def error(self):
if self._error:
errormessages = []
keys = self._error.keys(); keys.sort()
for key in keys:
errormessages.append(str(key) + ': ')
errormessages.append(component.getMultiAdapter(
(self._error[key], self.request),
IWidgetInputErrorView).snippet())
errormessages.append(str(key) + ', ')
return ''.join(errormessages[0:-1])
return ""
def getInputValue(self):
"""Return converted and validated widget data.
The value for this field will be represented as an `ObjectStorage`
instance which holds the subfield values as attributes. It will
need to be converted by higher-level code into some more useful
object (note that the default EditView calls `applyChanges`, which
does this).
"""
errors = []
content = self.factory()
for name in self.names:
try:
setattr(content, name, self.getSubWidget(name).getInputValue())
except Exception, e:
errors.append(e)
if self._error is None:
self._error = {}
if name not in self._error:
self._error[name] = e
if errors:
raise errors[0]
return content
def applyChanges(self, content):
field = self.context
# create our new object value
value = field.query(content, None)
if value is None:
# TODO: ObjectCreatedEvent here would be nice
value = self.factory()
# apply sub changes, see if there *are* any changes
# TODO: ObjectModifiedEvent here would be nice
changes = applyWidgetsChanges(self, field.schema, target=value,
names=self.names)
# if there's changes, then store the new value on the content
if changes:
field.set(content, value)
# TODO: If value implements ILocation, set name to field name and
# parent to content
return changes
def hasInput(self):
"""Is there input data for the field
Return ``True`` if there is data and ``False`` otherwise.
"""
for name in self.names:
if self.getSubWidget(name).hasInput():
return True
return False
def setRenderedValue(self, value):
"""Set the default data for the widget.
The given value should be used even if the user has entered
data.
"""
# re-call setupwidgets with the content
self._setUpEditWidgets()
for name in self.names:
self.getSubWidget(name).setRenderedValue(getattr(value, name, None))
zope.formlib-4.2.0/src/zope/formlib/objectwidget.txt 0000644 0000765 0000024 00000010702 12055134250 022622 0 ustar sweh staff 0000000 0000000 =============
Object Widget
=============
The following example shows a Family with Mother and Father.
First define the interface for a person:
>>> from zope.interface import Interface, implementer
>>> from zope.schema import TextLine
>>> class IPerson(Interface):
... """Interface for Persons."""
...
... name = TextLine(title=u'Name', description=u'The first name')
Let's define the class:
>>> @implementer(IPerson)
... class Person(object):
... def __init__(self, name=''):
... self.name = name
Let's define the interface family:
>>> from zope.schema import Object
>>> class IFamily(Interface):
... """The familiy interface."""
...
... mother = Object(title=u'Mother',
... required=False,
... schema=IPerson)
...
... father = Object(title=u'Father',
... required=False,
... schema=IPerson)
Let's define the class family with FieldProperty's mother and father
FieldProperty validate the values if they get added:
>>> from zope.schema.fieldproperty import FieldProperty
>>> @implementer(IFamily)
... class Family(object):
... """The familiy interface."""
... mother = FieldProperty(IFamily['mother'])
... father = FieldProperty(IFamily['father'])
...
... def __init__(self, mother=None, father=None):
... self.mother = mother
... self.father = father
Let's make a instance of Family with None attributes:
>>> family = Family()
>>> bool(family.mother == None)
True
>>> bool(family.father == None)
True
Let's make a instance of Family with None attributes:
>>> mother = Person(u'Margrith')
>>> father = Person(u'Joe')
>>> family = Family(mother, father)
>>> IPerson.providedBy(family.mother)
True
>>> IPerson.providedBy(family.father)
True
Let's define a dummy class which doesn't implements IPerson:
>>> class Dummy(object):
... """Dummy class."""
... def __init__(self, name=''):
... self.name = name
Raise a SchemaNotProvided exception if we add a Dummy instance to a Family
object:
>>> foo = Dummy('foo')
>>> bar = Dummy('bar')
>>> family = Family(foo, bar)
Traceback (most recent call last):
...
SchemaNotProvided
Now let's setup a enviroment for use the widget like in a real application:
>>> from zope.publisher.browser import TestRequest
>>> from zope.schema.interfaces import ITextLine
>>> from zope.schema import TextLine
>>> from zope.formlib.widgets import TextWidget
>>> from zope.formlib.widgets import ObjectWidget
>>> from zope.formlib.interfaces import IInputWidget
Register the TextLine widget used in the IPerson interface for the field 'name'.
>>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
>>> from zope.component import provideAdapter
>>> provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
... IInputWidget)
Let's define a request and provide input value for the mothers name used
in the family object:
>>> request = TestRequest(HTTP_ACCEPT_LANGUAGE='pl')
>>> request.form['field.mother.name'] = u'Margrith Ineichen'
Before we update the object let's check the value name of the mother
instance on the family object:
>>> family.mother.name
u'Margrith'
Now let's initialize a ObjectWidget with the right attributes:
>>> mother_field = IFamily['mother']
>>> factory = Person
>>> widget = ObjectWidget(mother_field, request, factory)
Now comes the magic. Apply changes means we force the ObjectWidget to read
the request, extract the value and save it on the content. The ObjectWidget
instance uses a real Person class (factory) for add the value. The value is
temporary stored in this factory class. The ObjectWidget reads the value from
this factory and set it to the attribute 'name' of the instance mother
(The object mother is allready there). If we don't have a instance mother
allready store in the family object, the factory instance will be stored
directly to the family attribute mother. For more information see the method
'applyChanges()' in the interface
zope.formlib.objectwidget.ObjectWidget.
>>> widget.applyChanges(family)
True
Test the updated mother's name value on the object family:
>>> family.mother.name
u'Margrith Ineichen'
>>> IPerson.providedBy(family.mother)
True
So, now you know my mothers and fathers name. I hope it's also clear how to
use the Object field and the ObjectWidget.
zope.formlib-4.2.0/src/zope/formlib/orderedSelectionList.pt 0000644 0000765 0000024 00000014176 12055134251 024114 0 ustar sweh staff 0000000 0000000
zope.formlib-4.2.0/src/zope/formlib/sequencewidget.py 0000644 0000765 0000024 00000026234 12055134250 023004 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Browser widgets for sequences
"""
__docformat__ = 'restructuredtext'
from zope import component
from zope.interface import implementer
from zope.i18n import translate
from zope.schema.interfaces import ValidationError
from zope.formlib.interfaces import IDisplayWidget, IInputWidget
from zope.formlib.interfaces import WidgetInputError, MissingInputError
from zope.formlib.widget import InputWidget
from zope.formlib.i18n import _
from zope.formlib.widget import BrowserWidget
from zope.formlib.widget import DisplayWidget, renderElement
from zope.browserpage import ViewPageTemplateFile
@implementer(IInputWidget)
class SequenceWidget(BrowserWidget, InputWidget):
"""A widget baseclass for a sequence of fields.
subwidget - Optional CustomWidget used to generate widgets for the
items in the sequence
"""
template = ViewPageTemplateFile('sequencewidget.pt')
_type = tuple
def __init__(self, context, field, request, subwidget=None):
super(SequenceWidget, self).__init__(context, request)
self.subwidget = subwidget
# The subwidgets are cached in this dict if preserve_widgets is True.
self._widgets = {}
self.preserve_widgets = False
def __call__(self):
"""Render the widget"""
self._update()
return self.template()
def _update(self):
"""Set various attributes for the template"""
sequence = self._getRenderedValue()
num_items = len(sequence)
self.need_add = (not self.context.max_length
or num_items < self.context.max_length)
self.need_delete = num_items and num_items > self.context.min_length
self.marker = self._getPresenceMarker(num_items)
def widgets(self):
"""Return a list of widgets to display"""
sequence = self._getRenderedValue()
result = []
for i, value in enumerate(sequence):
widget = self._getWidget(i)
widget.setRenderedValue(value)
result.append(widget)
return result
def addButtonLabel(self):
button_label = _('Add %s')
button_label = translate(button_label, context=self.request,
default=button_label)
title = self.context.title or self.context.__name__
title = translate(title, context=self.request, default=title)
return button_label % title
def _getWidget(self, i):
"""Return a widget for the i-th number of the sequence.
Normally this method creates a new widget each time, but when
the ``preserve_widgets`` attribute is True, it starts caching
widgets. We need it so that the errors on the subwidgets
would appear only if ``getInputValue`` was called.
``getInputValue`` on the subwidgets gets called on each
request that has data.
"""
if i not in self._widgets:
field = self.context.value_type
if self.subwidget is not None:
widget = self.subwidget(field, self.request)
else:
widget = component.getMultiAdapter(
(field, self.request), IInputWidget)
widget.setPrefix('%s.%d.' % (self.name, i))
if not self.preserve_widgets:
return widget
self._widgets[i] = widget
return self._widgets[i]
def hidden(self):
"""Render the list as hidden fields."""
# length of sequence info
sequence = self._getRenderedValue()
num_items = len(sequence)
# generate hidden fields for each value
parts = [self._getPresenceMarker(num_items)]
for i in range(num_items):
value = sequence[i]
widget = self._getWidget(i)
widget.setRenderedValue(value)
parts.append(widget.hidden())
return "\n".join(parts)
def _getRenderedValue(self):
"""Returns a sequence from the request or _data"""
if self._renderedValueSet():
if self._data is self.context.missing_value:
sequence = []
else:
sequence = list(self._data)
elif self.hasInput():
sequence = self._generateSequence()
elif self.context.default is not None:
sequence = self.context.default
else:
sequence = []
# ensure minimum number of items in the form
while len(sequence) < self.context.min_length:
# Shouldn't this use self.field.value_type.missing_value,
# instead of None?
sequence.append(None)
return sequence
def _getPresenceMarker(self, count=0):
return (''
% (self.name, count))
def getInputValue(self):
"""Return converted and validated widget data.
If there is no user input and the field is required, then a
``MissingInputError`` will be raised.
If there is no user input and the field is not required, then
the field default value will be returned.
A ``WidgetInputError`` is raised in the case of one or more
errors encountered, inputting, converting, or validating the data.
"""
if self.hasInput():
self.preserve_widgets = True
sequence = self._type(self._generateSequence())
if sequence != self.context.missing_value:
# catch and set field errors to ``_error`` attribute
try:
self.context.validate(sequence)
except WidgetInputError, error:
self._error = error
raise self._error
except ValidationError, error:
self._error = WidgetInputError(
self.context.__name__, self.label, error)
raise self._error
elif self.context.required:
raise MissingInputError(self.context.__name__,
self.context.title)
return sequence
raise MissingInputError(self.context.__name__, self.context.title)
# TODO: applyChanges isn't reporting "change" correctly (we're
# re-generating the sequence with every edit, and need to be smarter)
def applyChanges(self, content):
field = self.context
value = self.getInputValue()
change = field.query(content, self) != value
if change:
field.set(content, value)
return change
def hasInput(self):
"""Is there input data for the field
Return ``True`` if there is data and ``False`` otherwise.
"""
return (self.name + ".count") in self.request.form
def _generateSequence(self):
"""Extract the values of the subwidgets from the request.
Returns a list of values.
This can only be called if self.hasInput() returns true.
"""
if self.context.value_type is None:
# Why would this ever happen?
return []
# the marker field tells how many individual items were
# included in the input; we check for exactly that many input
# widgets
try:
count = int(self.request.form[self.name + ".count"])
except ValueError:
# could not convert to int; the input was not generated
# from the widget as implemented here
raise WidgetInputError(self.context.__name__, self.context.title)
# pre-populate
sequence = [None] * count
# now look through the request for interesting values
# in reverse so that we can remove items as we go
removing = self.name + ".remove" in self.request.form
for i in reversed(range(count)):
widget = self._getWidget(i)
if widget.hasValidInput():
# catch and set sequence widget errors to ``_error`` attribute
try:
sequence[i] = widget.getInputValue()
except WidgetInputError, error:
self._error = error
raise self._error
remove_key = "%s.remove_%d" % (self.name, i)
if remove_key in self.request.form and removing:
del sequence[i]
# add an entry to the list if the add button has been pressed
if self.name + ".add" in self.request.form:
# Should this be using self.context.value_type.missing_value
# instead of None?
sequence.append(None)
return sequence
class TupleSequenceWidget(SequenceWidget):
_type = tuple
class ListSequenceWidget(SequenceWidget):
_type = list
# Basic display widget
class SequenceDisplayWidget(DisplayWidget):
_missingValueMessage = _("sequence-value-not-provided",
u"(no value available)")
_emptySequenceMessage = _("sequence-value-is-empty",
u"(no values)")
tag = "ol"
itemTag = "li"
cssClass = "sequenceWidget"
extra = ""
def __init__(self, context, field, request, subwidget=None):
super(SequenceDisplayWidget, self).__init__(context, request)
self.subwidget = subwidget
def __call__(self):
# get the data to display:
if self._renderedValueSet():
data = self._data
else:
data = self.context.get(self.context.context)
# deal with special cases:
if data == self.context.missing_value:
return translate(self._missingValueMessage, self.request)
data = list(data)
if not data:
return translate(self._emptySequenceMessage, self.request)
parts = []
for i, item in enumerate(data):
widget = self._getWidget(i)
widget.setRenderedValue(item)
s = widget()
if self.itemTag:
s = "<%s>%s%s>" % (self.itemTag, s, self.itemTag)
parts.append(s)
contents = "\n".join(parts)
if self.tag:
contents = "\n%s\n" % contents
contents = renderElement(self.tag,
cssClass=self.cssClass,
extra=self.extra,
contents=contents)
return contents
def _getWidget(self, i):
field = self.context.value_type
if self.subwidget is not None:
widget = self.subwidget(field, self.request)
else:
widget = component.getMultiAdapter(
(field, self.request), IDisplayWidget)
widget.setPrefix('%s.%d.' % (self.name, i))
return widget
zope.formlib-4.2.0/src/zope/formlib/source.py 0000644 0000765 0000024 00000051236 12055134325 021273 0 ustar sweh staff 0000000 0000000 ##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Source widgets support
"""
from itertools import imap
import xml.sax.saxutils
from zope.component import adapter, getMultiAdapter
from zope.interface import implementer
import zope.browser.interfaces
import zope.schema.interfaces
from zope.schema.interfaces import \
ISourceQueriables, ValidationError, IVocabularyTokenized, IIterableSource
import zope.formlib.interfaces
import zope.formlib.widget
from zope.formlib.interfaces import (
WidgetInputError,
MissingInputError,
ISourceQueryView,
IWidgetInputErrorView,
IDisplayWidget,
IInputWidget)
from zope.formlib.i18n import _
from zope.formlib.widgets import (
SelectWidget, RadioWidget, MultiSelectWidget, OrderedMultiSelectWidget,
MultiCheckBoxWidget, MultiSelectSetWidget, MultiSelectFrozenSetWidget)
from zope.formlib.widget import InputWidget
import zope.formlib.itemswidgets
from zope.formlib.widget import DisplayWidget
@implementer(IDisplayWidget)
class SourceDisplayWidget(DisplayWidget):
def __init__(self, field, source, request):
super(SourceDisplayWidget, self).__init__(field, request)
self.source = source
required = False
def hidden(self):
return ''
def error(self):
return ''
def __call__(self):
"""Render the current value
"""
if self._renderedValueSet():
value = self._data
else:
value = self.context.default
if value == self.context.missing_value:
value = self._translate(_("SourceDisplayWidget-missing",
default="Nothing"))
else:
terms = getMultiAdapter((self.source, self.request),
zope.browser.interfaces.ITerms)
try:
term = terms.getTerm(value)
except LookupError:
value = self._translate(_("SourceDisplayWidget-invalid",
default="Invalid value"))
else:
value = self.renderTermForDisplay(term)
return value
def renderTermForDisplay(self, term):
# Provide a rendering of `term` for display; this is not for
# use when generating a select list.
return xml.sax.saxutils.escape(self._translate(term.title))
class SourceSequenceDisplayWidget(SourceDisplayWidget):
separator = ' \n'
def __call__(self):
if self._renderedValueSet():
seq = self._data
else:
seq = self.context.default
terms = getMultiAdapter((self.source, self.request),
zope.browser.interfaces.ITerms)
result = []
for value in seq:
try:
term = terms.getTerm(value)
except LookupError:
value = self._translate(_("SourceDisplayWidget-invalid",
default="Invalid value"))
else:
value = self.renderTermForDisplay(term)
result.append(value)
return self.separator.join(result)
@implementer(IInputWidget)
class SourceInputWidget(InputWidget):
_error = None
def __init__(self, field, source, request):
super(SourceInputWidget, self).__init__(field, request)
self.source = source
self.terms = getMultiAdapter((source, self.request),
zope.browser.interfaces.ITerms)
def queryviews(self):
queriables = ISourceQueriables(self.source, None)
if queriables is None:
# treat the source itself as a queriable
queriables = ((self.name + '.query', self.source), )
else:
queriables = [
(self.name + '.' +
unicode(i).encode('base64').strip().replace('=', '_'), s)
for (i, s) in queriables.getQueriables()]
return [
(name, getMultiAdapter(
(source, self.request),
ISourceQueryView,
)
) for (name, source) in queriables]
queryviews = property(queryviews)
def _value(self):
if self._renderedValueSet():
value = self._data
else:
for name, queryview in self.queryviews:
if name+'.apply' in self.request:
token = self.request.form.get(name+'.selection')
if token is not None:
break
else:
token = self.request.form.get(self.name)
if token is not None:
try:
value = self.terms.getValue(str(token))
except LookupError:
value = self.context.missing_value
else:
value = self.context.missing_value
return value
def hidden(self):
value = self._value()
if value == self.context.missing_value:
return '' # Nothing to hide ;)
try:
term = self.terms.getTerm(value)
except LookupError:
# A value was set, but it's not valid. Treat
# it as if it was missing and return nothing.
return ''
return (''
% (self.name, xml.sax.saxutils.quoteattr(term.token))
)
def error(self):
if self._error:
# TODO This code path is untested.
return getMultiAdapter((self._error, self.request),
IWidgetInputErrorView).snippet()
return ""
def __call__(self):
result = ['
']
value = self._value()
field = self.context
term = None
if value == field.missing_value:
result.append('
')
return '\n'.join(result)
def _renderResults(self, results, name):
terms = []
for value in results:
term = self.terms.getTerm(value)
terms.append((self._translate(term.title), term.token))
terms.sort()
apply = self._translate(_("SourceInputWidget-apply", default="Apply"))
return (
'\n'
''
% (name,
'\n'.join(
[(''
% (token, title))
for (title, token) in terms]),
name,
apply)
)
def renderTermForDisplay(self, term):
# Provide a rendering of `term` for display; this is not for
# use when generating a select list.
return xml.sax.saxutils.escape(self._translate(term.title))
required = property(lambda self: self.context.required)
def getInputValue(self):
for name, queryview in self.queryviews:
if name+'.apply' in self.request:
token = self.request.form.get(name+'.selection')
if token is not None:
break
else:
token = self.request.get(self.name)
field = self.context
if token is None:
if field.required:
# TODO This code path is untested.
raise MissingInputError(
field.__name__, self.label,
)
return field.missing_value
try:
value = self.terms.getValue(str(token))
except LookupError:
# TODO This code path is untested.
err = zope.schema.interfaces.ValidationError(
"Invalid value id", token)
raise WidgetInputError(field.__name__, self.label, err)
# Remaining code copied from SimpleInputWidget
# value must be valid per the field constraints
try:
field.validate(value)
except ValidationError, err:
# TODO This code path is untested.
self._error = WidgetInputError(field.__name__, self.label, err)
raise self._error
return value
def hasInput(self):
if self.name in self.request or self.name+'.displayed' in self.request:
return True
for name, queryview in self.queryviews:
if name+'.apply' in self.request:
token = self.request.form.get(name+'.selection')
if token is not None:
return True
return False
class SourceListInputWidget(SourceInputWidget):
def _input_value(self):
tokens = self.request.form.get(self.name)
for name, queryview in self.queryviews:
if name+'.apply' in self.request:
newtokens = self.request.form.get(name+'.selection')
if newtokens:
if tokens:
tokens = tokens + newtokens
else:
tokens = newtokens
if tokens:
remove = self.request.form.get(self.name+'.checked')
if remove and (self.name+'.remove' in self.request):
tokens = [token
for token in tokens
if token not in remove
]
value = []
for token in tokens:
try:
v = self.terms.getValue(str(token))
except LookupError:
pass # skip invalid tokens (shrug)
else:
value.append(v)
else:
if self.name+'.displayed' in self.request:
value = []
else:
value = self.context.missing_value
if value:
r = []
seen = {}
for s in value:
if s not in seen:
r.append(s)
seen[s] = 1
value = r
return value
def _value(self):
if self._renderedValueSet():
value = self._data
else:
value = self._input_value()
return value
def hidden(self):
value = self._value()
if value == self.context.missing_value:
return '' # Nothing to hide ;)
result = []
for v in value:
try:
term = self.terms.getTerm(value)
except LookupError:
# A value was set, but it's not valid. Treat
# it as if it was missing and skip
continue
else:
result.append(
''
% (self.name, xml.sax.saxutils.quoteattr(term.token))
)
def __call__(self):
result = ['
']
value = self._value()
if value:
for v in value:
try:
term = self.terms.getTerm(v)
except LookupError:
continue # skip
else:
result.append(
' '
% (self.name, xml.sax.saxutils.quoteattr(term.token))
)
result.append(' ' + self.renderTermForDisplay(term))
result.append(
' '
% (self.name, xml.sax.saxutils.quoteattr(term.token)))
result.append(' ')
result.append(
' '
% (self.name,
self._translate(_("MultipleSourceInputWidget-remove",
default="Remove")))
)
result.append(' ')
result.append(' '
% self.name)
result.append('
')
for name, queryview in self.queryviews:
result.append('
')
return '\n'.join(result)
def _renderResults(self, results, name):
terms = []
apply = self._translate(_("SourceListInputWidget-apply",
default="Apply"))
for value in results:
term = self.terms.getTerm(value)
terms.append((self._translate(term.title), term.token))
terms.sort()
return (
'\n'
''
% (name,
'\n'.join([('' % (token, title))
for (title, token) in terms]),
name,
apply)
)
def getInputValue(self):
value = self._input_value()
# Remaining code copied from SimpleInputWidget
# value must be valid per the field constraints
field = self.context
try:
field.validate(value)
except ValidationError, err:
# TODO This code path is untested.
self._error = WidgetInputError(field.__name__, self.label, err)
raise self._error
return value
def hasInput(self):
return self.name+'.displayed' in self.request.form
# Input widgets for IIterableSource:
# These widgets reuse the old-style vocabulary widgets via the class
# IterableSourceVocabulary that adapts a source (and its ITerms object)
# into a vocabulary. When/if vocabularies go away, these classes
# should be updated into full implementations.
@implementer(IVocabularyTokenized)
@adapter(IIterableSource)
class IterableSourceVocabulary(object):
"""Adapts an iterable source into a legacy vocabulary.
This can be used to wrap sources to make them usable with widgets that
expect vocabularies. Note that there must be an ITerms implementation
registered to obtain the terms.
"""
def __init__(self, source, request):
self.source = source
self.terms = getMultiAdapter((source, request),
zope.browser.interfaces.ITerms)
def getTerm(self, value):
return self.terms.getTerm(value)
def getTermByToken(self, token):
value = self.terms.getValue(token)
return self.getTerm(value)
def __iter__(self):
return imap(
lambda value: self.getTerm(value), self.source.__iter__())
def __len__(self):
return self.source.__len__()
def __contains__(self, value):
return self.source.__contains__(value)
class SourceSelectWidget(SelectWidget):
"""Provide a selection list for the item."""
def __init__(self, field, source, request):
super(SourceSelectWidget, self).__init__(
field, IterableSourceVocabulary(source, request), request)
# BBB
if not zope.formlib.itemswidgets.EXPLICIT_EMPTY_SELECTION:
# Even if the field is required, no input is needed, so don't
# worry the user about it:
self.required = False
class SourceDropdownWidget(SourceSelectWidget):
"""Variation of the SourceSelectWidget that uses a drop-down list."""
size = 1
explicit_empty_selection = True
class SourceRadioWidget(RadioWidget):
"""Radio widget for single item choices."""
def __init__(self, field, source, request):
super(SourceRadioWidget, self).__init__(
field, IterableSourceVocabulary(source, request), request)
class SourceMultiSelectWidget(MultiSelectWidget):
"""A multi-selection widget with ordering support."""
def __init__(self, field, source, request):
super(SourceMultiSelectWidget, self).__init__(
field, IterableSourceVocabulary(source, request), request)
class SourceOrderedMultiSelectWidget(OrderedMultiSelectWidget):
"""A multi-selection widget with ordering support."""
def __init__(self, field, source, request):
super(SourceOrderedMultiSelectWidget, self).__init__(
field, IterableSourceVocabulary(source, request), request)
class SourceMultiSelectSetWidget(MultiSelectSetWidget):
"""Provide a selection list for the set to be selected."""
def __init__(self, field, source, request):
super(SourceMultiSelectSetWidget, self).__init__(
field, IterableSourceVocabulary(source, request), request)
class SourceMultiSelectFrozenSetWidget(MultiSelectFrozenSetWidget):
"""Provide a selection list for the frozenset to be selected."""
def __init__(self, field, source, request):
super(SourceMultiSelectFrozenSetWidget, self).__init__(
field, IterableSourceVocabulary(source, request), request)
class SourceMultiCheckBoxWidget(MultiCheckBoxWidget):
"""Provide a list of checkboxes that provide the choice for the list."""
def __init__(self, field, source, request):
super(SourceMultiCheckBoxWidget, self).__init__(
field, IterableSourceVocabulary(source, request), request)
zope.formlib-4.2.0/src/zope/formlib/source.txt 0000644 0000765 0000024 00000107324 12055134251 021460 0 ustar sweh staff 0000000 0000000 ==============
Source Widgets
==============
Sources are objects that represent sets of values from which one might choose
and are used with Choice schema fields. Source widgets currently fall into two
categories:
- widgets for iterable sources
- widgets for queryable sources
Sources (combined with the available adapters) may support both approaches, but
no widgets currently support both.
In both cases, the widgets need views that can be used to get tokens to
represent source values in forms, as well as textual representations of values.
We use the `zope.browser.interfaces.ITerms` views for that.
All of our examples will be using the component architecture::
>>> import zope.interface
>>> import zope.component
>>> import zope.schema
This `ITerms` implementation can be used for the sources involved in
our tests::
>>> from zope.browser.interfaces import ITerms
>>> import zope.publisher.interfaces.browser
>>> from zope.schema.vocabulary import SimpleTerm
>>> @zope.interface.implementer(ITerms)
... class ListTerms:
...
... def __init__(self, source, request):
... pass # We don't actually need the source or the request :)
...
... def getTerm(self, value):
... title = unicode(value)
... try:
... token = title.encode('base64').strip()
... except binascii.Error:
... raise LookupError(token)
... return SimpleTerm(value, token=token, title=title)
...
... def getValue(self, token):
... return token.decode('base64')
This view just uses the unicode representations of values as titles and the
base-64 encoding of the titles as tokens. This is a very simple strategy
that's only approriate when the values have short and unique unicode
representations.
All of the source widgets are in a single module::
>>> import zope.formlib.source
We'll also need request objects::
>>> from zope.publisher.browser import TestRequest
Iterable Source Widgets
=======================
Iterable sources are expected to be simpler than queriable sources, so they
represent a good place to start. The most important aspect of iterable sources
for widgets is that it's actually possible to enumerate all the values from the
source. This allows each possible value to be listed in a