Form Processing

Diva comes with a simple module supporting the processing of HTML forms. A request handler that does some simple form processing typically looks something like this:

from diva.forms import Form, TextValidator
from diva.routing import redirect_to
from diva.templating import output, render

class LinkForm(Form):
    username = TextValidator(required=True)
    url = TextValidator(required=True, pattern=r'^https?://')
    title = TextValidator(required=True)

@output('submit.html')
def submit(request, response):
    form = LinkForm()
    if request.method == 'POST':
        if 'cancel' in request.POST:
            redirect_to('home')
        if form.validate(request.POST):
            # Form is valid, store data into the database
            link = Link(**form.data)
            link.store(app.db)
            redirect_to('info', link.id)
    return render(errors=form.errors)

This simple example already does a couple of things you may not expect:

  • When the form is redisplayed on POST due to validation errors, the form elements will already be populated with the previously entered values.
  • The form submission is protected against Cross-Site Request Forgery (CSRF) attacks, by adding a form token both as a cookie, and as a hidden form input field.

HTML Forms in Templates

Diva does not generate HTML markup for your forms automatically. The diva.forms package is only concerned with the form data, not the rendering of individual form elements and how they are assembled into the larger form.

For the form defined above, a simple template might contain something like this:

<form action="" method="post">
  <p>
    <label>Your name: <input type="text" name="username" /></label>
    <span py:if="'username' in errors" class="error">${errors.username}</span>
  </p><p>
    <label>Link URL: <input type="text" name="url" /></label>
    <span py:if="'url' in errors" class="error">${errors.url}</span>
  </p><p>
    <label>Title: <input type="text" name="title" /></label>
    <span py:if="'title' in errors" class="error">${errors.title}</span>
  </p>
  <hr />
  <p>
    <input type="submit" />
  </p>
</form>

Note that while you don't need to manually take care of filling in the form values, you do have to explicitly add any error messages that need to be displayed after a failed validation.

Of course, you can use Genshi macros to reduce the repetitive nature of defining forms this way. But because the form layout used on one site is probably different than the layout used by other sites, you need to define such macros yourself. But it's pretty simple, really.

API Documentation

diva.forms

Tools for form processing, data conversion and validation.

Assume the following raw input data, which may for example come from the body of a POST request, which basically represents a flat dictionary:

>>> data = {
...    'people[0][name]': 'johnny', 'people[0][age]': '42',
...    'people[1][name]': 'anna', 'people[1][age]': '23',
... }

Decoding this data will produce a properly nested structure of dictionaries and lists:

>>> data = decode(data)
>>> data
{'people': [{'age': '42', 'name': 'johnny'}, {'age': '23', 'name': 'anna'}]}

Note though that neither validation nor data type conversions have been performed. That is the responsibility of the validators:

>>> validate = DictValidator(people=ListValidator(DictValidator(
...     name=TextValidator(),
...     age=IntValidator()
... )))
>>> validate(data)
{'people': [{'age': 42, 'name': u'johnny'}, {'age': 23, 'name': u'anna'}]}

You can see that type conversions have been applied as requested.

If invalid data is encountered, a ValidationError will be raised:

>>> validate(decode({'people[0][age]': 'forty-two'}))
Traceback (most recent call last):
  ...
ValidationErrors: 1 error

To get useful information out of such an exception, it needs to be "unpacked":

>>> try:
...     validate(decode({'people[0][age]': 'forty-two'}))
... except ValidationError, e:
...     errors = e.unpack()         #doctest: +ELLIPSIS
>>> print errors['people'][0]['age'][0]
Please enter a whole number.

The structure of the unpacked errors mirrors that of the decoded data and the validators, using nested dictionary and lists.

On top of validators, there's a third abstraction layer: the Form class. The example given above could also be defined and used as follows:

>>> class PeopleForm(Form):
...     people = [{
...         'name': TextValidator(),
...         'age': IntValidator()
...     }]
>>> form = PeopleForm()
>>> form.validate(data)
True
>>> form['people']
[{'age': 42, 'name': u'johnny'}, {'age': 23, 'name': u'anna'}]

And here's an example with a validation error. Note that Form.validate() decodes the data automatically.

>>> form.validate({'people[0][name]': 'johnny', 'people[0][age]': '42',
...                'people[1][name]': 'anna', 'people[1][age]': 'twenty-three'})
False
>>> print form.errors['people'][1]['age'][0]
Please enter a whole number.

decode(data)

Decodes the flat dictionary d into a nested structure.

>>> decode({'foo': 'bar'})
{'foo': 'bar'}
>>> decode({'foo[0]': 'bar', 'foo[1]': 'baz'})
{'foo': ['bar', 'baz']}
>>> data = decode({'foo[bar]': '1', 'foo[baz]': '2'})
>>> assert data == {'foo': {'bar': '1', 'baz': '2'}}
>>> decode({'foo[bar][0]': 'baz', 'foo[bar][1]': 'buzz'})
{'foo': {'bar': ['baz', 'buzz']}}
>>> decode({'foo[0][bar]': '23', 'foo[1][baz]': '42'})
{'foo': [{'bar': '23'}, {'baz': '42'}]}
>>> decode({'foo[0][0]': '23', 'foo[0][1]': '42'})
{'foo': [['23', '42']]}
>>> decode({'foo': ['23', '42']})
{'foo': ['23', '42']}

encode(data)

Encodes a nested structure into a flat dictionary.

>>> encode({'foo': 'bar'})
{'foo': 'bar'}
>>> encode({'foo': ['bar', 'baz']})
{'foo[0]': 'bar', 'foo[1]': 'baz'}
>>> encode({'foo': [{'bar': '42', 'baz': '43'}]})
{'foo[0][baz]': '43', 'foo[0][bar]': '42'}

ValidationError

Exception raised when invalid data is encountered.

unpack(self, key=None)

(Not documented)

Validator

Abstract validator base class.

__call__(self, string)

(Not documented)

DictValidator

Apply a set of validators to a dictionary of values.

>>> validator = DictValidator(name=TextValidator(), age=IntValidator())
>>> validator({'name': u'John Doe', 'age': u'42'})
{'age': 42, 'name': u'John Doe'}

ValidationErrors

(Not documented)

__init__(self, errors)

(Not documented)

__unicode__(self)

(Not documented)

unpack(self, key=None)

(Not documented)

__init__(self, **validators)

(Not documented)

__call__(self, string)

(Not documented)

__repr__(self)

(Not documented)

ListValidator

Apply a single validator to a sequence of values.

>>> validator = ListValidator(IntValidator())
>>> validator([u'1', u'2', u'3'])
[1, 2, 3]

ValidationErrors

(Not documented)

__init__(self, errors)

(Not documented)

__unicode__(self)

(Not documented)

unpack(self, key=None)

(Not documented)

__init__(self, validator, min_size=None, max_size=None)

(Not documented)

__call__(self, string)

(Not documented)

__repr__(self)

(Not documented)

TextValidator

Validator for strings.

>>> validator = TextValidator(required=True, min_length=6)
>>> validator('foo bar')
u'foo bar'
>>> validator('')
Traceback (most recent call last):
  ...
ValidationError: This field is required.

You can also specify a regular expression that the content must match using the pattern parameter:

>>> validator = TextValidator(pattern=r'\w+')
>>> validator('foo bar')
Traceback (most recent call last):
  ...
ValidationError: The value does not match pattern "^\w+$".

Because displaying regular expression patterns to the user is ugly almost always unhelpful, you can specify a custom error message that should be used when the pattern does not match:

>>> validator = TextValidator(pattern=r'\w+',
...                           message='Only letters allowed here.')
>>> validator('foo bar')
Traceback (most recent call last):
  ...
ValidationError: Only letters allowed here.

__init__(self, required=False, min_length=None, max_length=None, pattern=None, message=None)

(Not documented)

__call__(self, string)

(Not documented)

IntValidator

Validator for integers.

>>> validator = IntValidator(min_value=0, max_value=99)
>>> validator('13')
13
>>> validator('thirteen')
Traceback (most recent call last):
  ...
ValidationError: Please enter a whole number.
>>> validator('193')
Traceback (most recent call last):
  ...
ValidationError: Ensure this value is less than or equal to 99.

__init__(self, required=False, min_value=None, max_value=None)

(Not documented)

__call__(self, string)

(Not documented)

BoolValidator

Validator for boolean values.

>>> validator = BoolValidator()
>>> validator('1')
True
>>> validator = BoolValidator()
>>> validator('')
False

__call__(self, string)

(Not documented)

FileUpload

Representation of a file uploaded through an HTML form.

__init__(self, name, fileobj, size)

(Not documented)

__repr__(self)

(Not documented)

FileValidator

Validator for file uploads.

__init__(self, required=False, max_size=None)

(Not documented)

__call__(self, value)

(Not documented)

FormMeta

Meta class for forms.

__new__(cls, name, bases, d)

(Not documented)

Form

Form base class.

>>> class PersonForm(Form):
...     name = TextValidator(required=True)
...     age = IntValidator()
>>> form = PersonForm()
>>> form.validate({'name': 'johnny', 'age': '42'})
True
>>> form['name']
u'johnny'
>>> form['age']
42

Let's cause a simple validation error:

>>> form = PersonForm()
>>> form.validate({'name': '', 'age': 'fourty-two'})
False
>>> print form.errors['age'][0]
Please enter a whole number.
>>> print form.errors['name'][0]
This field is required.

You can also add custom validation routines independent of individual fields, by adding methods that start with the prefix validate_, and take the data dictionary as argument. For example:

>>> class PersonForm(Form):
...     name = TextValidator(required=True)
...     age = IntValidator()
...
...     def validate_name_alpha(self, data):
...         if not data['name'].isalpha():
...             raise ValidationError('The value must only contain letters')
>>> form = PersonForm()
>>> form.validate({'name': 'mr.t', 'age': '42'})
False
>>> form.errors
{None: ['The value must only contain letters']}

Note that validation errors that are not related to a specific field can be found under the key None of the errors dictionary.

__init__(self, id=None, name=None, defaults=None)

(Not documented)

__contains__(self, name)

(Not documented)

__iter__(self)

(Not documented)

__delitem__(self, name)

(Not documented)

__getitem__(self, name)

(Not documented)

__setitem__(self, name, value)

(Not documented)

add_validator(cls, validator, name=None)

(Not documented)

validate(self, data)

(Not documented)

form_filter(request, response, chain)

(Not documented)