django-betterforms¶
Making django forms suck less.
Contents:
Introduction¶
django-betterforms
provides a suite of tools to make working with forms in
Django easier.
Installation¶
Install the package:
$ pip install django-betterforms
Or you can install it from source:
$ pip install -e git://github.com/fusionbox/django-betterforms@master#egg=django-betterforms-dev
Add
betterforms
to yourINSTALLED_APPS
.
Quick Start¶
Getting started with betterforms
is easy. If you are using the build in
form base classes provided by django, its as simple as switching to the form
base classes provided by betterforms
.
from betterforms.forms import BaseForm
class RegistrationForm(BaseForm):
...
class Meta:
fieldsets = (
('info', {'fields': ('username', 'email')}),
('location', {'fields': ('address', ('city', 'state', 'zip'))}),
('password', {'password1', 'password2'}),
)
And then in your template.
<form method="post">
{% include 'betterforms/form_as_fieldsets.html' %}
</form>
Which will render the following.
<fieldset class="formFieldset info">
<div class="required username formField">
<label for="id_username">Username</label>
<input id="id_username" name="username" type="text" />
</div>
<div class="required email formField">
<label for="id_email">Email</label>
<input id="id_email" name="email" type="text" />
</div>
</fieldset>
<fieldset class="formFieldset location">
<div class="required address formField">
<label for="id_address">Address</label>
<input id="id_address" name="address" type="text" />
</div>
<fieldset class="formFieldset location_1">
<div class="required city formField">
<label for="id_city">City</label>
<input id="id_city" name="city" type="text" />
</div>
<div class="required state formField">
<label for="id_state">State</label>
<input id="id_state" name="state" type="text" />
</div>
<div class="required zip formField">
<label for="id_zip">Zip</label>
<input id="id_zip" name="zip" type="text" />
</div>
</fieldset>
</fieldset>
<fieldset class="formFieldset password">
<div class="required password1 formField">
<label for="id_password1">Password</label>
<input id="id_password1" name="password1" type="password" />
</div>
<div class="required password2 formField">
<label for="id_password2">Confirm Password</label>
<input id="id_password2" name="password2" type="password" />
</div>
</fieldset>
Forms¶
django-betterforms
provides two new form base classes to use in place of
django.forms.Form
. and django.forms.ModelForm
.
Errors¶
Adding errors in betterforms
is easy:
>>> form = BlogEntryForm(request.POST)
>>> form.is_valid()
True
>>> form.field_error('title', 'This title is already taken')
>>> form.is_valid()
False
>>> form.errors
{'title': ['This title is already taken']}
You can also add global errors:
>>> form = BlogEntryForm(request.POST)
>>> form.form_error('Not accepting new entries at this time')
>>> form.is_valid()
False
>>> form.errors
{'__all__': ['Not accepting new entries at this time']}
form_error is simply a wrapper around field_error that uses the key __all__ for the field name.
Fieldsets¶
One of the most powerful features in betterforms
is the ability to declare
field groupings. Both BetterForm
and BetterModelForm
provide
a common interface for working with fieldsets.
Fieldsets can be declared in any of three formats, or any mix of the three formats.
As Two Tuples
Similar to admin fieldsets, as a list of two tuples. The two tuples should be in the format
(name, fieldset_options)
wherename
is a string representing the title of the fieldset andfieldset_options
is a dictionary which will be passed askwargs
to the constructor of the fieldset.from betterforms.forms import BetterForm class RegistrationForm(BetterForm): ... class Meta: fieldsets = ( ('info', {'fields': ('username', 'email')}), ('location', {'fields': ('address', ('city', 'state', 'zip'))}), ('password', {'password1', 'password2'}), )
As a list of field names
Fieldsets can be declared as a list of field names.
from betterforms.forms import BetterForm class RegistrationForm(BetterForm): ... class Meta: fieldsets = ( ('username', 'email'), ('address', ('city', 'state', 'zip')), ('password1', 'password2'), )
As instantiated
Fieldset
instances or subclasses ofFieldset
.Fieldsets can be declared as a list of field names.
from betterforms.forms import BetterForm, Fieldset class RegistrationForm(BetterForm): ... class Meta: fieldsets = ( Fieldset('info', fields=('username', 'email')), Fieldset('location', ('address', ('city', 'state', 'zip'))), Fieldset('password', ('password1', 'password2')), )
All three of these examples will have appoximately the same output. All of these formats can be mixed and matched and nested within each other. And Unlike django-admin, you may nest fieldsets as deep as you would like.
A Fieldset
can also optionally be declared with a legend kwarg,
which will then be made available as a property to the associated
BoundFieldset
.
Fieldset('location', ('address', ('city', 'state', 'zip')), legend='Place of Residence')
Should you choose to render the form using the betterform templates detailed below, each fieldset with a legend will be rendered with an added legend tag in the template.
Rendering¶
To render a form, use the provided template partial.
<form method="post">
{% include 'betterforms/form_as_fieldsets.html' %}
</form>
This partial assumes there is a variable form
available in its context.
This template does the following things.
outputs the
csrf_token
.outputs a hidden field named
next
if there is anext
value available in the template context.outputs the form media
- loops over
form.fieldsets
. for each fieldsets, renders the fieldset using the template
betterforms/fieldset_as_div.html
- for each item in the fieldset, if it is a fieldset, it is rendered using the same template, and if it is a field, renders it using the field template.
for each field, renders the field using the template
betterforms/field_as_div.html
- loops over
If you want to output the form without the CSRF token (for example on a GET form), you can do so by passing in the csrf_exempt variable.
<form method="post">
{% include 'betterforms/form_as_fieldsets.html' csrf_exempt=True %}
</form>
If you wish to override the label suffix, django-betterforms
provides a
convenient class attribute on the BetterForm
and
BetterModelForm
classes.
class MyForm(forms.BetterForm):
# ... fields
label_suffix = '->'
Warning
Due to a bug in dealing with the label suffix in Django < 1.6, the
label_suffix
will not appear in any forms rendered using the
betterforms templates. For more information, refer to the Django bug
#18134.
Changelist Forms¶
Changelist Forms are designed to facilitate easy searching and sorting on django models, along with providing a framework for building other functionality that deals with operations on lists of model instances.
-
class
betterforms.changelist.
ChangeListForm
¶ Base form class for all Changelist forms.
setting the queryset:
All changelist forms need to know what queryset to begin with. You can do this by either passing a named keyword parameter into the contructor of the form, or by defining a model attribute on the class.
-
class
betterforms.changelist.
SearchForm
[source]¶ - Form class which facilitates searching across a set of specified fields for a model. This form adds a field to the model
q
for the search query.-
SEARCH_FIELDS
¶ The list of fields that will be searched against.
-
CASE_SENSITIVE
¶ - Whether the search should be case sensitive.
Here is a simple
SearchForm
example for searching across users.# my_app/forms.py from django.contrib.auth.models import get_user_model from betterforms.forms import SearchForm class UserSearchForm(SearchForm): SEARCH_FIELDS = ('username', 'email', 'name') model = get_user_model() # my_app.views.py from my_app.forms import UserSearchForm def user_list_view(request): form = UserSearchForm(request.GET) context = {'form': form} if form.is_valid: context['queryset'] = form.get_queryset() return render_to_response(context, ...)
SearchForm
checks to see if the query value is present in any of the fields declared inSEARCH_FIELDS
by or-ing togetherQ
objects using__contains
or__icontains
queries on those fields.
-
-
class
betterforms.changelist.
SortForm
[source]¶ - Form which facilitates the sorting instances of a model. This form adds a hidden field
sorts
to the model which is used to dictate the columns which should be sorted on and in what order.-
HEADERS
¶ The list of
Header
objects for sorting.Headers can be declared in multiple ways.
As instantiated
Header
objects.:# my_app/forms.py from betterforms.forms import SortForm, Header class UserSortForm(SortForm): HEADERS = ( Header('username', ..), Header('email', ..), Header('name', ..), ) model = User
As a string:
# my_app/forms.py from betterforms.forms import SortForm class UserSortForm(SortForm): HEADERS = ( 'username', 'email', 'name', ) model = User
As an iterable of
*args
which will be used to instantiate theHeader
object.:# my_app/forms.py from betterforms.forms import SortForm class UserSortForm(SortForm): HEADERS = ( ('username', ..), ('email', ..), ('name', ..), ) model = User
As a two-tuple of header name and
**kwargs
. The name and provided**kwargs
will be used to instantiate theHeader
objects.# my_app/forms.py from betterforms.forms import SortForm class UserSortForm(SortForm): HEADERS = ( ('username', {..}), ('email', {..}), ('name', {..}), ) model = User
All of these examples are roughly equivilent, resulting in the form haveing three sortable headers,
('username', 'email', 'name')
, which will map to those named fields on theUser
model.See documentation on the
Header
class for more information on how sort headers can be configured.
-
get_order_by
()[source]¶ Returns a list of column names that are used in the
order_by
call on the returned queryset.
During instantiation, all declared headers on
form.HEADERS
are converted toHeader
objects and are accessible fromform.headers
.>>> [header for header in form.headers] # Iterate over all headers. >>> form.headers[2] # Get the header at index-2 >>> form.headers['username'] # Get the header named 'username'
-
-
class
betterforms.changelist.
Header
(name, label=None, column_name=None, is_sortable=True)[source]¶ Headers are the the mechanism through which
SortForm
shines. They provide querystrings for operations related to sorting by whatever query parameter that header is tied to, as well as other values that are helpful during rendering.-
name
¶
The name of the header.
-
label
¶
The human readable name of the header.
-
is_active
¶
Boolean
as to whether this header is currently being used to sort.-
is_ascending
¶
Boolean
as to whether this header is being used to sort the data in ascending order.-
is_descending
¶
Boolean
as to whether this header is being used to sort the data in descending order.-
css_classes
¶
Space separated list of css classes, suitable for output in the template as an HTML element’s css class attribute.
-
priority
¶
1-indexed number as to the priority this header is at in the list of sorts. Returns
None
if the header is not active.-
querystring
¶
The querystring that will sort by this header as the primary sort, moving all other active sorts in their current order after this one. Preserves all other query parameters.
-
remove_querystring
¶
The querystring that will remove this header from the sorts. Preserves all other query parameters.
-
singular_querystring
¶
The querystring that will sort by this header, and deactivate all other active sorts. Preserves all other query parameters.
-
Working With Changelists¶
Outputting sort form headers can be done using a provided template partial
located at betterforms/sort_form_header.html
<th class="{{ header.css_classes }}">
{% if header.is_sortable %}
<a href="?{{ header.querystring }}">{{ header.label }}</a>
{% if header.is_active %}
{% if header.is_ascending %}
▾
{% elif header.is_descending %}
▴
{% endif %}
<a href="" data-sort_by="title" data-direction="up"></a>
<span class="filterActive"><span>{{ header.priority }}</span> <a href="?{{ header.remove_querystring }}">x</a></span>
{% endif %}
{% else %}
{{ header.label }}
{% endif %}
</th>
This example assumes that you are using a table to output your data. It should be trivial to modify this to suite your needs.
-
class
betterforms.views.
BrowseView
[source]¶ Class-based view for working with changelists. It is a combination of
FormView
andListView
in that it handles form submissions and providing a optionally paginated queryset for rendering in the template.Works similarly to the standard
FormView
class provided by django, except that the form is instantiated usingrequest.GET
, and theobject_list
passed into the template context comes fromform.get_queryset()
.
MultiForm and MultiModelForm¶
A container that allows you to treat multiple forms as one form. This is great
for using more than one form on a page that share the same submit button.
MultiForm
imitates the Form API so that it is invisible to anybody
else (for example, generic views) that you are using a MultiForm
.
There are a couple of differences, though. One lies in how you initialize the form. See this example:
class UserProfileMultiForm(MultiForm):
form_classes = {
'user': UserForm,
'profile': ProfileForm,
}
UserProfileMultiForm(initial={
'user': {
# User's initial data
},
'profile': {
# Profile's initial data
},
})
The initial data argument has to be a nested dictionary so that we can associate the right initial data with the right form class.
The other major difference is that there is no direct field access because this
could lead to namespace clashes. You have to access the fields from their
forms. All forms are available using the key provided in
form_classes
:
form = UserProfileMultiForm()
# get the Field object
form['user'].fields['name']
# get the BoundField object
form['user']['name']
MultiForm
, however, does all you to iterate over all the fields of all
the forms.
{% for field in form %}
{{ field }}
{% endfor %}
If you are relying on the fields to come out in a consistent order, you should
use an OrderedDict to define the form_classes
.
from collections import OrderedDict
class UserProfileMultiForm(MultiForm):
form_classes = OrderedDict((
('user', UserForm),
('profile', ProfileForm),
))
Working with ModelForms¶
MultiModelForm adds ModelForm support on top of MultiForm. That simply means that it includes support for the instance parameter in initialization and adds a save method.
class UserProfileMultiForm(MultiModelForm):
form_classes = {
'user': UserForm,
'profile': ProfileForm,
}
user = User.objects.get(pk=123)
UserProfileMultiForm(instance={
'user': user,
'profile': user.profile,
})
Working with CreateView¶
It is pretty easy to use MultiModelForms with Django’s
CreateView
, usually you will have to
override the form_valid()
method to do some specific saving functionality. For example, you could have a
signup form that created a user and a user profile object all in one:
# forms.py
from django import forms
from authtools.forms import UserCreationForm
from betterforms.multiform import MultiModelForm
from .models import UserProfile
class UserProfileForm(forms.ModelForm):
class Meta:
fields = ('favorite_color',)
class UserCreationMultiForm(MultiModelForm):
form_classes = {
'user': UserCreationForm,
'profile': UserProfileForm,
}
# views.py
from django.views.generic import CreateView
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import redirect
from .forms import UserCreationMultiForm
class UserSignupView(CreateView):
form_class = UserCreationMultiForm
success_url = reverse_lazy('home')
def form_valid(self, form):
# Save the user first, because the profile needs a user before it
# can be saved.
user = form['user'].save()
profile = form['profile'].save(commit=False)
profile.user = user
profile.save()
return redirect(self.get_success_url())
Note
In this example, we used the UserCreationForm
from the django-authtools
package just for the purposes of brevity. You could of course use any
ModelForm that you wanted to.
Of course, we could put the save logic in the UserCreationMultiForm
itself
by overriding the MultiModelForm.save()
method.
class UserCreationMultiForm(MultiModelForm):
form_classes = {
'user': UserCreationForm,
'profile': UserProfileForm,
}
def save(self, commit=True):
objects = super(UserCreationMultiForm, self).save(commit=False)
if commit:
user = objects['user']
user.save()
profile = objects['profile']
profile.user = user
profile.save()
return objects
If we do that, we can simplify our view to this:
class UserSignupView(CreateView):
form_class = UserCreationMultiForm
success_url = reverse_lazy('home')
Working with UpdateView¶
Working with UpdateView
likewise is
quite easy, but you most likely will have to override the
django.views.generic.edit.FormMixin.get_form_kwargs
method in
order to pass in the instances that you want to work on. If we keep with the
user/profile example, it would look something like this:
# forms.py
from django import forms
from django.contrib.auth import get_user_model
from betterforms.multiform import MultiModelForm
from .models import UserProfile
User = get_user_model()
class UserEditForm(forms.ModelForm):
class Meta:
fields = ('email',)
class UserProfileForm(forms.ModelForm):
class Meta:
fields = ('favorite_color',)
class UserEditMultiForm(MultiModelForm):
form_classes = {
'user': UserEditForm,
'profile': UserProfileForm,
}
# views.py
from django.views.generic import UpdateView
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import redirect
from django.contrib.auth import get_user_model
from .forms import UserEditMultiForm
User = get_user_model()
class UserSignupView(UpdateView):
model = User
form_class = UserEditMultiForm
success_url = reverse_lazy('home')
def get_form_kwargs(self):
kwargs = super(UserSignupView, self).get_form_kwargs()
kwargs.update(instance={
'user': self.object,
'profile': self.object.profile,
})
return kwargs
API Reference¶
-
class
betterforms.multiform.
MultiForm
[source]¶ The main interface for customizing
MultiForms
is through overriding theform_classes
class attribute.Once a MultiForm is instantiated, you can access the child form instances with their names like this:
>>> class MyMultiForm(MultiForm): form_classes = { 'foo': FooForm, 'bar': BarForm, } >>> forms = MyMultiForm() >>> foo_form = forms['foo']
You may also iterate over a multiform to get all of the fields for each child instance.
MultiForm API
The following attributes and methods are made available for customizing the instantiation of multiforms.
-
__init__
(*args, **kwargs)[source]¶ The
__init__()
is basically just a pass-through to the children form classes’ initialization methods, the only thing that it does is provide special handling for theinitial
parameter. Instead of being a dictionary of initial values,initial
is now a dictionary of form name, initial data pairs.UserProfileMultiForm(initial={ 'user': { # User's initial data }, 'profile': { # Profile's initial data }, })
-
form_classes
¶ This is a dictionary of form name, form class pairs. If the order of the forms is important (for example for output), you can use an OrderedDict instead of a plain dictionary.
-
get_form_args_kwargs
(key, args, kwargs)[source]¶ This method is available for customizing the instantiation of each form instance. It should return a two-tuple of args and kwargs that will get passed to the child form class that corresponds with the key that is passed in. The default implementation just adds a prefix to each class to prevent field value clashes.
Form API
The following attributes and methods are made available for mimicking the
Form
API.-
media
¶
-
is_bound
¶
-
cleaned_data
¶ Returns an OrderedDict of the
cleaned_data
for each of the child forms.
-
-
class
betterforms.multiform.
MultiModelForm
[source]¶ MultiModelForm
differs fromMultiForm
only in that adds special handling for theinstance
parameter for initialization and has asave()
method.-
__init__
(*args, **kwargs)[source]¶ MultiModelForm's
initialization method provides special handling for theinstance
parameter. Instead of being one object, theinstance
parameter is expected to be a dictionary of form name, instance object pairs.UserProfileMultiForm(instance={ 'user': user, 'profile': user.profile, })
-
save
(commit=True)[source]¶ The
save()
method will iterate through the child classes and call save on each of them. It returns an OrderedDict of form name, object pairs, where the object is what is returned by the save method of the child form class. Like theModelForm.save
method, ifcommit
isFalse
,MultiModelForm.save()
will add asave_m2m
method to theMultiModelForm
instance to aid in saving the many-to-many relations later.
-
Addendum About django-multiform¶
There is another Django app that provides a similar wrapper called
django-multiform that provides essentially the same features as betterform’s
MultiForm
. I searched for an app that did this feature when I started
work on betterform’s version, but couldn’t find one. I have looked at
django-multiform now and I think that while they are pretty similar, but there
are some differences which I think should be noted:
- django-multiform’s
MultiForm
class actually inherits from Django’s Form class. I don’t think it is very clear if this is a benefit or a disadvantage, but to me it seems that it means that there is Form API that exposed by django-multiform’sMultiForm
that doesn’t actually delegate to the child classes. - I think that django-multiform’s method of dispatching the different values
for instance and initial to the child classes is more complicated that it
needs to be. Instead of just accepting a dictionary like betterform’s
MultiForm
does, with django-multiform, you have to write a dispatch_init_initial method.
Changelog¶
1.1.1 (2014-08-22)¶
- (Bugfix) Output both the prefixed and non-prefixed name when the Form is prefixed. [Rocky Meza, #17]
1.1.0 (2014-08-04)¶
- Output required for fields even on forms that don’t define required_css_class [#16]
1.0.1 (2014-07-07)¶
- (Bugfix) Handle None initial values more robustly in MultiForm
1.0.0 (2014-07-04)¶
Backwards-incompatible changes:
- Moved all the partials to live the betterforms directory
- Dropped support for Django 1.3
New Features and Bugfixes:
- Support Python 3
- Support Django 1.6
- Add MultiForm and MultiModelForm
- Make NonBraindamagedErrorMixin use error_class
- Use NON_FIELD_ERRORS constant instead of hardcoded value
- Add csrf_exempt argument for form_as_fieldsets
- Add legend attribute to Fieldset
0.1.3 (2013-10-17)¶
- Add
betterforms.changelist
module with form classes that assist in listing, searching and filtering querysets.
0.1.2 (2013-07-25)¶
- actually update the package.
0.1.1 (2013-07-25)¶
- fix form rendering for forms with no fieldsets
0.1.0 (2013-07-25)¶
Initial Release