To Do List - Part 7: Deleting Tasks

Welcome to Part 7 of my tutorial series on how to create a To Do List app with Django. The tutorial series so far has covered models, views, forms, templates and updating tasks.

In this part, we will cover deleting tasks. Users will be able to remove tasks from their To Do List in the browser, which will trigger the task to be removed from the database.

To do this, we need to take the following steps:

  1. Write the view

  2. Add a URL

  3. Add a link to the template

  4. Write tests

The Code

The code for this tutorial is available on GitHub. Before & after.

Step 1: Write the view

Our view needs to do the following:

  • Identify which task the deletion request is for

  • Delete the task

  • Redirect the user to the index view

# todo/views.py

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse

def delete_task(request: HttpRequest, task_id: int) -> HttpResponse:

    task = get_object_or_404(Task, id=task_id)
    task.delete()

    success_url = reverse("index")

    return HttpResponseRedirect(success_url)

Let’s go through this step by step.

Identify the task

The URL will provide the ID of the task to the view. The get_object_or_404 function is a shortcut that will return a task or raise a 404 error.

task = get_object_or_404(Task, id=task_id)

The following snippet is also valid:

from django.http import Http404

try:
    task = Task.objects.get(id=task_id)
except Task.DoesNotExist:
    raise Http404()

Delete the task

The Django base model (django.db.models.Model) provides a method to delete objects:

task.delete()

Redirect to the index view

Once the task is deleted, we want to render the index template. Rather than repeat the code of the index view to get the tasks and an empty form, we can use a HTTP redirect.

The HttpResponseRedirect needs a URL to redirect to. To get the URL, we can use the reverse function. This gets the full URL from the name supplied in urls.py.

success_url = reverse("index")

return HttpResponseRedirect(success_url)

Step 2: Add a URL

The next step is to go into urls.py and add a URL pattern that will call the delete_task view we just wrote.

We need to make sure the URL includes the ID of the task that is going to be deleted.

# todo/urls.py

from django.urls import path
from todo import views

urlpatterns = [
    path("", views.index, name="index"),
    path(
        "update/<int:task_id>/<str:new_status>",
        views.update_task_status,
        name="update_status",
    ),
    path("delete/<int:task_id>", views.delete_task, name="delete"),
]

Go to todo/templates/partials/todo_row.html. This template has an icon intended to delete tasks.

We need to add a URL to the link. Make sure you include the ID of the task that is to be deleted.

<td>
    <a class="m-2" href="{% url 'delete' task.id %}">
        <i class="link-dark bi-x-lg"></i>
    </a>
</td>

Step 4: Write tests for the view

We need to test:

  • Does the view return a 302 status code (302 means there was a redirect)?

  • Does the view delete a task?

  • If the view cannot find the task, does it raise a 404?

# todo/tests.py
from django.test import TestCase, Client
from django.urls import reverse

class TestDeleteTaskView(TestCase):
    def setUp(self):
        self.task = Task.objects.create(name="Book dentist appointment")
        self.client = Client()

    def test_delete_task(self):
        url = reverse("delete", args=[self.task.id])

        response = self.client.get(url)

        self.assertEqual(response.status_code, 302)

    def test_delete_task_deletes_task(self):
        url = reverse("delete", args=[self.task.id])

        self.assertEqual(Task.objects.count(), 1)

        self.client.get(url)

        self.assertEqual(Task.objects.count(), 0)

    def test_delete_task_raises_404(self):
        bad_id = 999
        self.assertFalse(Task.objects.filter(id=bad_id).exists())

        url = reverse("delete", args=[bad_id])

        response = self.client.get(url)

        self.assertEqual(response.status_code, 404)

How to test delete views

Views are functions that return a HTTP response. We need our tests to call the view with a HTTP request and any parameters from the URL.

Django provides a class called Client which can be used to simulate responses to a given URL.

We can then write our tests by making assertions about the HTTP response returned by the view.

For example, in this test we check the HTTP status code of the response. If it is 302, then we know there has been a redirect, as expected.

def test_delete_task(self):
    url = reverse("delete", args=[self.task.id])

    response = self.client.get(url)

    self.assertEqual(response.status_code, 302)

In this test, we can test the view actually deletes a task by counting the number of tasks in the database before and after making the call to the client.

def test_delete_task(self):
    url = reverse("delete", args=[self.task.id])

    response = self.client.get(url)

    self.assertEqual(response.status_code, 302)

Next Up

Part 8 - Filtering Tasks