Django: Using django-filter plugin with CBVs
Django-filter is an extremely powerful plugin for Django which extends the framework by providing a common set of APIs for easily setting up and managing user-facing filters on models. This plugin provides an immense amount of new functionality, all while avoid further boilerplate code for developers.
This plugin is so incredibly useful that the very popular Django Rest Framework plugin is built on top of the django-filter plugin for helping build out its REST API views.
Unfortunately, the django-filters documentation, while extremely concise and well-written, does not provide an example of how to integrate it with class based views. In this article, I will remedy this by providing one such example.
Our end goal in this article is to utilize the django-filter plugin to very quickly write a Django app which lets users browse a library’s available books and filter based on author, genre, rating, and other fields. Of course, all of this will be accomplished only using class based views.
Sample Models
Below are the Django models we’ll use for our example.
class Author(models.Model):
first_name = models.CharField(max_length=64)
last_name = models.CharField(max_length=64)
class Category(models.Model):
name = models.CharField(max_length=64)
class Book(models.Model):
RATINGS = [
(1, "Trash"),
(2, "C-tier"),
(3, "B-tier"),
(4, "A-tier"),
(5, "S-tier"),
]
title = models.CharField(max_length=128)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
librarian_rating = models.IntegerField(choices=RATINGS, help_text="Our local librarian's rating of this book.")
These models are meant to represent a collection of books found in a library.
Next, let’s construct the django-filter class for this model, adapting from the examples already provided by the excellent django-filter documentation.
from django_filters.filterset import FilterSet
from .models import Book
class BookFilter(FilterSet):
class Meta:
model = Book
fields = ['author', 'category', 'librarian_rating']
As with any django-filter FilterSet
classes, this is fully customizable to your desires. Consult the django-filter docs for all the customization options available. For now, we’ll move on to connecting this to our CBV.
Connecting the Filter to the CBV
Our filter-less class based view for our Book
and Author
classes appears below. This CBV provides an easy way for our end users to browse every book, all at once, without any filtering:
from django.views.generic import ListView
from .models import Book
class BookListView(ListView):
template_name = 'books.html'
model = Book
context_object_name = 'books'
Now, if we wanted to provide our users with the ability to filter on these books, we’ll need to leverage the filter class we defined above, and connect it to this CBV.
To do this, we’ll leverage the FilterView
mixin that comes bundled with the django_filters
plugin. This involves two simple steps:
- Switching our class inheritance from
ListView
toFilterView
in the class definition. - Setting the
filterset_class
field in the class and point it to theBookFilter
class we created earlier.
from django_filters.views import FilterView
from .filters import BookFilter
from .models import Book
class BookListView(FilterView):
template_name = 'books.html'
model = Book
context_object_name = 'books'
filterset_class = BookFilter
This updated FilterView
comes with a new filter
variable which sends the entire FilterSet
object to the template. Inside this object, is the FilterSet.form
which is a standard Django form object, pre-populated with all the fields necessary for the user to manipulate the filter settings. We can interact with it on the template side as we would any normal form:
{% extends 'base.html' %}
{% block title %}Books{% endblock %}
{% block content %}
<h1>Books</h1>
<div class="row my-5 mx-3">
<div class="col">
<div class="card">
<form method="get" action="">
<h5 class="card-header">Filters</h5>
<div class="card-body">
{{ filter.form.as_p }}
</div>
<div class="card-footer text-center">
<input class="btn btn-secondary" type="submit" value="Filter" />
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Category</th>
<th scope="col">Rating</th>
</tr>
</thead>
<tbody>
{% for book in books %}
<tr>
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>{{ book.category }}</td>
<td>{{ book.get_librarian_rating_display }}</td>
</tr>
{% empty %}
<tr><td colspan="6"><i>No books in this filter.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
And voila! It’s that simple! Thanks to the power of Django and django-filter, we now have a fully-functional filter form for the user, and we did this by writing very little extra code.