Rebar¶
Rebar makes your Django forms stronger.
Rebar provides form-related tools for Django which enable complex form interactions.
Installation¶
You can install Rebar using pip or easy_install:
$ pip install rebar
Once installed, Rebar provides three primary tools:
- Form Groups, for working with heterongenous collections of forms.
- State Validators, for validating that form data is in a specific state.
- Testing tools, which provide convenient helpers for writing unit tests for forms.
Documentation¶
Form Groups¶
Rebar Form Groups provide a way to manipulate a heterogenous set of Forms or FormSets as a single entity.
Warning
Restrictions
To treat a set of heterogenous Forms or FormSets as a single entity, some restrictions are necessary. Specifically, every member of a Form Group will receive the same arguments at instantiation.
This means if you’re using ModelForms and ModelFormSets, every
member has the same instance
. If your data model does not meet
this requirement, you’ll need to work around it to use Form Groups.
Declaration¶
Form Groups classes are created using a factory function, much like Form Sets. The only required argument is the sequence of members for the Form Group. A Form Group may contain any number of Forms or FormSets.
Let’s take an example where we might split form validation for a Contact into basic information and address information.
from django import forms
class ContactForm(forms.Form):
first_name = forms.CharField(
label="First Name")
last_name = forms.CharField(
label="Last Name")
email = forms.EmailField(
label="Email Address")
class AddressForm(forms.Form):
street = forms.CharField()
city = forms.CharField()
state = forms.CharField()
A Form Group allows you to combine the two and treat them as one.
from rebar.group import formgroup_factory
ContactFormGroup = formgroup_factory(
(
(ContactForm, 'contact'),
(AddressForm, 'address'),
),
)
The ContactFormGroup
class can now be instantiated like any other
form.
>>> ContactFormGroup()
<rebar.group.FormGroup ...>
Using Form Groups¶
Form Groups attempt to “look” as much like a single form as possible. Note that I say as possible, since they are a different creature, you can’t use them completely without knowledge. The goal is to make them similar enough to work with Django’s class based views
Accessing Member Forms¶
Once you’ve instantiated a Form Group, its members are accesible either by index or name.
>>> form_group = ContactFormGroup()
>>> form_group.contact
<ContactForm ...>
>>> form_group.address
<AddressForm ...>
>>> form_group[0] == form_group.contact
True
The members are provided in the order of declaration.
Form Prefixes¶
Form Groups have a prefix, much like FormSets, and sets the prefix on each member.
>>> form_group = ContactFormGroup()
>>> form_group.prefix
'group'
>>> form_group.contact.prefix
'group-contact'
You can also override the default prefix.
>>> form_group = ContactFormGroup(prefix='contact')
>>> form_group.prefix
'contact'
>>> form_group.contact.prefix
'contact-contact'
Validation¶
FormGroups use a similar approach to validation as FormSets. Calling
is_valid()
on a FormGroup instance will return True
if all
members are valid.
The errors
property is a list of ErrorLists, in group member
order.
Just as FormSets support a clean method for performing any
validation at the set level, FormGroups provide a clean
hook for
performing any validation across the entire group. In order to take
advantage of this hook, you’ll need to subclass FormGroup
.
from django.core.exceptions import ValidationError
from rebar.group import FormGroup
class BaseInvalidFormGroup(FormGroup):
def clean(self):
raise ValidationError("Group validation error.")
This class is passed to formgroup_factory
and used as the base
class for the new Form Group.
InvalidFormGroup = formgroup_factory(
(
(ContactForm, 'contact'),
(AddressForm, 'address'),
),
formgroup=BaseInvalidFormGroup,
)
When you instantiate the form group with data, any errors raised by
the clean
method are available as “group errors”:
>>> bound_formgroup = InvalidFormGroup(data={})
>>> bound_formgroup.is_valid()
False
>>> bound_formgroup.group_errors()
[u'Group validation error.']
There are two things to note about group level validation:
- Unlike
Form.clean()
, the return value ofFormGroup.clean()
is unimportant - Unlike accessing the
errors
property of Forms, FormSets, or FormGroups,FormGroup.group_errors()
does not trigger validation.
Passing Extra Arguments¶
Most arguments that you pass to a Form Group will be passed in to its
members, as well. Sometimes, however, you want to pass arguments to
specific members of the Form Group. The member_kwargs
parameter
allows you to do this.
member_kwargs
is a dict, where each key is the name of a Form
Group member, and the value is a dict of keyword arguments to pass to
that member.
For example:
>>> form_group = ContactFormGroup(
... member_kwargs={
... 'address': {
... 'prefix': 'just_address',
... },
... },
... )
>>> form_group.contact.prefix
'group-contact'
>>> form_group.address.prefix
'just_address'
In this example we override the prefix argument. A more realistic application is when you have a heavily customized form subclass that requires some additional piece of information.
Form Groups in Views¶
Using in Class Based Views¶
Form Groups are designed to be usable with Django’s class based
views. The group class can be specified as the form_class for an
edit view. If you need to pass additional arguments, you can override
the get_form_kwargs method to add the member_kwargs
.
Rendering Form Groups¶
Form Groups do not provide shortcuts for rendering in templates. The shortest way to emit the members is to simply iterate over the members:
{% for form in formgroup.forms %}
{{ form.as_p }}
{% endfor %}
Form Groups do provide media definitions that roll-up any media found in members.
State Validators¶
There are times when it’s useful to have validation logic above and beyond what a form applies. For example, a CMS might have very lax validation for saving a document in progress, but enforce additional validation for publication.
State Validators provide a method for encapsulating validation logic for states above and beyond basic form or model validation.
State Validators are made up of individual validation functions, and provides error information about failures, if any. They are similar to the validators in Forms or Models, in that you describe validators on a field basis. State Validators can be used to validate forms, models, and dicts of values.
Creating Validators¶
A State Validator consists of a collection of validation functions. A validation function takes an input value and raises a ValidationError if the value is invalid.
For example, validation functions that enforce a field as “required” and one that require an integer value might be written as follows.
from django.core.exceptions import ValidationError
def required(value):
if not bool(value):
raise ValidationError("This field is required.")
def is_int(value):
try:
int(value)
except (ValueError, TypeError):
raise ValidationError("An integer is required.")
These are wrapped into a State Validator using the
statevalidator_factory()
.
from rebar.validators import statevalidator_factory
AgeValidator = statevalidator_factory(
{
'age': (required, is_int,),
},
)
statevalidator_factory
takes a dict which maps field names to one
or more validator functions, and returns a StateValidator
class.
Note that when validating, all validators will be called for each field, regardless of whether a preceeding validator raises an exception. The goal is to collect all errors that need to be corrected.
Validating Data¶
Once a StateValidator class has been created, it can be used to
validate data. State Validators can validate a simple dict of fields,
a form, or a model. When validating a form, its cleaned_data
will
be validated if it is bound, otherwise the initial data values will be
used.
>>> validator = AgeValidator()
>>> validator.is_valid({'age': 10})
True
>>> validator.is_valid({'age': 'ten'})
False
>>> validator.is_valid({})
False
Accessing Errors¶
In addition to determining if the data is valid or not, the errors
raised by the validation functions can be retrieved through the
errors
method. StateValidator.errors()
returns a dict, where the keys correspond to field names, and the
values are a sequence of errors. errors
returns an empty dict if
there are no errors.
>>> validator = AgeValidator()
>>> validator.errors({'age': 10})
{}
>>> validator.errors({'age': 'ten'})
{'age': [u'An integer is required.']}
>>> validator.errors({})
{'age': [u'This field is required.', u'An integer is required.']}
Enabling & Disabling Validators¶
State Validators may be disabled or re-enabled. A disabled validator is always valid, and returns no errors.
>>> validator.enabled
True
>>> validator.disable()
>>> validator.is_valid({})
True
>>> validator.errors({})
{}
>>> validator.enabled
False
>>> validator.enable()
>>> validator.is_valid({})
False
Testing with Rebar¶
Rebar includes some helpers for writing unit tests for Django Forms.
Generating Form Data¶
A common pattern for testing Forms and Formsets is to create a dict of form data to use when instantiating the form. When dealing with a large form, or a form with lots of initial values, keeping the test in sync can be cumbersome.
rebar.testing.flatten_to_dict()
takes a Form, Formset, or
FormGroup, and returns its fields as a dict. This allows you to create
an unbound form, flatten it, and use the resulting dict as data to
test with.
If passed a FormSet, the return of flatten_to_dict
will include
the ManagementForm.
For example, if you have a form with a required field:
from django import forms
class NameForm(forms.Form):
first_name = forms.CharField(required=True)
You can create an unbound version of the form to generate the form data dict from.
>>> from rebar.testing import flatten_to_dict
>>> unbound_form = NameForm()
>>> form_data = flatten_to_dict(unbound_form)
>>> form_data['first_name'] = 'Larry'
>>> NameForm(data=form_data).is_valid() is True
True
This is an obviously oversimplified example, but flatten_to_dict
allows you to focus your test on the fields that actually matter in
that context.
If a ModelForm is passed to flatten_to_dict
with a foreign key,
the related object’s primary key, if any, will be used as the value
for that field. This correlates with how Django treats those fields in
form proessing.
Empty Form Data for Formsets¶
FormSets allow you to create a view that contains multiple instances
of the same form. FormSets have a convenience property, empty_form,
which returns an empty copy of the form, with its index set to the
placeholder __prefix__
.
Rebar provides a convience function,
rebar.testing.empty_form_data()
, which takes the empty form and
returns the form data dict with the prefix correctly filled in. For
example, if the FormSet contains a single item, the prefix will be set
to 2.
For example, assume we make a FormSet from our example NameForm
above.
from django.forms import formsets
NameFormSet = formsets.formset_factory(form=NameForm)
When we instantiate that FormSet, it will have a single form in it, which is empty (ie, we didn’t start with any forms). That forms prefix contains “1”, indicating its place in the sequence.
>>> formset = NameFormSet()
>>> len(formset)
1
>>> formset[0].prefix
u'form-0'
The empty_form property contains a copy of the NameForm, with its
prefix set to the __prefix__
sentinel.
>>> formset.empty_form
<NameForm ...>
>>> formset.empty_form.prefix
u'form-__prefix__'
If we pass the FormSet to empty_form_data
, we’ll get a dict of
data for the next form in the sequence.
>>> from rebar.testing import empty_form_data
>>> empty_form_data (formset)
{u'form-1-first_name': None}
You can also specify a specific index for the generated form data.
>>> empty_form_data (formset, index=42)
{u'form-42-first_name': None}
API Documentation¶
rebar Package¶
rebar
Package¶
group
Module¶
-
class
rebar.group.
FormGroup
(data=None, files=None, auto_id='id_%s', prefix=None, initial=None, label_suffix=':', instance=<rebar.group.Unspecified object>, error_class=None, member_kwargs=None)¶ Bases:
object
Form-like wrapper for a heterogenous collection of Forms.
A FormGroup collects an ordered set of Forms and/or FormSets, and provides convenience methods for validating the group as a whole.
-
add_prefix
(field_name)¶ Return the field name with a prefix prepended.
-
clean
()¶ Hook for doing formgroup-wide cleaning/validation.
Subclasses can override this to perform validation after .clean() has been called on every member.
Any ValidationError raised by this method will be accessible via formgroup.group_errors().()
-
errors
¶
-
forms
¶
-
get_default_prefix
()¶
-
group_errors
()¶ Return the group level validation errors.
Returns an ErrorList of errors that aren’t associated with a particular form. Returns an empty ErrorList if there are none.
-
html_id
(field_name, form=None)¶ Return the html ID for the given field_name.
-
is_valid
()¶
-
media
¶
-
save
()¶ Save the changes to the instance and any related objects.
-
-
class
rebar.group.
StateValidatorFormGroup
(*args, **kwargs)¶ Bases:
rebar.validators.StateValidatorFormMixin
,rebar.group.FormGroup
Subclasses are expected to define the state_validators property, which is a mapping of states to StateValidator objects.
-
get_errors
(*states)¶
-
is_valid
(*states)¶ Returns True if no errors are thrown for the specified state.
-
-
rebar.group.
formgroup_factory
(form_classes, formgroup=None, state_validators=None)¶ Return a FormGroup class for the given form[set] form_classes.
testing
Module¶
Tools for testing Forms, FormSets, and FormGroups.
-
rebar.testing.
empty_form_data
(formset, index=None)¶ Return a form data dictionary for a “new” form in a formset.
Given a formset and an index, return a copy of the empty form data. If index is not provided, the index of the first empty form will be used as the new index.
-
rebar.testing.
flatten_to_dict
(item)¶ Recursively flatten a Form-like object to a data dict.
Given a Form-like object such as a Form, ModelForm, FormSet, or FormGroup, flatten the members into a single dict, similar to what is provided by request.POST.
If
item
is a FormSet, all member Forms will be included in the resulting data dictionary
validators
Module¶
-
class
rebar.validators.
StateValidator
¶ Bases:
object
Field Validators which must pass for an object to be in a state.
-
disable
()¶ Disable the validator; when disabled, no errors will be returned.
-
enable
()¶ Enable the validators.
-
enabled
¶
-
errors
(instance)¶ Run all field validators and return a dict of errors.
The keys of the resulting dict coorespond to field names. instance can be a dict (ie, form.cleaned_data), a form, a formset, or a model instance.
If instance is a form, full_clean() will be called if the form is bound.
If instance is a formset, full_clean() will be called on each member form, if bound.
-
is_valid
(instance)¶ Return True if no errors are raised when validating instance.
instance can be a dict (ie, form.cleaned_data), a form, or a model instance. If instance is a form, full_clean() will be called.
-
validators
= {}¶
-
-
class
rebar.validators.
StateValidatorFormMixin
(*args, **kwargs)¶ Bases:
object
Mixin for adding state validators to forms.
Subclasses are expected to define the state_validators property, which is a mapping of states to StateValidator objects.
-
get_errors
(state)¶ Return any validation errors raised for the specified state.
-
is_valid
(*states)¶ Returns True if no errors are thrown for the specified state.
-
state_validators
= {}¶
-
-
rebar.validators.
statevalidator_factory
(field_validators, validator=<class 'rebar.validators.StateValidator'>)¶ Return a StateValidator Class with the given validators.
Developing Rebar¶
To run the Rebar unittests, ensure the package is set up for
development (ie, you’ve run python setup.py develop
), and then
run:
$ python setup.py test