Welcome to Part 3 of my tutorial series to build a To Do list app. In the previous tutorial we added a model. This means we have a table in the database to store data but we need a way for our users to interact with it.
In this tutorial, we are going to add URLs, Views and Templates. If you’re not familiar with how Django works, here’s a recap:
- The Template is a HTML file with placeholders for data. There will be at least one template per page.
- The URLs file (sometimes known as “routes”) maps the URL requested by the user to the view that will handle the request.
- The View accepts the HTTP request from the user. It will fetch the required data and return a response, often by rendering a template.
To create a To Do list app, we will need views to view, add, edit and delete data. In this tutorial, we will focus on displaying data for the user.
Here’s what we are going to do:
- Add a View
- Create a template
- Configure the URLs
- Test the View
The Code
If you want to follow along, you can clone this repository. If you’d prefer to skip to the finished code, then check out this repository.
Step 1: Add a view
The purpose of Django views is to accept a HTTP request and return a HTTP response. The response can often include HTML to render in the user’s browser.
We want this view to return the tasks in our To Do list.
Go to views.py
and add the following code:
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from todo.models import Task
def index(request: HttpRequest) -> HttpResponse:
tasks = Task.objects.all().order_by("-created")
context = {"tasks": tasks}
return render(request, "temp_index.html", context)
Let’s go through this line by line:
Function definition
def index(request: HttpRequest) -> HttpResponse:
I have included type-hints here to help explain how the view works. Leave them out (e.g. def index(request):
) if you prefer.
The input to a view is a request, which is an instance of the HttpRequest
class. The view is a function that returns an instance of the HttpResponse
class.
Queryset
tasks = Task.objects.all().order_by("-created")
This line fetches all tasks from the database and orders them by their date of creation in descending order (newest first).
The returned tasks
is a queryset.
Context
context = {"tasks": tasks}
Context is a dictionary of data that will be passed to the template. We can use the context to pass all kinds of data to the template, including querysets, forms, constants and text.
Render
return render(request, "temp_index.html", context)
The render
function injects the data supplied in context
into the template (temp_index.html
) and returns the response as HTML to the user.
Step 2: Create a template
In your todo
app (same directory as views.py
), create a directory called templates
. Inside that folder, create a file called temp_index.html
.
In temp_index.html
, add the following code:
<h1>To Do List</h1>
<table>
<thead>
<th>Name</th>
<th>Status</th>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>{{task.name}}</td>
<td>{{task.status}}</td>
</tr>
{% endfor %}
</tbody>
</table>
Django uses its own templating language for the templates (docs)
It allows us to iterate through the tasks and access attributes of each task object.
This is a very basic template with no CSS. It won’t look good but we come back later to improve the styling.
Step 3: Add URLs
In Step 1, we created a view. In Step 2, we created a template. The final step is to create our URLs so we can view the contents of the template in the browser.
In your todo
app, create a file called urls.py
and add the following code:
# todo/urls.py
from django.urls import path
from todo import views
urlpatterns = [path("", views.index, name="index")]
The path
function accepts a URL path, a view and a name.
The URL path is the URL requested by the user. As the supplied path is an empty string, it means we intend it to use the domain without an extension e.g. localhost:8000
. If we were to supply a value like “list”, then the pattern will match localhost:8000/list
.
The second argument is the View that will handle the request if the user’s URL matches the pattern. In this case, it is the index
view we wrote in Step 1.
The name is a reference to the URL pattern. This is useful when referring to URLs in templates because it means you don’t have to write out the full URL every time.
Include the app URLs
Right now, if you go to localhost:8000
, you will still see this screen:

This is because we haven’t included the URL patterns of our app in the URL patterns for the project.
To fix this, go to mysite/urls.py
and add the following code:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("todo.urls"))
]
This instructs Django to look in our new urls.py
in the todo
app, when matching the user’s URL to a view.
Now, go back to the browser, reload the page and you should now see your to do list:

Step 4: Test your view (bonus)
The final step is to add some tests for our view.
# todo/tests.py
import datetime
from django.db.models import QuerySet
from django.http import HttpResponse
from django.urls import reverse
from django.test import TestCase, Client
from todo.models import Task
class TestTaskModel(TestCase):
...
class TestIndexView(TestCase):
@classmethod
def setUpTestData(cls):
cls.url = reverse("index")
cls.client = Client()
cls.task = Task.objects.create(name="book dentist appointment")
def test_index_view_returns_httpresponse(self):
"""Test our view returns a HttpResponse"""
response = self.client.get(self.url)
self.assertTrue(isinstance(response, HttpResponse))
def test_status_code(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_context(self):
"""Tests the context contains queryset of tasks"""
response = self.client.get(self.url)
self.assertIn("tasks", response.context)
tasks = response.context["tasks"]
self.assertTrue(isinstance(tasks, QuerySet))
self.assertEqual(tasks.first(), self.task)
def test_template_used(self):
response = self.client.get(self.url)
self.assertTemplateUsed(response, "temp_index.html")
Here we test:
- Does the view return an instance of the
HttpResponse
class? - Does the returned
HttpResponse
instance have a status code of200
? - Does the context contain tasks?
- Is the expected template used?
The key to testing views is the Client
class provided by Django. Using reverse
to get the URL from the name, we can simulate requests to the view and analyse the response.
Next Steps
We have configured URLs for our project, created a View to handle requests from the user, and a Template to render data returned from the view.
The next step is to add a form so that users can add their own tasks to the To Do list.