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:
Write the view
Add a URL
Add a link to the template
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"),
]
Step 3: Add a link to the template
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)