Django Forms

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

This article assumes you have a project called DjangoSandBox in an application called formsintroduction.

A Django Form instance automatically provides all the heavy work when working with HTTP forms; rendering UI, validation, cleaning and collection of data.

Basic Form

A basic Django Form is a class which inherits from forms.Form. It defines fields and looks similar to a Django model.

The following example defines a form which has a name and height field.

# forms.py
from django import forms

class BasicFormExample(forms.Form):
    name = forms.CharField(label="Name", min_length=3, max_length=30)

    height = forms.DecimalField(max_value=2, help_text="Height in meters", label="Height (M.CM)", decimal_places=2)  

We can now use an instance of BasicFormExample within a function view; we pass it to the context object dictionary with a key of ‘form’.

When posting back we create an instance of the form initiated with the POST data. We can then ask the form if it is valid; this returns false if the model or additional form validation (which we will add later) is invalid.

We can ask for cleaned_data of the fields from the form; this gets us validated data fields. In this example we simply collect the data and pass them into a HttpResponse instance.

If the form is not considered valid we pass the form instance back to the request object.

#views/py

from django.http import HttpResponse
from django.shortcuts import render

from .forms.bastic_form_example import BasicFormExample

def basic(request):
    if request.POST:
        form = BasicFormExample(request.POST)
        if form.is_valid():
            name = form.cleaned_data['name']
            height = form.cleaned_data['height']

            return HttpResponse("{0} is {1} tall".format(name, height))
        else:
            return render(request, "formsintroduction/basic_form_example.html", {'form': form})
    else:
        form = BasicFormExample()
        return render(request, 'formsintroduction/basic_form_example.html', {'form': form})

For our template we simply need to ask for the form to render itself. We use as_table function upon the form instance passed into the template context data.

Django provides the functions as_table, as_ul and as_p; this simply defines which html element to surrounds the rendered input element; TD, UI or P.

We also need to add a csrf_token to ensure we are protected from Cross-site request forgery attacks. When working with forms ‘{% csrf_token %}’ should always be included on the page; as long as we include this Django will take care of the rest.

We also add a button to submit the form.

<!-- templates/formsintroduction/basic_form_example.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

We now hook in our view function into our URL routing config.

#formsintroduction/urls.py
from django.conf.urls import patterns, url

from . import views

urlpatterns = 
    patterns('',
url(r'^basic/$', views.basic, name="form_basic"),

We need to make sure that our app’s urls.py is configured within the project urls.py. I was working with an app called formsintroduction inside a project called DjangoSandBox.

#DjangoSandBox/urls.py
from django.conf.urls import patterns, include, url

from formsintroduction import urls as formsintroduction_urls

urlpatterns = 
    patterns('',
              url(r'^formsintroduction/', include(formsintroduction_urls, namespace="formsintroduction")),
             )

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/basic/

Validation

This assumes that you have an understanding of adding validation onto Django models. If not you can read about this in my post here.

We automatically get server and client side validation for our model. For the example above this would ensure the name is of a minimum 3 characters and a maximum of 30. For the height it ensures a maximum value of 2 and a maximum number of two decimal places.

In short the constraints and validation that we place upon our model are automatically validated against in our form.

We can add additional validation onto our form.

Note: Where possible validation should be placed upon the model to support code reuse. Only bespoke validation for the page should be added directly onto the form.

Lets first write a custom validation function for our name field. We want to ensure that it starts with an upper case letter and then has at least 2 lower case letters. Upon failure we raise a django.core.exceptions.ValidationError.

# validators.py 
from re import match

from django.core.exceptions import ValidationError

def validate_name(a_string):
    a_match = match(r"[A-Z][a-z]{2,}$", a_string)
    if a_match:
        return True

    raise ValidationError("Name must be a capitalised single word")

We now change our form to look like the following:

# forms/basic_form_example.py
from django import forms
from ..validators import validate_name

error_name = {
    'required': 'You must enter a name!',
    'invalid': 'Invalid name format.'
}


class BasicFormExample(forms.Form):
    name = forms.CharField(label="Name", min_length=3, max_length=30, error_messages=error_name,
                           initial="Jim", validators=[validate_name])

    height = forms.DecimalField(max_value=2, help_text="Height in meters", label="Height (M.CM)", decimal_places=2)

    def clean(self):
        cleaned_data = super(BasicFormExample, self).clean()

        name = cleaned_data.get("name")
        height = cleaned_data.get("height")

        if name is None and height is None:
            msg = "Both name and height are required!!!"
            self.add_error('name', msg)
            self.add_error('height', msg)
            raise forms.ValidationError(msg)

        return self.cleaned_data

    def clean_name(self):
        name = self.cleaned_data.get("name")

        if " " in name:
            raise forms.ValidationError("No spaces in the name please")

        return name

In the example above:

  • Field.validators takes a list of custom validators or inbuilt Django validators. These have been covered in my post about Django Model Validation.
  • Field.error_messages takes a dictionary of error key to error message. We use it to provide custom error messages to the existing validation. This is the same funcitonality as per models. This has been covered in my post about Django Model Validation.
  • The clean_field provides a hook to place specific field validation. The above is a bad example as it could easily be added as a validator against the model or form field using regular expressions; however this is only an example.
    • Raising a ValidationError here automatically associates the error to the field
  • The clean function provides an additional hook to perform cross field validation or anything else which might not be applicable or possible anywhere else.
    • We can add validation messages against a field with the add_error function.
    • We can raise a general validation message against the form by raising a ValidationError.

Note: Validation error messages are associated to a field or their form. the association affects where they are displayed to the user. Field errors are displayed next to the field they arose from, form errors are displayed above all fields. These have been covered in my post about Django Model Validation.

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/basic/

Basic Form With HTML

In the above example we use the form passed into the template context data to render itself. If we need more control over the placement or composition of the html there is nothing stopping us from writing the html ourselves.

This example renders the form using html and Django template operators.

<!-- templates/formsintroduction/basic_form_with_html_example.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    {{ form.non_field_errors }}
    <div class="fieldWrapper">
        {{ form.message.errors }}
        <label for="{{ form.name.id_for_label }}">Name:</label>
        {{ form.name }}
    </div>
    <div class="fieldWrapper">
        {{ form.height.errors }}
        {{ form.height.label_tag }}
        {{ form.height }}
    </div>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

We have access to some properties on the form which come in handy when building a working form page:

  • The form.name and form.height renders the applicable input for the fields name and height respectively;. This includes any client side validation.
  • The form.height.errors and form.name.errors renders the fields errors when and if applicable.
  • The form.name.id_for_label reutrns the id to be used for the name field label.
  • The form.height.label_tag is used to render the label for the height column instead of constructing it ourselves with the id_for_label property.
  • The form.non_field_errors returns true or false depending upon if we have any validation errors which are not related directly to a field.
  • The form.message.errors renders all non field validation error messages.

We can now hook in our new template into what is a virtual copy of the view function called in the example above.

#views.py

from django.http import HttpResponse
from django.shortcuts import render

from .forms.class_form_example import ClassBasedForm
def basic_html(request):
    if request.POST:
        form = BasicFormExample(request.POST)
        if form.is_valid():
            name = form.cleaned_data['name']
            height = form.cleaned_data['height']

            return HttpResponse("{0} is {1} tall".format(name, height))
        else:
            return render(request, "formsintroduction/basic_form_with_html_example.html", {'form': form})
    else:
        form = BasicFormExample()
        return render(request, 'formsintroduction/basic_form_with_html_example.html', {'form': form})

Hook in our new view into our URL route config:

url(r'^basichtml/$', views.basic_html, name="form_basic_html"),

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/basichtml/

Class Form

If we have a model class we can automatically generate a form from our model definition.

First our models; we are going to resuse the PhoneAddress and PhoneContact from the previous article when we looked at views. They sit in the viewsintroduction application.

# viewsintroduction/models.py
class PhoneAddress(Model):
    number = models.IntegerField()
    street_name = models.CharField(max_length=20)
    city = models.CharField(max_length=20)

    def __str__(self):
        return "{0} {1} {2}".format(self.number, self.street_name, self.city)

    def get_absolute_url(self):
        return reverse('viewsintroduction:address', args=[self.id])

We can create a form based upon our model by simply inheriting from ModleForm and setting the model property of the Meta internal class.

We also define the fields property which defines which fields of the model are going to be displayed; this is mandatory.

#forms/clased_based_form.py

.from django import forms
from django.forms import ModelForm

from viewsintroduction.models import PhoneAddress

class ClassBasedForm(ModelForm):        
    class Meta:
        model = PhoneAddress
        fields = ("city", "street_name", "number")
        labels = {'number': "House No."}
        help_texts = {'number': "This is the number of the house."}

With regards to validation:

  • We can implement the clean and clean_field functions
  • We can add custom validation messages via the error_messages property of the class meta. This takes a dictionary keyed upon each field name, which in itself takes a dictionary keyed upon each error type and the custom error message.
  • We can override the init function and set any settings as required; for example adding a custom validator onto a field.

A fuller example adding in validation and more meta information:

#forms/clased_based_form.py

from django import forms
from django.forms import ModelForm

from viewsintroduction.models import PhoneAddress
from ..validators import validate_name

class ClassBasedForm(ModelForm):
    def __init__(self, *args, **kwargs):
        super(ClassBasedForm, self).__init__(*args, **kwargs)
        self.fields["city"].validators.append(validate_name)

    def clean(self):
        cleaned_data = super(ClassBasedForm, self).clean()

        city = cleaned_data.get("city")
        street_name = cleaned_data.get("street_name")
        number = cleaned_data.get("number")

        if city is None and street_name is None and number is None:
            msg = "None of the fields have been set!!!"
            for a_field in ('city', 'street_name', 'number'):
                self.add_error(a_field, msg)
            raise forms.ValidationError(msg)

        return self.cleaned_data

    def clean_city(self):
        city = self.cleaned_data.get("city")

        if " " in city:
            raise forms.ValidationError("No spaces in the city name please")

        return city

    class Meta:
        model = PhoneAddress
        fields = ("city", "street_name", "number")
        labels = {'number': "House No."}
        help_texts = {'number': "This is the number of the house."}
        error_messages = {
            'number': {
                'required': "We need a number of the house!",
                'max_length': "We only accept houses up to 20!"}
        }

Our view function looks similar to previous examples with a few changes:

  • Our function view is going to be passed in a parameter called pk which defaults to none. We will strip this from the URL within our URL routing config shortly.
  • We use get_object_or_404 to return the record. It is passed the primary key value and the model class. It will return the record or raise a 404 HTTP response if no record exists.
  • We need to initiate the form with the post data if we are posting back otherwise the record instance. This is done with request.POST or None and instance=an_address being passed into the form constructor. If both are not set then we initiate the form from no data; i.e. the starting point.
  • Calling form.save will automatically create or update the address record.
  • Our address record knows its own URL via the get_absolute_url function. We can simply call redirect upon the record to redirect to the detail view we made in the previous article.
# views.py
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404

from .forms.class_form_example import ClassBasedForm
from viewsintroduction.models import PhoneAddress

def class_form(request, pk=None):
    if pk:
        an_address = get_object_or_404(PhoneAddress, pk=pk)
        form = ClassBasedForm(request.POST or None, instance=an_address)
    else:
        form = ClassBasedForm(request.POST or None, initial={'city': 'Plymouth'})

    if request.POST and form.is_valid():
        an_address = form.save(commit=True)
        return redirect(an_address)
    else:
        return render(request, 'formsintroduction/class_based_form_example.html', {'form': form})

In our template we will simply get our form to render itself:

<!-- templates/formsintroduction/class_based_form_example.html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

We now hook in our view function with two a create and edit URL:

#urls.py

url(r'^class/create/$', views.class_form, name="form_class_create"),
url(r'^class/(?P<pk>[0-9]+)/edit/$', views.class_form, name="form_class_edit"),

With regards to the edit URL matching. We match the URL class/edit/xxx/ where xxx is the primary key. The primary key is made up of one or more numerical digits as noted by [0-9]+. We assign the integral into a variable called pk. The string ?P reads make a parameter for reference later on.

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/class/create
http://127.0.0.1:8000/formsintroduction/class/1/edit

Note: replace 1 with the id of your record

Model Form Factory

If we don’t want to customise the class based form we can use the modelform_factory class.

#views.py

from django.forms import modelform_factory
from django.shortcuts import render, redirect, get_object_or_404

from viewsintroduction.models import PhoneAddress
#views.py

def model_form_factory_form(request, pk=None):
    phone_address_form = modelform_factory(PhoneAddress, fields=("city", "street_name", "number"))

    if pk:
        an_address = get_object_or_404(PhoneAddress, pk=pk)
        form = phone_address_form(request.POST or None, instance=an_address)
    else:
        form = phone_address_form(request.POST or None)

    if request.POST and form.is_valid():
        an_address = form.save(commit=True)
        return redirect(an_address)
    else:
        return render(request, 'formsintroduction/form_factory_example.html', {'form': form})

A simple template which asks the form to render itself.

<!-- templates/formsintroduction/form_factory_example.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Form Factory Example</title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

Again we set us two URLs; one for creating and one for editing.

#urls.py
url(r'^factory/create/$', views.model_form_factory_form, name="form_factory_create"),
url(r'^factory/(?P<pk>[0-9]+)/edit/$', views.model_form_factory_form, name="form_factory_edit"),

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/factory/create
http://127.0.0.1:8000/formsintroduction/factory/1/edit

Note: replace 1 with the id of your record

Widgets

Widgets provide extra customisation onto our fields; for example we can add a PasswordInput widget onto a CharField to give us a password input box which displays * instead of the password.

PasswordInput = forms.CharField(max_length=10, widget=forms.PasswordInput)

We can can add a Textarea onto a CharField to allow multi-line text input.

TextField = forms.CharField(widget=forms.Textarea)

We can render an input as hidden:

HiddenInput = forms.CharField(max_length=10, widget=forms.HiddenInput, initial='a')

The normal choice field be be changed to radios or a multi select list box:

TITLE_CHOICES = (
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
)

# Drop Down
ChoiceField = forms.CharField(max_length=3, widget=forms.Select(choices=TITLE_CHOICES))

# Radio
RadioSelect = forms.CharField(max_length=10, widget=forms.RadioSelect(choices=TITLE_CHOICES))

# Multiple display and select
CheckboxSelectMultiple = forms.CharField(max_length=10, widget=forms.CheckboxSelectMultiple(choices=TITLE_CHOICES))

We can create a date selection widget limited to certain years or months:

YEARS_ = years = (2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009)

SelectDateWidget = forms.CharField(max_length=10, widget=SelectDateWidget(years=YEARS_))

A lot of the widgets can take parameters for extra customisation. Check the Django documentation for available widgets and their options.

We provide a working example for all of these widgets in the “A Full Example” section below.

Joins

Joins fields for 1:1 and 1:n are implemented by a ModelChoiceField form field defining the possible records to be joined to with the queryset parameter.

ForeignKey = forms.ModelChoiceField(queryset=ChildOfMany.objects.all())

OneToOneField = forms.ModelChoiceField(queryset=ChildOfOne.objects.all())

Many to many join fields are implemented in the same way but with a ModelMultipleChoiceField form field.

ManyToManyField = forms.ModelMultipleChoiceField(queryset=ChildManyToMany.objects.all())

We provide a working example for all of these in the “A Full Example” section below.

A Full Example

For every model field there is a recommended form field and as such editor and potentially a widget. The following is an example with “one of virtually everything”.

Lets take a model which has a field of virtually every type including joins of all types; 1:1, 1:n and 1:m.

#models.py
from django.db import models
from django.db.models import Model

TITLE_CHOICES = (
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
)


class ChildOfMany(Model):
    CharField = models.CharField(max_length=10)

    def __str__(self):
        return self.CharField


class ChildOfOne(Model):
    CharField = models.CharField(max_length=10)

    def __str__(self):
        return self.CharField


class ChildManyToMany(Model):
    CharField = models.CharField(max_length=10)

    def __str__(self):
        return self.CharField

    class Meta:
        verbose_name_plural = "children"


class ModelFieldsToFormFields(Model):
    ChoiceField = models.CharField(max_length=3, choices=TITLE_CHOICES)
    CharField = models.CharField(max_length=10)
    CommaSeparatedIntegerField = models.CharField(max_length=50)
    EmailField = models.EmailField()
    TextField = models.TextField()
    URLField = models.URLField()

    DateField = models.DateField()
    DateTimeField = models.DateTimeField()
    TimeField = models.TimeField()

    # FileField = models.FileField()
    # ImageField = models.ImageField()
    # FilePathField = models.FilePathField()

    BigIntegerField = models.BigIntegerField()
    BooleanField = models.BooleanField()
    NullBooleanField = models.NullBooleanField()

    PositiveIntegerField = models.PositiveIntegerField()
    PositiveSmallIntegerField = models.PositiveSmallIntegerField()
    SlugField = models.SlugField()
    SmallIntegerField = models.SmallIntegerField()
    DecimalField = models.DecimalField(decimal_places=2, max_digits=5)
    FloatField = models.FloatField()
    IntegerField = models.IntegerField()
    GenericIPAddressField = models.GenericIPAddressField()

    # Joins
    ForeignKey = models.ForeignKey(ChildOfMany)
    ManyToManyField = models.ManyToManyField(ChildManyToMany)
    OneToOneField = models.OneToOneField(ChildOfOne)

Our form, using Django advised mappings of form field and widgets to model fields will then look like this:

# forms/complex_form_with_widgets.py

from django import forms
from django.forms.extras.widgets import SelectDateWidget

from ..models import TITLE_CHOICES, ChildOfOne, ChildManyToMany, ChildOfMany, ModelFieldsToFormFields

YEARS_ = years = (2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009)


class ComplexFormWithWidgets(forms.ModelForm):
    ChoiceField = forms.CharField(max_length=3, widget=forms.Select(choices=TITLE_CHOICES))
    CharField = forms.CharField(max_length=10)
    CommaSeparatedIntegerField = forms.CharField()
    EmailField = forms.EmailField()
    TextField = forms.CharField(widget=forms.Textarea)
    URLField = forms.URLField()

    DateField = forms.DateField()
    DateTimeField = forms.DateTimeField()
    TimeField = forms.TimeField()

    # FileField = models.FileField()
    # ImageField = models.ImageField()
    # FilePathField = forms.FilePathField((match="*.py", recursive=True)

    BigIntegerField = forms.IntegerField(min_value=-9223372036854775808, max_value=9223372036854775807.)
    BooleanField = forms.BooleanField()
    NullBooleanField = forms.NullBooleanField()

    PositiveIntegerField = forms.IntegerField(min_value=0, max_value=2147483647)
    PositiveSmallIntegerField = forms.IntegerField(min_value=0, max_value=32767)
    SlugField = forms.SlugField()
    SmallIntegerField = forms.IntegerField(min_value=-32768, max_value=32767)
    DecimalField = forms.DecimalField()
    FloatField = forms.FloatField()
    IntegerField = forms.IntegerField()
    GenericIPAddressField = forms.GenericIPAddressField()

    # Joins
    ForeignKey = forms.ModelChoiceField(queryset=ChildOfMany.objects.all())
    ManyToManyField = forms.ModelMultipleChoiceField(queryset=ChildManyToMany.objects.all())
    OneToOneField = forms.ModelChoiceField(queryset=ChildOfOne.objects.all())

    # More Widgets
    PasswordInput = forms.CharField(max_length=10, widget=forms.PasswordInput)
    HiddenInput = forms.CharField(max_length=10, widget=forms.HiddenInput, initial='a')
    RadioSelect = forms.CharField(max_length=10, widget=forms.RadioSelect(choices=TITLE_CHOICES))
    CheckboxSelectMultiple = forms.CharField(max_length=10, widget=forms.CheckboxSelectMultiple(choices=TITLE_CHOICES))
    SelectDateWidget = forms.CharField(max_length=10, widget=SelectDateWidget(years=YEARS_))

    class Meta:
        model = ModelFieldsToFormFields
        exclude = ()  # Better to set fields explicitly

There is no real change in our view function from the model form example previously.

#views.py

from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404

from .forms.complex_form_with_widgets import ComplexFormWithWidgets
from .models import ModelFieldsToFormFields
def complete_model_example(request, pk=None):
    if pk:
        an_instance = get_object_or_404(ModelFieldsToFormFields, pk=pk)
        form = ComplexFormWithWidgets(request.POST or None, instance=an_instance)
    else:
        form = ComplexFormWithWidgets(request.POST or None)

    if request.POST and form.is_valid():
        if form.is_valid():
            an_instance = form.save(commit=True)
            return HttpResponse("Created with id={0}".format(an_instance.id))
    else:
        return render(request, 'formsintroduction/complete_model_form_example.html', {'form': form})

A simple html template asking the form to render itself:

<!-- templates/formsintroduction/complete_model_form_example.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<form method="post" action="">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <p><input type="submit" value="Create"/></p>
</form>
</body>
</html>

Hook in our view to a create and edit URL within our URL routing config:

# urls.py
url(r'^complete_model/create/$', views.complete_model_example, name="form_complete_create"),
url(r'^complete_model/(?P<pk>[0-9]+)/edit/$', views.complete_model_example, name="form_complete_edit")

To test the page run the development server and navigate to:

http://127.0.0.1:8000/formsintroduction/complete_model/create
http://127.0.0.1:8000/formsintroduction/complete_model/1/edit

Note: replace 1 with the id of your record

Additional Notes

We can pass data to the HTML via the attrs parameter which takes a dictionary of property to name. The following makes our char field take on a CSS class name.

CharField = forms.CharField(max_length=10, attrs={'class': 'special'})

References

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s