zope.formlib-4.2.0/0000755000076500000240000000000012055141032014205 5ustar swehstaff00000000000000zope.formlib-4.2.0/bootstrap.py0000644000076500000240000001011512055134250016576 0ustar swehstaff00000000000000############################################################################## # # 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.cfg0000644000076500000240000000071712055134250016526 0ustar swehstaff00000000000000[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.txt0000644000076500000240000001074512055136470016037 0ustar swehstaff00000000000000======= 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.txt0000644000076500000240000000004012055134250016314 0ustar swehstaff00000000000000Zope Foundation and Contributorszope.formlib-4.2.0/LICENSE.txt0000644000076500000240000000402612055134250016036 0ustar swehstaff00000000000000Zope 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-INFO0000644000076500000240000031365412055141032015316 0ustar swehstaff00000000000000Metadata-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 data Required 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.txt0000644000076500000240000000032612055134251015711 0ustar swehstaff00000000000000.. 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.cfg0000644000076500000240000000007312055141032016026 0ustar swehstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 zope.formlib-4.2.0/setup.py0000644000076500000240000000663212055136470015740 0ustar swehstaff00000000000000############################################################################## # # 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/0000755000076500000240000000000012055141032014774 5ustar swehstaff00000000000000zope.formlib-4.2.0/src/zope/0000755000076500000240000000000012055141032015751 5ustar swehstaff00000000000000zope.formlib-4.2.0/src/zope/__init__.py0000644000076500000240000000007012055134250020063 0ustar swehstaff00000000000000__import__('pkg_resources').declare_namespace(__name__) zope.formlib-4.2.0/src/zope/formlib/0000755000076500000240000000000012055141032017403 5ustar swehstaff00000000000000zope.formlib-4.2.0/src/zope/formlib/__init__.py0000644000076500000240000000114512055134250021521 0ustar swehstaff00000000000000############################################################################## # # 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.py0000644000076500000240000001024412055134251022305 0ustar swehstaff00000000000000############################################################################## # # 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.txt0000644000076500000240000000371712055134250021120 0ustar swehstaff00000000000000Functional 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.zcml0000644000076500000240000004666212055134250022275 0ustar swehstaff00000000000000 zope.formlib-4.2.0/src/zope/formlib/errors.py0000644000076500000240000000334512055134251021303 0ustar swehstaff00000000000000############################################################################## # # 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.txt0000644000076500000240000000515012055134250021465 0ustar swehstaff00000000000000============== 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.py0000644000076500000240000000430412055134250021760 0ustar swehstaff00000000000000############################################################################## # # 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.py0000644000076500000240000007617412055134250020743 0ustar swehstaff00000000000000############################################################################## # # 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.txt0000644000076500000240000021044712055134250021123 0ustar swehstaff00000000000000===== 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 data Required 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.zcml0000644000076500000240000000123012055134250022115 0ustar swehstaff00000000000000 zope.formlib-4.2.0/src/zope/formlib/i18n.py0000644000076500000240000000022512055134251020540 0ustar swehstaff00000000000000"""\ I18N support for zope.formlib """ __docformat__ = "reStructuredText" import zope.i18nmessageid _ = zope.i18nmessageid.MessageFactory("zope") zope.formlib-4.2.0/src/zope/formlib/interfaces.py0000644000076500000240000010436312055134250022113 0ustar swehstaff00000000000000############################################################################## # # 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.py0000644000076500000240000005332412055134251022501 0ustar swehstaff00000000000000############################################################################## # # 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.py0000644000076500000240000000145612055134251022610 0ustar swehstaff00000000000000############################################################################## # # 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.pt0000644000076500000240000000037012055134251022427 0ustar swehstaff00000000000000
The Legend
zope.formlib-4.2.0/src/zope/formlib/objectwidget.py0000644000076500000240000001405012055134251022434 0ustar swehstaff00000000000000############################################################################## # # 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.txt0000644000076500000240000001070212055134250022622 0ustar swehstaff00000000000000============= 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.pt0000644000076500000240000001417612055134251024114 0ustar swehstaff00000000000000


zope.formlib-4.2.0/src/zope/formlib/pageform.pt0000644000076500000240000001260112055134250021554 0ustar swehstaff00000000000000

Do something

Form status summary
  • Error Type
Extra top
error
Extra bottom

Do something

Form status summary
  • Error Type
Extra top
error
Extra bottom