Custom Validation

The default validation support is useful for some tasks, however in many cases you will want to provide your own validation rules tailored to your schema.

Flatland provides a low level interface for custom validation logic, based on a simple callable. Also provided is a higher level, class-based interface that provides conveniences for messaging, i18n and validator reuse. A library of commonly needed validators is included.

Custom Validation Basics

To use custom validation, assign a list of one or more validators to a field’s validators attribute. Each validator will be evaluated in sequence until a validator returns false or the list of validators is exhausted. If the list is exhausted and all have returned true, the element is considered valid.

A validator is a callable of the form:

validator(element, state) → bool

element is the element being validated, and state is the value passed into validate(), which defaults to None.

A typical validator will examine the value of the element:

def no_shouting(element, state):
    """Disallow ALL CAPS TEXT."""
    if element.value.isupper():
        return False
    else:
        return True

# Try out the validator
from flatland import String
form = String(validators=[no_shouting])
form.set('OH HAI')
assert not form.validate()
assert not form.valid

Validation Phases

There are two phases when validating an element or container of elements. First, each element is visited once descending down the container, breadth-first. Then each is visited again ascending back up the container.

The simple, scalar types such as String and Integer process their validators on the descent phase. The containers, such as Form and List process validators on the ascent phase.

The upshot of the phased evaluation is that container validators fire after their children, allowing container validation logic that considers the validity and status of child elements.

>>> from flatland import Dict, String
>>> def tattle(element, state):
...     print element.name
...     return True
...
>>> schema = (Dict.named('outer').
...                of(String.named('inner').
...                          using(validators=[tattle])).
...                using(validators=[tattle]))
>>> form = schema()
>>> form.validate()
inner
outer
True

Short-Circuiting Descent Validation

Descent validation can be aborted early by returning SkipAll or SkipAllFalse from a validator. Children will not be validated or have their valid attribute assigned. This capability comes in handy in a web environment when designing rich UIs.

Containers will run any validators in their descent_validators list during the descent phase. Descent validation is the only phase that may be short-circuited.

>>> from flatland import Dict, SkipAll, String
>>> def skip_children(element, state):
...     return SkipAll
...
>>> def always_fail(element, state):
...     return False
...
>>> schema = Dict.of(String.named('child').using(validators=[always_fail])).\
...               using(descent_validators=[skip_children])
>>> form = schema()
>>> form.validate()
True
>>> form['child'].valid
Unevaluated

Messaging

A form that fails to submit without a clear reason is frustrating. Messages may be stashed in the errors and warnings lists on elements. In your UI or template code, these can be used to flag individual form elements that failed validation and the reason(s) why.

def no_shouting(element, state):
    """Disallow ALL CAPS TEXT."""
    if element.value.isupper():
        element.errors.append("NO SHOUTING!")
        return False
    else:
        return True

See also add_error(), a wrapper around errors.append that ensures that identical messages aren’t added to an element more than once.

A powerful and i18n-capable interface to validation and messaging is available in the higher level Validation API.

Normalization

If you want to tweak the element’s value or u string representation, validators are free to assign directly to those attributes. There is no special enforcement of assignment to these attributes, however the convention is to consider them immutable outside of normalizing validators.

Validation state

validate() accepts an optional state argument. state can be anything you like, such as a dictionary, an object, or a string. Whatever you choose, it will be supplied to each and every validator that’s called.

state can be a convenient way of passing transient information to validators that require additional information to make their decision. For example, in a web environment, one may need to supply the client’s IP address or the logged-in user for some validators to function.

A dictionary is a good place to start if you’re considering passing information in state. None of the validators that ship with flatland access state, so no worries about type conflicts there.

class User(object):
    """A mock website user class."""

    def check_password(self, plaintext):
        """Mock comparing a password to one stored in a database."""
        return plaintext == 'secret'

def password_validator(element, state):
    """Check that a field matches the user's current password."""
    user = state['user']
    return user.check_password(element.value)

from flatland import String
form = String(validators=[password_validator])
form.set('WrongPassword')
state = dict(user=User())
assert not form.validate(state)

Examining Other Elements

Element provides a rich API for accessing a form’s members, an element’s parents, children, etc. Writing simple validators such as requiring two fields to match is easy, and complex validations are not much harder.

def passwords_must_match(element, state):
    """Both password fields must match for a password change to succeed."""
    if element.value == element.find('../password2', single=True).value:
        return True
    element.errors.append("Passwords must match.")
    return False

from flatland import Form, String
class ChangePassword(Form):
    password = String.using(validators=[passwords_must_match])
    password2 = String
    new_password = String

form = ChangePassword()
form.set({'password': 'foo', 'password2': 'f00', 'new_password': 'bar'})
assert not form.validate()
assert form['password'].errors

Short-Circuiting Validation

To stop validation of an element & skip any remaining members of flatland.Element.validators, return flatland.Skip from the validator:

from flatland import Skip

def succeed_early(element, state):
    return Skip

def always_fails(element, state):
    return False

from flatland import String
form = String(validators=[succeed_early, always_fails])
assert form.validate()

Above, always_fails is never invoked.

To stop validation early with a failure, simply return False.