To validate data, you must bind it to your 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, form_data
>>> 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 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:
<div>
<label class="field_req" for="foo">
Foo
</label>
<input id="foo" name="foo" type="text" value="" />
<span class="field_error">
Please enter a value
</span>
</div>
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
All validators raise a ValidationError if the validation failed.
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 *
A validation function is simply a function that, given a value, raises a ValidationError if it is invalid.
Successful if value is an int
>>> integer('1', field)
1
>>> integer('1.2', field)
...
ValidationError: Value is not an integer
Successful if value is a float
>>> float_('1', field)
1.0
>>> float_('1.2', field)
1.2
>>> float_('asdf', field)
...
ValidationError: Value is not a number
Successful if value looks like a currency amount (has exactly two digits after a decimal point)
>>> currency('asdf', field)
...
ValidationError: Value is not a number
>>> currency('1', field)
...
ValidationError: Please specify full currency value, including cents (e.g., 12.34)
>>> currency('1.0', field)
...
ValidationError: Please specify full currency value, including cents (e.g., 12.34)
>>> currency('1.00', field)
Successful if value is neither None nor the empty string (yes, including empty lists)
>>> required('asdf', field)
>>> required('', field)
...
ValidationError: Please enter a value
Successful if value is a valid RFC 822 email address. Ignores the more subtle intricacies of what is legal inside a quoted region, and thus may accept some technically invalid addresses, but will never reject a valid address (which is a much worse problem).
>>> email('a+formalchemy@gmail.com', field)
>>> email('a+."<>"@gmail.com', field)
>>> email('a+."<>"."[]"@gmail.com', field)
>>> email('a+."<>@gmail.com', field)
...
ValidationError: Unterminated quoted section in recipient
>>> email('a+."<>""[]"@gmail.com', field)
...
ValidationError: Quoted section must be followed by '@' or '.'
>>> email('<>@gmail.com', field)
...
ValidationError: Reserved character present in recipient
>>> email(chr(0) + '@gmail.com', field)
...
ValidationError: Control characters present
>>> email(chr(129) + '@gmail.com', field)
...
ValidationError: Non-ASCII characters present
>>> email('', field)
>>> email('asdf', field)
...
ValidationError: Missing @ sign
>>> email('@', field)
...
ValidationError: Recipient must be non-empty
>>> email('a@', field)
...
ValidationError: Domain must be non-empty
>>> email('a@gmail.com.', field)
...
ValidationError: Domain must not end with '.'
>>> email('a@gmail..com', field)
...
ValidationError: Domain must not contain '..'
>>> email('a@gmail>com', field)
...
ValidationError: Reserved character present in domain
Returns a validator that is successful if the input’s length is at least the given one.
>>> minlength(0)('a', field)
...
ValueError: Invalid minimum length
>>> minlength(2)('a', field)
...
ValidationError: Value must be at least 2 characters long
>>> minlength(2)('ab', field)
Returns a validator that is successful if the input’s length is at most the given one.
>>> maxlength(0)('a', field)
...
ValueError: Invalid maximum length
>>> maxlength(1)('a', field)
>>> maxlength(1)('ab', field)
...
ValidationError: Value must be no more than 1 characters long
Returns a validator that is successful if the input matches (that is, fulfils the semantics of re.match) the given expression. Expressions may be either a string or a Pattern object of the sort returned by re.compile.
>>> regex('[A-Z]+$')('ASDF', field)
>>> regex('[A-Z]+$')('abc', field)
...
ValidationError: Invalid input
>>> import re
>>> pattern = re.compile('[A-Z]+$', re.I)
>>> regex(pattern)('abc')
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
{AttributeField(number): ['Value must be less than 0']}
>>> fs.number.errors
['Value must be less than 0']