To Do List - Part 3: URLs, Views & Templates

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:

  1. Add a View

  2. Create a template

  3. Configure the URLs

  4. 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 of 200?

  • 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.

Part 4 - Adding a Form