django-filter: filtering a foreign key model property

How to filter a foreign key model property with django-filter.

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 queryset author_full_name, by concatenating author__first_name and author__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!

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!

More from the blog: