Django Forms 1

Tutorial on how to use forms in Django

This story will show how to use forms in Django.

Prerequisites

I'm working on an Ubuntu 20.04, Django 3.1.1 and django-extensions 3.0.9 for good measure.

This will not be a complete from zero to hero tutorial, rather I'll assume that you know enough basic Django to be able to apply the shown yourself.

Setup

If you wanna follow along, setup a simple django project:

python3 -m venv ./venv
source venv/bin/activate
pip install django django-extensions
django-admin startproject config .
python manage.py startapp products

update the settings.py file with the new app:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'products',
]

...

TEMPLATE_DIR = os.path.join(BASE_DIR,"templates")
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [TEMPLATE_DIR],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Models

We are setting up a project to handle sales from shops; To that extend, we need a Shop and Product model:

from django.db import models


class Shop(models.Model):
    name = models.CharField(max_length=100)


class Product(models.Model):
    shop = models.ForeignKey(Shop, related_name="products", on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)

Forms

We will start by creating a simple form so we are able to create a Shop in the system. We will begin with a function based view and a manually created form, and from there gradually use more and more of Django's included functionality.

Shop form in a function based view

in a forms.py file in the app folder, create the form like this:

from django import forms


class ShopForm(forms.Form):
    name = forms.CharField(label='The shop name', max_length=100)

We also need a view:

from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import ShopForm


def create_shop(request):
    if request.method == 'POST':
        form = ShopForm(request.POST)

        if form.is_valid():
            return HttpResponseRedirect('/')
    else:
        form = ShopForm()

    return render(request, 'create_shop.html', {'form': form})

Setup the urls in a urls.py:

from django.urls import path

from . import views


urlpatterns = [
    path('shop/create', views.create_shop, name='create_shop'),
]

And finally, a template:

<form action="/your-name/" method="post">
    {% csrf_token %}
    {{ form }}
    <input type="submit" value="Submit">
</form>

We should now have:
Shop form

Right now, if you enter anything in the shop form and click submit, nothing really happens, you are simply redirected to / as specified in the:

        if form.is_valid():
            return HttpResponseRedirect('/')

But, we really wanna save the new shop and we do this after the form.is_valid() and before the return HttpResponseRedirect('/'):

        if form.is_valid():
            name = form.cleaned_data.get('name', None)
            if name:
                new_shop = Shop(name=name)
                new_shop.save()

            return HttpResponseRedirect('/')

After the form.is_valid() has been executed, the form will contain a dictionary called cleaned_data with all the form data in it, after it has been validated and 'cleaned' (eg. made into Python types). If you printed out the form.cleaned_data, you would have gotten:

{'name': 'abc'}

If we enter a name now and submit the form, we would create an entry in the database.

Just for fun, what does the request.POST contain that we initialize the form with? if we print it out we get:

<QueryDict: {'csrfmiddlewaretoken': ['mGIi8M3YJ2X21sBRDoMr1jVRHZ3ar2jl3akkwQrIgKSXAHYGW804QXVw3PqeAInj'], 'name': ['ccc']}>

So, it contains the CSRF token as well as the 'uncleaned' data.

What if instead, we wanted to have the object creation logic (eg. create the new shop) stay in the form instead of in the view? we could create a .save() method on the form and call that from the view. The form could look like:

class ShopForm(forms.Form):
    name = forms.CharField(label='The shop name', max_length=100)

    def save(self):
        name = self.cleaned_data.get('name', None)
        new_shop = Shop(name=name)
        new_shop.save()

and we update the view to be:

        if form.is_valid():
            name = form.cleaned_data.get('name', None)
            if name:
                form.save()

            return HttpResponseRedirect('/')

If we try and create a new shop, it will still work.

is_valid()

Lets take a closer look at what the is_valid() method actually does.

1 If the form has bound data, then the form.full_clean() is called.

2 form.full_clean() iterates over the form fields and each fields also iterates over:

--- to_python() is called, creating the python type of the raw data.

--- Data is validated with field specific rules which includes custom validators.

--- Any custom made clean_FIELD(...) methods are called.

3 form.full_clean() then executes the form.clean() method, where validation that require access to multiple fields can take place (for example in a password form, ensure that password1 and password2 matches)

Failures in any of these steps raises a ValidationError(...).

If no ValidationErrors are raised then the method returns True, and False otherwise.

Modelform instead of Form

Lets swap the ShopForm we created for a ModelForm. This saves us a few lines of code.

class ShopForm(forms.ModelForm):
    class Meta:
        model = Shop
        fields = ['name', ]

and we will update our view as well:

def create_shop(request):
    if request.method == 'POST':
        form = ShopForm(request.POST)

        if form.is_valid():
            form.save()
            return HttpResponseRedirect('/')
    else:
        form = ShopForm()

    return render(request, 'create_shop.html', {'form': form})

Everything will work just like before, but we don't have to reimplement the .save() method. Additionally, the form will inherent the validation logic from the model which will also save us some lines of code.

Class based FormView

Lets try and get the modelform to work with a class based view, in this case a FormView.

class CreateShopView(FormView):
    form_class = ShopForm
    template_name = "create_shop.html"
    success_url = "/"

    def form_valid(self, form):
        form.save()
        return super().form_valid(form)

We also need to update our url a bit:

urlpatterns = [
    path('shop/create', views.CreateShopView.as_view(), name='create_shop'),
]

And again, everything works as expected.

Class based CreateView

We can make it even simpler by using a CreateView, a generic view, which will make our view even simpler:

from django.views.generic.edit import CreateView

class CreateShopView(CreateView):
    template_name = "create_shop.html"
    form_class = ShopForm
    success_url = "/"

Again, everything will work as before. And with CreateView we can make it even simpler by removing the form altogether and simply adding the model to the view:

class CreateShopView(CreateView):
    template_name = "create_shop.html"
    model = Shop
    fields = "__all__"
    success_url = "/"

No need for a separate form in this case. But, typically, it makes sense to have a separate form as you probably want to have some custom logic in your form anyway.

Extra kwargs for the form with class based views

Imagine we have a slightly different model for the Shop:

from django.db import models
from django.contrib.auth import get_user_model


User = get_user_model()


class Shop(models.Model):
    name = models.CharField(max_length=100)
    updated_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=None, blank=True, null=True)

    def __str__(self):
        return self.name

We now have a updated_by field that we want set whenever the model instance is updated or created (in our case). Now, in the form, we want this done automatically, eg. the logged in user should simply be added to the instance.

In our CreateView version, we could do this by capturing the logged in user and pass this information to the form where it will be saved.

class CreateShopView(CreateView):
    template_name = "create_shop.html"
    form_class = ShopForm
    success_url = "/"

    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.updated_by = self.request.user
        self.object.save()
        return HttpResponseRedirect(self.get_success_url())

    def get_form_kwargs(self, *args, **kwargs):
        kwargs = super().get_form_kwargs(*args, **kwargs)
        kwargs['updated_by'] = self.request.user
        return kwargs  

If we just use this without updating the form we will get an error when trying to create a Shop. This is because, the form doesn't have a 'updated_by' field but is still passed a updated_by keyword argument. So, we need to remove this updated by kwarg before the form gets initialized which can be done by overriding the form's init method:

class ShopForm(forms.ModelForm):
    class Meta:
        model = Shop
        fields = ['name', ]

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('updated_by')
        super().__init__(*args, **kwargs)

References

Django forms official documentation

Django views official documentation

Classy Class Based Views

Blog post on formview