This article sheds light on some FormEncode features that aren't well documented, and will eventually be merged with the FormEncode manual. For now, please read it alongside the manual and the source code. This is from an interview with FormEncode's author Ian Bicking during PyCon 2008.
How can I customize the look of my error messages?
By default, error messages are put into a <span class="error-message">, and the input control gets a class="error" added to any other classes it has. So customize these two classes in CSS, and that's all you need in the vast majority of cases.
If you need to change the HTML, use the ``formatters`` arg to ``formencode.htmlfill.render()``. Pass a dict of formatter names to functions. The functions should take an error message and return the HTML for it. You can choose a formatter via the formatter attribute in the <form:error> tag. The default formatter is called, unsurprisingly, "default".
A standard set of formatters is available if you don't set the formatters dict, see default_formatter_dict at the bottom of formencode.htmlfill.
``formencode.htmlfill.render()`` also takes an ``error_class`` argument to specify the CSS class for input controls that have errors.
Can I display an error message not tied to a single field?
Absolutely. You can set arbitrary keys in the errors dict, and display them with <form:error>. This can be useful to place a single error message for a group of controls.
I need a "This page contains errors; scroll down to see them" message at the top of my form
You can write a chained validator that injects a general error key if there are any errors, or have ``@validate`` do this after the fact. (Not implemented in the current ``@validate``.) You'll need a <form:error> tag to display the general error message, or take advantage of the fact that unplaced error messages are put a the top of the form.
There are two incomplete pieces of FormEncode that may help this in future versions. One, the default error message is in the None key in the errors dict, but htmlfill has no way to extract it. Two, <form:iferror name="*"> is being considered as a way to display some HTML in the event of any error. Neither of these are yet implemented.
Those <form:error> tags are invalid HTML!
The first time you display the form, it will contain those <form:error> tags. Browsers ignore these but they are invalid HTML. To get rid of them, filter the page with htmlfill before sending it to the user:
1 | return htmlfill.render(render("/my_form.html"), {}, {})
|
This fills the page with no previous values or error messages, which has the net effect of removing the <form:error> and <form:iferror> tags without making any other changes.
You can also fill in default values this way; e.g., from a database record. However, this is not generally recommended due to the special needs of non-string values (boolean/date/None). Instead, set the ``value`` attribute of the HTML controls the traditional way, typically using WebHelpers.
Does .from_python() ever get called?
Not automatically. ``@validate`` calls ``.to_python()``. But you can call ``.from_python()`` manually to convert a Python value to its HTML form, in preparation for setting a ``value`` attribute in a form. You may also want to create another method to convert a datbase value if it's different from the Python format, or to generate a "blank" value for composite widgets.
One problem with FormEncode in web applications is that while it makes sense to raise ``Invalid`` errors when validating form input, it does not make sense the other way. If a database value is invalid, there's nothing the user can do about it, so you either display the value as-is, silently convert it to blank, or convert it to blank and tell the user.
How do I make a composite widget?
A composite widget combines several HTML controls into a group, which can be placed in several forms or even multiple times in one form. The inner controls are called "subwidgets", and may be nested arbitrarily deep. Typical uses are:
- Address: street, city, state/province, zip/postal code, country
- Date/time: month/day/year or hour/minute pulldowns
- Range: X - X [choose unit]
- Latitude/longutide
Error messages may be attached to the individual subwidgets or to the entire group, or even both.
There are two basic strategies:
1
2
3 | import formencode.validators as v
class Date(v.FancyValidator):
|
1) Subclass Schema and define a validator attribute for each subwidget. The HTML input tags should be named "widget.subwidget"
...
What does partial validation mean?
If a validator is partial, it will run even if a previous
validator failed. Schema validators are automatically partial. Chained
validators may be set to partial by overriding
FancyValidator.validate_partial_python and .validate_partial_other, and set
.validate_partial_form=True. These are dict validators. Frequently these
methods will be the same as their non-partial counterparts, but they may be
different if for instance some keys are missing from the dict because a
previous validator failed. If a schema fails to validate a field it removes it
from the dict. (Normally you won't notice this because there will be a
corresponding error message.)
Unfinished notes
FormValidator is an abstract base class (interface). FancyValidator does the
same thing.
Setting the initial values from a form: take Python values, pass them through
.from_python(), and then to htmlfill.
Checkboxes need if_missing=False to prevent error.
htmlfill.schemabuilder: not often used, but can specify a validator directly in
the form.
Putting complicated validator together with complicated template and controller
code: this is what ToscaWidgets is trying to do. No answer yet for plain
FormEncode.
Variabledecode: specify in pre_converter and it works both ways (to Python and
from Python). (Converters run in opposite order with from_python).
Dots in field names: can do form[name] in javascript instead of form.name.
Test validators with doctest, or call .to_python manually.
Multiple forms on a page: htmlfill does not handle this. It works on the
entire page.
@validate limitations, and imitating it in a method: Ian doesn't know about
this.
pre_validators: mainly for variabledecode.
chained_validators: doing multi-field checks. For errors, raise Invalid with a
dict containing the keys specified in form:iferror, and it will merge them
with the previous error messages. The value will be a dict rather than a
string. In normal case you'd use a FancyValidator. You can use a schema
validator if you want to specify the fields by defining attributes.
For composite widgets (nested schema validators): make a schema validator and
override .validate_python, calling the supeclass method first to validate each
field. Then put your code for multi-field checks. For errors, raise Invalid
with a string, it will go to the parent field's key. Or raise Invalid with a
dict with messages for each subfield; these correspond to "field.subfield" in
the form (use variabledecode). If the dict has a None key, the error message
will be propagated to the parent field key.
Select lists: use OneOf.
Select lists with int values: All(OneOf(), Int()). A field validator. Or add
new validator to FormEncode.
Dates: use DateConverter. Times: is there a time validator?
Messages: set your messages in the class or in a constructor arg, then raise
Invalid passing it self.message(message_key, state, **substitution_vars).
Don't use FileUploadKeeper.
Use .to_python when changing the object's type, .validate_python otherwise.
FancyValidator.to_python calls .validate_python and ._to_python.
ForEach validator: skip.
api.py:
------
Invalid.
Validator:
.all_messages: for documenting the validators.
.subvalidators: internal.
validators.py
-------------
ConvertType: useful for JSON and stuff, not for forms
Wrapper: turns functions to validators
Mention arguments and _unpack_.
Constant, Empty: useless.
MaxLength, MinLength, NotEmpty: use with All, but the functionality is built into other
validators (String , Int, FancyValidator).
Regex: when a text field needs to match a regex. Has several subclasses. Use
^ and $ in any realistic regex.
PlainText: is simple text?
OneOf: normally want to override the message for the meaning of the field.
DictConverter: possibly useful for select lists. Name-to-number like months.
Not that useful.
IndexListConverter: like dict converter but uses the implicit list index.
DateValidator: normally use with All(DateValidator(), DateConverter()). (All
is backwards!)
Bool: use StringBool instead.
Int, Number, String: useful. Bug: Number needs 'min' to prevent negative numbers.
UnicodeString: use this instead of string.
Set: for multiselects or groups of checkboxes. Value is always a sequence.
Email: resolve_domain option to verify domains are valid. (DNS lookups can be
slow. Requires PyDNS to be installed.)
SignedString: a hidden field with a string the user can't change.
FileUploadKeeper: saves to a temp directory.
RequireIfMissing: don't use, implementation unfinished.
FieldsMatch: use as chained validator
CreditCardValidator: is a nested validator (uses expire validator and security
code validator).
compound
--------
Any, All
schema.py
---------
Funcions at bottom of module might be useful for merging error messages.
SimpleFormValidator: study. Pass a validate function. This combines
.validate_python and .to_python (by modifying the passed-in dict). Third arg is
the validator (self); you can read its attributes.
declarative.py
--------------
Base class of all validators. Handles _unpackags_ and assigns keyword args
to attributes, keeps validator order. You can call the class to do .to_python
etc and it uses a singleton instance.
foreach.py
----------
Study.
formgen.py: deorecated.
htmlfill_schemabuilder.py
-------------------------
Create a validator from HTML.
<input type="text" name="some_int"
form:validator="int" form:message="use an integer!">
class Schema:
some_int = Int(messages={...: 'use an integer!'})
sqlschema.py
------------
Useful as an example of mapping a SQLObject record to a Python dict.
Deprecated.
interface.py
------------
Documentation.
sqlformgen: ignore.
Original questions
- Step-by-step HOWTO for a medium-sized form in Pylons, including HTML, validators, and controller code.
- How to build a composite widget with both subfields and widget-level validation, and a single error message for the group.
- Putting a "this form contains errors, scroll down to see them" message at the top of the form. (May require a change in htmlfill for 'field="*"' or 'field="form"', or a chained validator that sets the 'form' error key.)
- Nice ways to style error messages.
- When to use FancyValidator vs Schema vs Form for composite widgets.
- What "partial validation" means, when to use it, and how.
- How to associate a complex validator with its associated HTML, controller code, and Javascript to make maintenance and reuse easier. (ToscaWidgets partially answers this but not completely, and we need some approach that can work without it.)
- The advantages/disadvantages of using htmlfill for initial display as well as validation. (Booleans need if_missing=False, some complex types like dates may not work.) Not using it works better overall, but requires some duplication between .from_python and controller code, as well as setting values manually the traditional way.
- Variabledecode, and how you only need to specify it once for two-way conversion.
- The problem of dots in field names: can't use Javascript DOM syntax with them, have to use getElementByName. FormEncode can supposedly use another delimeter.
- An example of a form with composite widgets, including using the same widget twice in a form.
- A description of each of the standard validators, how to use them in Pylons, and whether they're recommended/useful for typical forms. (Some like the identity validator seem to be merely academic or for testing.)
- An in-depth look at building a validator and using it in another validator in Pylons.
- How to test your validators, see the input/output values available to it, etc.
- Multiple forms on a page. (I think FormEncode handles this fine but Django newforms doesn't?)
- The limitations of @validate, and how to mimic its behavior inside an action.
- A visual trace-through of how HTML values get into FormEncode, what they're turned into (including sub-dicts), the various field_dicts and sub-field_dicts that are passed, how to associate an error with a subfield or with a higher level, etc.
- More explanation of pre-validators and chained validators.
- How to both validate subfields and an entire composite widget without using pre-validators or chained validators (in order to keep all the validation code together). Can you override .to_python or ._to_python, call the parent method, and then do your overall evaluation?
- Limiting select lists results to the allowed values. (OneOf)
- Elaborate on the base classes in api.py.
- Which validators to use with which HTML controls and which Python types; e.g., select lists, integer select lists (OneOf + Integer), dates (MM/DD/YYYY and DD/MM/YYYY, and YYYY-MM-DD), times. Some of this gets into boilerplate HTML.
- How to use the 'messages' for multi-lingual output, and how to add that capability to your validators.
- How to use FileUploadKeeper in Pylons.
- When to use .to_python vs .validate_python vs .validate.
Inline Form Validation (try this if @validate isn't working for you)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | def draw_form(self):
unfilled_html = a_function_that_draws_your_form_page_html()
# when htmlfill.render fills your form with values, it uses the defaults dict to determine what the value of
# each field is. for some input types, if there is no value for a field in the defaults dict then
# htmlfill.render behaves as though a there exists a dict entry for the field having a value of None and
# draws the form accordingly.
defaults = dict(field_1="tall", field_2="small)
# this redundant pass through htmlfill.render is only required if you are using the form:error or
# form:iferror tags in your template. htmlfill.render will strip them out and return valid html (assuming the
# validity of the remaining html).
filled_html = htmlfill.render(unfilled_html, defaults)
return filled_html
def receive_form_post(self):
if you_want_only_postvars:
params = request.POST
else:
params = request.params
# this is where you want to apply variable_decode if your input fields have been formatted
if postvars_are_variable_encoded:
dict_char = "."
list_char = "-"
postvars = variabledecode.variable_decode(params, dict_char, list_char)
else:
postvars = params
this_schema = TheAppropriateSchema()
try:
state = dict() # this won't actually work. you'll need an object that formencode can hang things on.
state["useful_state_information"] = something_useful
form_result = this_schema.to_python(postvars, state)
# validation is successful - form_result contains good data for you to consume
redirect_to("somewhere")
except Invalid, e:
defaults = request.params
errors = e.error_dict
unfilled_html = a_function_that_draws_your_form_page_html()
filled_html = htmlfill.render(unfilled_html, defaults, errors)
return filled_html
|
Notes:
- a_function_that_draws_your_form_page_html is what its name suggests. This may be helpful when you find yourself re-drawing pages often.