To Do List - Part 4: Adding a form

Welcome to Part 4 of my tutorial series on building a To Do List app with Django. In the previous tutorial, we added views to process requests from the user. In this tutorial, we will create a form that will let users submit new tasks for their To Do List.

The Code

As usual, the code for this tutorial is available on Github. If you would like to follow along, the starting code is here, and the finished code is here.

Step 1: Create a form class

Django takes an object orientated approach to forms. The form classes can give you functionality including:

  • Rendering entire forms in templates

  • Cleaning data received in forms

  • Validating forms with the is_valid method

  • Saving data to the database

We are going to use a ModelForm to create our form.

In your todo app, create a file called

Add the following code:

from django import forms

from todo.models import Task

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ["name"]

Step 2: Import the form into the view

We are going to put the form on the same page as the To Do list, so we are going to add the form to the index view.

In your view, we need to:

  • Import TaskForm from todo.forms

  • Create an instance of the form inside the index view

  • Include the form in the context dictionary

My now looks like this:

# todo/

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

from todo.forms import TaskForm
from todo.models import Task

def index(request: HttpRequest) -> HttpResponse:
    form = TaskForm()
    tasks = Task.objects.all().order_by("-created")

    context = {"tasks": tasks, "form": form}

    return render(request, "temp_index.html", context)

Step 3: Add the form to the template

Go to temp_index.html and add the following code snippet:

    {% csrf_token %}
    <input type="submit" value="Add">

Without having to type out <input> tags for individual fields, we have rendered our form.

However, the form doesn’t work because our view doesn’t have any code to handle submitted forms from the user.

Step 3: Edit our view to process POST requests

Here’s what we need to do to our view:

For a POST request:

  1. Create a form object using the POST request

  2. Check if the data supplied by the user is valid

  3. Create a task in the database using

  4. Send an empty form and the list of tasks back to the user

For other requests

  • Just send an empty form and a list of tasks back to the user

This is what my view now looks like:

def index(request: HttpRequest) -> HttpResponse:

    if request.method == "POST":
        form = TaskForm(data=request.POST)
        if form.is_valid():

    form = TaskForm()
    tasks = Task.objects.all().order_by("-created")

    context = {"tasks": tasks, "form": form}

    return render(request, "temp_index.html", context)

In the browser, I can add new tasks to my to-do list and they appear immediately in the list.

Step 4: Test the form (bonus!)

We can add tests for our form.

We are going to test:

  • Does our form contain a field for the task name?

  • Is a form submitted with a task name valid?

  • Is a form submitted with no name invalid?

These are my tests:

# todo/

from todo.forms import TaskForm

class TestTaskForm(TestCase):
    def test_form_instance(self):
        """Test that form has name field"""
        form = TaskForm()

        self.assertIn("name", form.fields)

    def test_is_valid(self):
        form = TaskForm(data={"name": "Book dentist appointment"})


    def test_empty_form_invalid(self):
        form = TaskForm(data={"name": None})


Step 5: Add attributes to the form

So far, we have rendered the form but we don’t have any control in the template.

It’s very common to not see the form in the template as {{form}} . Some developers prefer to write out the full HTML themselves and others prefer to render form fields individually. Widget Tweaks is a popular package that makes use of the form object in templates but gives you more control.

It is possible to render the form using {{form}} and customise it.

However, the customisation must happen in, not the template.

For example, I can change to:

  • Remove the label

  • Add a placeholder

  • Add an aria-label

  • Add CSS classes

We can do all of these by customising the widget used to render the name field.

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ["name"]

    name = forms.CharField(
                "aria-label": "name",
                "placeholder": "What do you need to do?",
                "class": "form-control form-control-lg inline-block",

I have added three CSS classes but right now our templates don’t load any CSS, we will take care of this in the next step.

Next Steps

Before we take things further, we are going to improve the UI.

Part 5 - Improving the UI