Effective Django Forms§

Form Basics§

Forms in Context§

Views Convert Request to Response
Forms Convert input to Python objects
Models Data and business logic

Defining Forms§

Forms are composed of fields, which have a widget.

from django.utils.translation import gettext_lazy as _
from django import forms

class ContactForm(forms.Form):

    name = forms.CharField(label=_("Your Name"),
        max_length=255,
        widget=forms.TextInput,
    )

    email = forms.EmailField(label=_("Email address"))

Instantiating a Form§

Unbound forms don't have data associated with them, but they can be rendered:

form = ContactForm()

Bound forms have specific data associated, which can be validated:

form = ContactForm(data=request.POST, files=request.FILES)

Accessing Fields§

Two ways to access fields on a Form instance

Initial Data§

form = ContactForm(
    initial={
        'name': 'First and Last Name',
    },
)
>>> form['name'].value()
'First and Last Name'

Validation§

Validating the Form§

Field Validation§

Field Cleaning§

.clean_email()§

class ContactForm(forms.Form):
    name = forms.CharField(
        label=_("Name"),
        max_length=255,
    )

    email = forms.EmailField(
        label=_("Email address"),
    )

    def clean_email(self):

        if (self.cleaned_data.get('email', '')
            .endswith('hotmail.com')):

            raise ValidationError("Invalid email address.")

        return self.cleaned_data.get('email', '')

Form Validation§

.clean() example§

class ContactForm(forms.Form):
    name = forms.CharField(
        label=_("Name"),
        max_length=255,
    )

    email = forms.EmailField(label=_("Email address"))
    confirm_email = forms.EmailField(label=_("Confirm"))

    def clean(self):
        if (self.cleaned_data.get('email') !=
            self.cleaned_data.get('confirm_email')):

            raise ValidationError("Email addresses do not match.")

        return self.cleaned_data

Initial != Default Data§

Passing Extra Information§

class MyForm(forms.Form):
    def __init__(self, *args, **kwargs):
        self._user = kwargs.pop('user')
        super(MyForm, self).__init__(*args, **kwargs)

Tracking Changes§

Testing§

Testing Forms§

  • Initial states
  • Field Validation
  • Final state of cleaned_data

Unit Tests§

import unittest

class FormTests(unittest.TestCase):
    def test_validation(self):
        form_data = {
            'name': 'X' * 300,
        }

        form = ContactForm(data=form_data)
        self.assertFalse(form.is_valid())

Test Data§

from rebar.testing import flatten_to_dict

form_data = flatten_to_dict(ContactForm())
form_data.update({
        'name': 'X' * 300,
    })
form = ContactForm(data=form_data)
assert(not form.is_valid())

Rendering Forms§

Idiomatic Form Usage§

from django.views.generic.edit import FormMixin, ProcessFormView

class ContactView(FormMixin, ProcessFormView):
    form_class = ContactForm
    success_url = '/contact/sent'

    def form_valid(self, form):
        # do something -- save, send, etc
        pass

    def form_invalid(self, form):
        # do something -- log the error, etc -- if needed
        pass

Form Output§

Three primary "whole-form" output modes:

<tr><th><label for="id_name">Name:</label></th>
  <td><input id="id_name" type="text" name="name" maxlength="255" /></td></tr>
<tr><th><label for="id_email">Email:</label></th>
  <td><input id="id_email" type="text" name="email" maxlength="Email address" /></td></tr>
<tr><th><label for="id_confirm_email">Confirm email:</label></th>
  <td><input id="id_confirm_email" type="text" name="confirm_email" maxlength="Confirm" /></td></tr>

Controlling Form Output§

{% for field in form %}
{{ field.label_tag }}: {{ field }}
{{ field.errors }}
{% endfor %}
{{ field.non_field_errors }}

Additional rendering properties:

Customizing Rendering§

You can specify additional attributes for widgets as part of the form definition.

class ContactForm(forms.Form):
    name = forms.CharField(
        max_length=255,
        widget=forms.Textarea(
            attrs={'class': 'custom'},
        ),
    )

You can also specify form-wide CSS classes to add for error and required states.

class ContactForm(forms.Form):
    error_css_class = 'error'
    required_css_class = 'required'

Customizing Error Messages§

Built in validators have default error messages

>>> generic = forms.CharField()
>>> generic.clean('')
Traceback (most recent call last):
  ...
ValidationError: [u'This field is required.']

error_messages lets you customize those messages

>>> name = forms.CharField(
...   error_messages={'required': 'Please enter your name'})
>>> name.clean('')
Traceback (most recent call last):
  ...
ValidationError: [u'Please enter your name']

Error Class§

Error Class§

from django.forms.util import ErrorList

class ParagraphErrorList(ErrorList):
    def __unicode__(self):
        return self.as_paragraphs()

    def as_paragraphs(self):
        return "<p>%s</p>" % (
            ",".join(e for e in self.errors)
        )

form = ContactForm(data=form_data, error_class=ParagraphErrorList)

Multiple Forms§

Avoid potential name collisions with prefix:

contact_form = ContactForm(prefix='contact')

Adds the prefix to HTML name and ID:

<tr><th><label for="id_contact-name">Name:</label></th>
  <td><input id="id_contact-name" type="text" name="contact-name"
       maxlength="255" /></td></tr>
<tr><th><label for="id_contact-email">Email:</label></th>
  <td><input id="id_contact-email" type="text" name="contact-email"
       maxlength="Email address" /></td></tr>
<tr><th><label for="id_contact-confirm_email">Confirm
     email:</label></th>
  <td><input id="id_contact-confirm_email" type="text"
       name="contact-confirm_email" maxlength="Confirm" /></td></tr>

Forms for Models§

Model Forms§

Model Forms§

from django.db import models
from django import forms

class Contact(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    notes = models.TextField()

class ContactForm(forms.ModelForm):
    class Meta:
        model = Contact

Limiting Fields§

class ContactForm(forms.ModelForm):

    class Meta:
        model = Contact
        fields = ('name', 'email',)



class ContactForm(forms.ModelForm):

    class Meta:
        model = Contact
        exclude = ('notes',)

Overriding Fields§

class ContactForm(forms.ModelForm):

    name = forms.CharField(widget=forms.TextInput)

    class Meta:
        model = Contact

Instantiating Model Forms§

model_form = ContactForm()

model_form = ContactForm(
    instance=Contact.objects.get(id=2)
    )

ModelForm.is_valid()§

Testing§

class ModelFormTests(unittest.TestCase):
    def test_validation(self):
        form_data = {
            'name': 'Test Name',
        }

        form = ContactForm(data=form_data)
        self.assert_(form.is_valid())
        self.assertEqual(form.instance.name, 'Test Name')

        form.save()

        self.assertEqual(
            Contact.objects.get(id=form.instance.id).name,
            'Test Name'
        )

Form Sets§

Form Sets§

Defining Form Sets§

from django.forms import formsets

ContactFormSet = formsets.formset_factory(
    ContactForm,
)
formset = ContactFormSet(data=request.POST)

Factory kwargs:

Using Form Sets§

<form action=”” method=”POST”>
{% formset %}
</form>

Or more control over output:

<form action=”.” method=”POST”>
{% formset.management_form %}
{% for form in formset %}
   {% form %}
{% endfor %}
</form>

Management Form§

formset.is_valid()§

FormSet.clean()§

from django.forms import formsets

class BaseContactFormSet(formsets.BaseFormSet):
    def clean(self):
        names = []
        for form in self.forms:
            if form.cleaned_data.get('name') in names:
                raise ValidationError()
            names.append(form.cleaned_data.get('name'))

ContactFormSet = formsets.formset_factory(
    ContactForm,
    formset=BaseContactFormSet
)

Insertion§

Deletion§

ContactFormSet = formsets.formset_factory(
    ContactForm, can_delete=True,
)

Ordering Forms§

ContactFormSet = formsets.formset_factory(
    ContactForm,
    can_order=True,
)

Testing§

from rebar.testing import flatten_to_dict, empty_form_data

formset = ContactFormSet()
form_data = flatten_to_dict(formset)
form_data.update(
    empty_form_data(formset, len(formset))
)

Model Form Sets§

Advanced & Miscellaneous Detritus§

Localizing Fields§

Enable in settings.py:

USE_L10N = True
USE_THOUSAND_SEPARATOR = True # optional

Localizing Fields Example§

And then use the localize kwarg

>>> from django import forms
>>> class DateForm(forms.Form):
...     pycon_ends = forms.DateField(localize=True)

>>> DateForm({'pycon_ends': '3/15/2012'}).is_valid()
True
>>> DateForm({'pycon_ends': '15/3/2012'}).is_valid()
False

>>> from django.utils import translation
>>> translation.activate('en_GB')
>>> DateForm({'pycon_ends':'15/3/2012'}).is_valid()
True

Dynamic Forms§

State Validators§

State Validators§

from django import forms
from rebar.validators import StateValidator, StateValidatorFormMixin

class PublishValidator(StateValidator):
    validators = {
        'title': lambda x: bool(x),
     }

class EventForm(StateValidatorFormMixin, forms.Form):
    state_validators = {
        'publish': PublishValidator,
    }
    title = forms.CharField(required=False)

State Validators§

>>> form = EventForm(data={})
>>> form.is_valid()
True
>>> form.is_valid('publish')
False
>>> form.errors('publish')
{'title': 'This field is required'}