Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 44

Filtering Tasks by Status and Priority

Recommended resources

Hello! Welcome back to our journey of building a full-stack task manager.

In our last lesson, you implemented a powerful search feature, allowing users to find tasks using free-text queries. That was a huge step in making the application more usable. Now, we're going to add another layer of control: structured filtering.

While search is great for finding specific text, filtering allows users to narrow down their list based on predefined criteria, like a task's status or priority. This lesson will teach you how to implement dropdowns or input fields to sort and view tasks by these properties.

To do this efficiently, we'll use a popular third-party package called django-filter. While you could build this manually like we started to with the search feature, using a dedicated library will keep your code cleaner, reduce boilerplate, and give you access to more advanced features out of the box.

By the end of this lesson, your application will look something like this, with dedicated controls for filtering the task list:

Product Listing with Filtering Options
A web application interface displaying a list of products alongside various filtering options, including categories, a search bar, a featured checkbox, and a date picker. This is similar to what we will build for our tasks.

1. Setting Up django-filter

First, we need to install the package and tell our Django project that we're using it.

  1. Install the package: Open your terminal (with your virtual environment activated) and run the following command:

    pip install django-filter
    
  2. Update settings.py: Add 'django_filters' to your INSTALLED_APPS list in your project's settings.py file. It's conventional to place third-party apps after Django's built-in apps but before your own apps.

    # In settings.py
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    
        # Third-party apps
        'django_filters',
    
        # Your apps
        'your_app_name', # Make sure this is the actual name of your app
    ]
    

The following video gives a great overview of this initial setup and the entire filtering process. We'll follow its structure closely.

How to easily filter objects in Django

Watch this section from the video 'How to easily filter objects in Django' by Cloud With Django to see the installation process in action.

Watch from the beginning until 02:04. This part covers installing django-filter and adding it to INSTALLED_APPS, which is exactly what we just did.


2. Creating a FilterSet

A FilterSet is a class that defines how to filter a specific model. It's very similar in concept to a Django ModelForm. You tell it which model to use and which fields you want to be able to filter by.

First, create a new file named filters.py inside your application directory (the same directory that contains your models.py and views.py).

Now, let's define our filter. For this example, let's assume your Task model has a status field (e.g., 'To Do', 'In Progress', 'Done') and is associated with the logged-in user.

# In your_app_name/filters.py

import django_filters
from .models import Task

class TaskFilter(django_filters.FilterSet):
    class Meta:
        model = Task
        fields = ['status'] # We will start by filtering on the 'status' field

This simple TaskFilter tells django-filter that we want to create a filter for the Task model based on its status field. By default, this will create an "exact match" filter.

The django-filter documentation and the next section of the video provide more detail on how this FilterSet works.

Getting Started - django-filter 25.2 documentation

The official documentation provides a clear, concise explanation of how a FilterSet is constructed. Take a moment to read it.

Read the section titled 'The filter'. Focus on the 'Generating filters with Meta.fields' part. Notice how it's similar to creating a ModelForm.


3. Integrating the Filter into Your View

Now we need to use this TaskFilter in our TaskListView. We'll modify the view to handle the filter form, apply the filtering to the queryset, and pass the filter object to the template.

Here's how we'll combine the filtering logic with the user-specific data and the search functionality you already built.

# In your_app_name/views.py

from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from .models import Task
from .filters import TaskFilter # Import the filter

class TaskListView(LoginRequiredMixin, ListView):
    model = Task
    context_object_name = 'tasks'
    template_name = 'task_list.html'

    def get_queryset(self):
        # Start with all tasks for the current user
        queryset = super().get_queryset().filter(user=self.request.user)

        # Apply the filter
        self.filterset = TaskFilter(self.request.GET, queryset=queryset)
        
        # Use the filtered queryset for further filtering
        queryset = self.filterset.qs

        # Apply the search query
        query = self.request.GET.get('q')
        if query:
            queryset = queryset.filter(
                Q(title__icontains=query) | 
                Q(description__icontains=query)
            )
        
        return queryset.order_by('created')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # Pass the filterset and search query to the template
        context['filterset'] = self.filterset
        context['search_query'] = self.request.GET.get('q', '')
        return context

Key Changes:

  1. Import TaskFilter: We bring in the filter we just created.
  2. get_queryset():
    • We first get the tasks for the logged-in user.
    • We then instantiate TaskFilter, passing it the request.GET data (which contains any submitted filter values) and the user's queryset. We store it as self.filterset so we can access it in get_context_data.
    • We update our queryset to be the filtered result from self.filterset.qs.
    • Finally, we apply the search logic to this already filtered queryset. This allows search and filtering to work together!
  3. get_context_data(): We add self.filterset to the context. This will allow us to render the filter form in the template.

4. Updating the Template

The final step is to add the filter form to task_list.html. django-filter automatically generates a form from your FilterSet.

Place this code right next to your search form.

<!-- In task_list.html -->

<div class="row mb-4">
    <div class="col-md-6">
        <h5>Search Tasks</h5>
        <form method="GET" class="">
            <div class="input-group">
                <input type="text" name="q" class="form-control" placeholder="Search title or description..." value="{{ search_query }}">
                <button class="btn btn-outline-primary" type="submit">Search</button>
            </div>
        </form>
    </div>
    <div class="col-md-6">
        <h5>Filter Tasks</h5>
        <form method="GET" class="">
            <div class="input-group">
                {{ filterset.form.as_p }}
                <button class="btn btn-outline-secondary" type="submit">Filter</button>
            </div>
        </form>
    </div>
</div>

{% if search_query %}
    <p>Showing search results for: <strong>"{{ search_query }}"</strong></p>
{% endif %}

<h3>Your Tasks</h3>
<table class="table">
    <thead>
        <!-- your table headers -->
    </thead>
    <tbody>
        {% for task in tasks %}
        <tr>
            <!-- your table data -->
        </tr>
        {% empty %}
        <tr>
            <td colspan="5">No tasks found.</td>
        </tr>
        {% endfor %}
    </tbody>
</table>

Key Changes:

  • Filter Form: We created a new <form method="GET">.
  • {{ filterset.form.as_p }}: This is the magic part. It renders the form fields generated by our TaskFilter. Since we only specified status, it will render a text input for status. The .as_p wraps the fields in <p> tags.
  • Submit Button: A button to apply the filter.

Now, run your server and try it out! You can type a status into the new input field, click "Filter," and the list will update. Notice that the filter value and the search query both appear in the URL.

Test your understanding!

Let's say your Task model also has a priority field (e.g., a CharField). How would you modify your TaskFilter in filters.py to allow users to filter by both status and priority?

Show answer

You would simply add 'priority' to the fields list in the Meta class.

# In your_app_name/filters.py

class TaskFilter(django_filters.FilterSet):
    class Meta:
        model = Task
        fields = ['status', 'priority'] # Add priority here

django-filter will automatically add a new input field for priority to the form.


5. Improving the Filter with ChoiceFilter

A text input for status works, but it's not ideal. Users have to remember the exact names of the statuses. A dropdown menu (<select>) would be much more user-friendly.

We can achieve this by customizing our TaskFilter. Let's assume your Task model has choices defined like this:

# In your_app_name/models.py

class Task(models.Model):
    STATUS_CHOICES = [
        ('todo', 'To Do'),
        ('in_progress', 'In Progress'),
        ('done', 'Done'),
    ]
    # ... other fields ...
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='todo')
    # ...

If you have choices set up, you can tell django-filter to use a ChoiceFilter. Update your filters.py:

# In your_app_name/filters.py

import django_filters
from .models import Task

class TaskFilter(django_filters.FilterSet):
    # Explicitly declare the status field as a ChoiceFilter
    status = django_filters.ChoiceFilter(choices=Task.STATUS_CHOICES)

    class Meta:
        model = Task
        fields = ['status'] # The field is now declared above

By explicitly defining status as a ChoiceFilter and passing it the same choices from your model, django-filter will now render a dropdown menu instead of a text input. Refresh your page to see the change!


Conclusion

Fantastic work! You have now implemented a powerful and user-friendly filtering system. This is a standard feature in modern web applications, and you've learned how to build it using a clean, maintainable, and industry-standard approach with django-filter.

Key Takeaways:

  • django-filter is a powerful library for adding filtering to QuerySets with minimal boilerplate.
  • A FilterSet class defines which model and fields to filter on, similar to a ModelForm.
  • You can integrate filtering into a ListView by instantiating the FilterSet in get_queryset and passing it to the template via get_context_data.
  • The filter form is rendered in the template using {{ filterset.form }}.
  • You can customize filter fields (e.g., using ChoiceFilter) for a better user experience.

Preview of the Next Lesson:

Your task list is getting more functional, but what happens when a user has hundreds of tasks? The page would become incredibly long. In the next lesson, we will address this by implementing pagination, which will break your long list of tasks into multiple pages. We will also see how to make sure our new pagination controls work correctly with our filtering and search features.