To Do List - Part 6: Edit Objects

In the previous tutorials, we have been building a To Do list app.

We created a page that lists all the tasks and added a form so that the user can add their own.

In the previous tutorial, we made improvements to our templates. Here, we added buttons to change the status of tasks and delete them.

Today, we will get those buttons working.

Code

To follow this tutorial, you can pull this branch of the repository. The finished code for this tutorial can be found here.

Step 1: Add a view

Our view needs to:

  • Only attempt to update the task status if the new status is belongs to the the StatusChoice class.

  • Query the database to get the task based on the ID provided in the URL.

  • Update the task status

  • Redirect to the index view

Unlike the index view, the update_task_status view receives two arguments: task_id which is an integer, and new_status which is a string. These are both provided in the URL.

In views.py, add the following code:

def update_task_status(
    request: HttpRequest, task_id: int, new_status: str
) -> HttpResponse:

    if not new_status in Task.StatusChoice.values:
        return HttpResponseBadRequest("Invalid status")

    task = get_object_or_404(Task, id=task_id)

    task.status = new_status
    task.save()

    success_url = reverse("index")

    return HttpResponseRedirect(success_url)

Step 2: Add a URL

Our next step is to add a URL pattern that will call the view.

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",
    ),
]

<int:task_id> is a placeholder for an integer. It will be passed to the view as task_id.

<str:new_status> is a placeholder for a string. It will be passed to the view as new_status

Make sure the names of arguments match the arguments in the view you defined in Step 1.

Here is an example of a pattern that will match the view:

localhost:8000/update/12/doing

Step 3: Add tests (optional)

Add tests to verify the correct response is returned by the view.

The view returns a 302 (redirect) if the task update was successful.

It returns a 400 (bad request) if the view was called with an invalid status.

It returns a 404 (not found) if the view cannot find a task with the supplied ID.

We should test all three outcomes.

# todo/tests.py

class TestUpdateView(TestCase):
    def setUp(self):
        self.task = Task.objects.create(
            name="Book dentist appointment", status=Task.StatusChoice.TODO
        )
        self.client = Client()

    def test_task_status_update(self):
        """Test the view updates the status of the task"""
        self.url = reverse("update_status", args=[self.task.id, Task.StatusChoice.DONE])

        self.assertEqual(self.task.status, Task.StatusChoice.TODO)

        response = self.client.get(self.url)

        self.assertEqual(response.status_code, 302)

        self.task.refresh_from_db()

        self.assertEqual(self.task.status, Task.StatusChoice.DONE)

    def test_task_status_update_raise_404(self):
        """Test a 404 is raised if attempts to update a task that doesn't exist"""

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

        url = reverse("update_status", args=[bad_id, Task.StatusChoice.DOING])

        response = self.client.get(url)

        self.assertEqual(response.status_code, 404)

    def test_task_status_update_bad_request(self):
        """Test a Bad Request is raised if user attempts to update status to a value
        that isn't one of the status choices"""

        invalid_status = "Not one of the choices"

        self.assertNotIn(invalid_status, Task.StatusChoice.values)

        url = reverse("update_status", args=[self.task.id, invalid_status])

        response = self.client.get(url)

        self.assertEqual(response.status_code, 400)

Here, we test the view using the Client class imported from django.test.

Views can be tested by providing a URL to the client instance.

We get the URL by using the reverse function. It accepts the URL pattern name (defined in urls.py ) and a list of arguments in order they appear in the URL.

Our view redirects to the index view once the task has been updated. We do this so we don’t have to repeat the code to fetch tasks and a form. As a result, a successful HTTP response will be 302 rather than 200.

Step 4: Update the template

The final step is to add the URL to the links in the template.

Here is an example:

href="{% url 'update_status' task.id Status.TODO %}"

You need the URL pattern name you defined in urls.py wrapped in quotations, followed by the arguments separated by a space.

<td>
    {% if task.status == Status.TODO %}
     <div class="btn btn-danger border">To Do</div>
    {% else %}
     <a class="btn btn-light border" href="{% url 'update_status' task.id Status.TODO %}">To Do</a>
    {% endif %}

    {% if task.status == Status.DOING %}
    <div class="btn btn-primary border">Doing</div>
    {% else %}
    <a class="btn btn-light border" href="{% url 'update_status' task.id Status.DOING %}">Doing</a>
    {% endif %}

    {% if task.status == Status.DONE %}
    <div class="btn btn-success border">Done</div>
    {% else %}
    <a class="btn btn-light border" href="{% url 'update_status' task.id Status.DONE %}">Done</a>
    {% endif %}
</td>

Result

You should now be able to click the buttons to change the status of a task.

Next Up

Part 7 - Deleting Tasks from the Database