:mod:`formalchemy.validators` -- Validation stuff
=================================================
.. Commented imports
>>> from formalchemy.tests import *
.. automodule:: formalchemy.validators
To validate data, you must bind it to your :class:`~formalchemy.forms.FieldSet`
along with the SQLAlchemy model. Normally, you will retrieve `data` from a
dict::
>>> from formalchemy.tests import User, bill
>>> from formalchemy.forms import FieldSet
>>> fs = FieldSet(User)
>>> fs.configure(include=[fs.name]) # we only use the name field here
>>> fs.rebind(bill, data={'User-1-name': 'Sam'})
Validation is performed simply by invoking `fs.validate()`, which
returns True if validation was successful, and False otherwise.
Validation functions are added with the `validate` method, described above.
If validation fails, `fs.errors` will be populated. `errors` is a
dictionary of validation failures, and is always empty before `validate()`
is run. Dictionary keys are attributes; values are lists of messages
given to `ValidationException`. Global errors (not specific to a
single attribute) are under the key `None`.
Rendering a :class:`~formalchemy.forms.FieldSet` with errors will result in
error messages being displayed inline. Here's what this looks like for a
required field that was not supplied with a value:
.. sourcecode:: html
Please enter a value
If validation succeeds, you can have `FormAlchemy` put the submitted
data back into the bound model object with `fs.sync`. (If you bound to
a class instead of an object, the class will be instantiated for you.)
The object will be placed into the current session, if one exists::
>>> if fs.validate(): fs.sync()
>>> print(bill.name)
Sam
Exception
---------
All validators raise a `ValidationError` if the validation failed.
.. exception:: ValidationError
Validators
----------
`formalchemy.validators` contains two types of functions: validation functions
that can be used directly, and validation function _generators_ that _return_ a
validation function satisfying some conditon. E.g., `validators.maxlength(30)`
will return a validation function that can then be passed to `validate`.
>>> from formalchemy.validators import *
Validation Functions
********************
A validation function is simply a function that, given a value, raises
a ValidationError if it is invalid.
..
>>> field = fs.name
.. autofunction:: integer
>>> integer('1', field)
1
>>> integer('1.2', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Value is not an integer
.. autofunction:: float_
>>> float_('1', field)
1.0
>>> float_('1.2', field)
1.2
>>> float_('asdf', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Value is not a number
.. autofunction:: currency
>>> currency('asdf', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Value is not a number
>>> currency('1', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Please specify full currency value, including cents (e.g., 12.34)
>>> currency('1.0', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Please specify full currency value, including cents (e.g., 12.34)
>>> currency('1.00', field)
.. autofunction:: required
>>> required('asdf', field)
>>> required('', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Please enter a value
.. autofunction:: email
>>> email('a+formalchemy@gmail.com', field)
>>> email('a+."<>"@gmail.com', field)
>>> email('a+."<>"."[]"@gmail.com', field)
>>> email('a+."<>@gmail.com', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Unterminated quoted section in recipient
>>> email('a+."<>""[]"@gmail.com', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Quoted section must be followed by '@' or '.'
>>> email('<>@gmail.com', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Reserved character present in recipient
>>> email(chr(0) + '@gmail.com', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Control characters present
>>> email(chr(129) + '@gmail.com', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Non-ASCII characters present
>>> email('', field)
>>> email('asdf', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Missing @ sign
>>> email('@', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Recipient must be non-empty
>>> email('a@', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Domain must be non-empty
>>> email('a@gmail.com.', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Domain must not end with '.'
>>> email('a@gmail..com', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Domain must not contain '..'
>>> email('a@gmail>com', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Reserved character present in domain
Function generators
*******************
.. autofunction:: length
>>> length(min=2, max=5)('a', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Value must be at least 2 characters long
>>> length(min=2, max=5)('abcdef', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Value must be no more than 5 characters long
>>> length(min=2, max=5)('abcde', field)
.. autofunction:: minlength
>>> minlength(0)('a', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError: Invalid minimum length
>>> minlength(2)('a', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Value must be at least 2 characters long
>>> minlength(2)('ab', field)
.. autofunction:: maxlength
>>> maxlength(0)('a', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError: Invalid maximum length
>>> maxlength(1)('a', field)
>>> maxlength(1)('ab', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Value must be no more than 1 characters long
.. autofunction:: regex
>>> regex('[A-Z]+$')('ASDF', field)
>>> regex('[A-Z]+$')('abc', field) # doctest: +ELLIPSIS,+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValidationError: Invalid input
>>> import re
>>> pattern = re.compile('[A-Z]+$', re.I)
>>> regex(pattern)('abc')
Write your own validator
------------------------
You can write your own validator, with the following function signature. The
`field` parameter will be the `Field` object being validated (and though its
`.parent` attribute, the `FieldSet`::
>>> def negative(value, field):
... if not (isinstance(value, int) and value < 0):
... raise ValidationError('Value must be less than 0')
Then bind it to a field::
>>> from formalchemy import types
>>> fs = FieldSet(One)
>>> fs.append(Field('number', type=types.Integer))
>>> fs.configure(include=[fs.number.validate(negative)])
Then it should work::
>>> fs.rebind(One, data={'One--number': '-2'})
>>> fs.validate()
True
>>> fs.rebind(One, data={'One--number': '2'})
>>> fs.validate()
False
You can also use the `field` positional argument to compare with some other fields
in the same FieldSet if you know this will be contained in a FieldSet, for example:
>>> def passwd2_validator(value, field):
... if field.parent.passwd1.value != value:
... raise validators.ValidationError('Password do not match')
The `FieldSet.errors` and `Field.errors` attributes contain your custom error
message::
>>> fs.errors # doctest: +IGNORE_UNICODE
{Field(number): [literal('Value must be less than 0')]}
>>> fs.number.errors # doctest: +IGNORE_UNICODE
[literal('Value must be less than 0')]