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.