To Do List: Part 8 - Filtering Tasks

In this tutorial, we are going to implement a basic filter.

This will work by specifying how we want to filter our tasks in the URL and display the filtered data set by loading a new page.

The advantage of this approach is you won't need to write any JavaScript at all. The disadvantage is your application will have to load a new page every time you change a filter.

If you want to implement a filter without reloading the whole page each time, then you will need to use htmx or a JavaScript framework like React.

The Code

If you would like to follow this tutorial, then you can pull this branch for the starting code.

The finished code can be found here.

Step 1: Add a URL

The first step is to add a URL. We are going to filter the tasks by their status, so we will create a URL pattern that includes a task status.

Go to todo/urls.py and add a URL pattern that will invoke a view called "filter_tasks".

My urls.py now looks like this:

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"),
    path("filter/<str:status>", views.filter_tasks, name="filter")
]

Step 2: Add a view

The next step is to add a view. The view will perform the filtering by making a query to the database using the status provided in the URL.

We need to create a new view. The view needs to:

  • Check that the status is valid. If the status isn't valid, then raise an error.

  • Fetch the tasks from the database

  • Send the tasks and the current status to the template

We also want the user to be able to use the form to add new tasks without changing the behaviour of the filter. Therefore, our view should also provide an empty form in response to a GET request and process completed forms in response to a POST request.

Views need to return a HttpResponse, we will use the index.html template. The context (the second argument of render) will include the tasks, the form and the user's chosen status.

My view looks like this:

def filter_tasks(request: HttpRequest, status: str) -> HttpResponse:

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

    if request.method == "POST":
        form = TaskForm(data=request.POST)
        if form.is_valid():
            form.save()

    tasks = Task.objects.filter(status=status).order_by("-created")

    form = TaskForm()

    context = {
        "tasks": tasks,
        "form": form,
        "Status": Task.StatusChoice,
        "filter_choice": status,
    }

    return render(request, "index.html", context)

In the snippet above, we created a QuerySet called tasks, which we use to get all of the tasks in our to do list that have the provided status and order them with the most recently created task first. If you would like to learn how to create more complex queries, then please refer to my guide to filtering Django QuerySets.

Step 3: Update the Template

The final step is to add links to the status tabs on the homepage so that users can filter the tasks.

In Part 5, we added the HTML for the homepage and styled it with Bootstrap.

Above the task list, we added some tabs with our status options.

We need to do two things:

  1. Add styling to the selected status, so the user knows which one has been selected

  2. Add URLs to the href attribute, so that clicking on a status loads a new page with filtered tasks.

How to add logic to CSS

We want the selected status to be styled differently so it is clear which status the user has chosen.

We can use template tags in the class names to selectively apply CSS classes.

    <ul id="tabs" class="nav nav-pills mb-4 pb-2">
        <li class="nav-item">
            <a class="nav-link {% if filter_choice == 'ALL' %}active{% endif %}" href="{% url 'index' %}">
                All
            </a>
        </li>
        <li class="nav-item">
            <a class="nav-link {% if filter_choice == Status.TODO %}active{% endif %}"
                href="{% url 'filter' Status.TODO %}">To Do</a>
        </li>
        <li class="nav-item">
            <a class="nav-link {% if filter_choice == Status.DOING %}active{% endif %}"
                href="{% url 'filter' Status.DOING %}">Doing</a>
        </li>
        <li class="nav-item">
            <a class="nav-link {% if filter_choice == Status.DONE %}active{% endif %}"
                href="{% url 'filter' Status.DONE %}">Done</a>
        </li>
    </ul>

Conclusion

We have added filtering to our application without writing any JavaScript. We do this by putting the user's chosen status into the URL. We wrote a view to process the request from the user. The view fetches tasks from the database, filtering the results by status. By using the same template as the homepage, it gives the illusion that the user is still on the same page.

While this approach is simple and doesn't require JavaScript, we do need to reload a new page after each interaction. If you want to filter without reloading the page, then these are your options:

  • Use vanilla JavaScript and an AJAX request to the server

  • Use a JavaScript framework like React or Vue

  • Use htmx