django-filter: filtering a foreign key model property
django-filter is a fantastic library that makes easy to add complex filters in Django. While it requires very little code to get up and running, it can be a bit tricky to figure out how to do more non-standard things, like for example filtering against a foreign key model property.
In this brief post we'll see with an example how I've achieved this.
The models
Consider two simple Django models, Book
and Author
:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey("Author", on_delete=models.CASCADE)
def __str__(self):
return self.title
class Author(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
def __str__(self):
return self.full_name
Book
has a ForeignKey
to Author
, and Author
has a full_name
property that returns the full name of the author.
My use case is to be able to filter the Book
list by the Author
full_name
property.
Django filter would make easy to filter against a model field, that is, things that are actual database columns, like author__first_name
or author__last_name
, but filtering against a model property like author__full_name
is not so straightforward.
In other words, we would like to hit our page at /books/?author_full_name=clarke
and get all the books written by Arthur C. Clarke.
The filter
To clarify things better, here's how the filter would look like if we were filtering against a model field:
from django_filters import FilterSet
from .models import Book
class BookFilterSet(FilterSet):
class Meta:
model = Book
fields = ["author__last_name"]
This will work fine for hitting /books/?author_first_name=clarke
. What happens instead if we try to filter against a model property? Like this:
from django_filters import FilterSet
from .models import Book
class BookFilterSet(FilterSet):
class Meta:
model = Book
fields = ["author__full_name"]
Django filter will complain with this error:
TypeError: 'Meta.fields' must not contain non-model field names: author__full_name
Let's see how to fix this!
Fixing the filter, wiring up the view and the queryset
To make Django filter work with a model property, first off we need to define a custom filter for that property:
from django_filters import FilterSet, CharFilter
from .models import Book
class BookFilterSet(FilterSet):
author_full_name = CharFilter(lookup_expr="icontains", label="Author full name")
class Meta:
model = Book
fields = ["other_fields_here"]
Here, the BookFilterSet
is instructed to use a CharFilter
for the author_full_name
property. In this example, we also use the icontains
lookup expression.
Next up, we wire up the Django view alongside with the get_queryset
method. The "trick" to make Django filter work with a model property is to use an annotation to add a new field to the queryset, that is, the author_full_name
field:
from django.db.models import F
from django.db.models import Value as V
from django.db.models.functions import Concat
from django_filters.views import FilterView
from .models import Book
from .filters import BookFilterSet
class BookList(FilterView):
model = Book
filterset_class = BookFilterSet
template_name = "books/book_list.html"
def get_queryset(self):
return (
super()
.get_queryset()
.annotate(
author_full_name=Concat(
F("author__first_name"), V(" "), F("author__last_name")
)
)
)
Here's a breakdown of the queryset:
super().get_queryset()
gets the base queryset.annotate()
adds a new field to the querysetauthor_full_name
, by concatenatingauthor__first_name
andauthor__last_name
fields.
With this change in place, Django filter is now able to filter against the author_full_name
property. This way, we can hit /books/?author_full_name=clarke
in Django, and have the filter work properly.
Here's the query again:
def get_queryset(self):
return (
super()
.get_queryset()
.annotate(
author_full_name=Concat(
F("author__first_name"), V(" "), F("author__last_name")
)
)
)
Notice the use of F()
and V()
expressions. F()
is used to reference a field in the database. The database function Concat()
instead is used to concatenate the fields. If you want, you can read more about expressions in Query Expressions, and functions in Database Functions.
Conclusion
django-filter is a fantastic, easy to use, well-documented library for filtering in Django. While it works almost out of the box with model fields, it requires a bit of extra work to handle Django model properties.
In this post, we saw how achieve that by using an annotation to add a new field to the queryset, and then using that field in the filter.
Another alternative, if you don't mind using a third party library, is to use django-queryable-properties.
Thanks for reading!