Getting Started with Django Filters: A Simple Overview

Getting Started with Django Filters: A Simple Overview

In this article we'll take a look at Django-filter a package that gives us the ability to quickly and efficiently filter our querysets. To fully appreciate what this package does for us, we'll first implement a basic search feature where we write the filtering logic ourselves and then try the same thing using Django-filter.

Pre-requisites

This article assumes you understand how to implement basic Django features such as models, views, forms, templates, etc.

Overview

We are going to work with a Product model, which has some dummy data for querying the database. I'm not going to show the process of setting up a new Django project but I will provide the necessary code for understanding what we are trying to achieve here.

We are going to create a Product model:

# models.py
class Product(models.Model):
    class Category(models.TextChoices):
        CLOTHES = 'Clothes', 'Clothes'
        SHOES = 'Shoes', 'Shoes'

    class Gender(models.TextChoices):
        MALE = 'M', 'Male'
        FEMALE = 'F', 'Female'

    class Color(models.TextChoices):
        BLACK = 'Black', 'Black'
        WHITE = 'White', 'White'
        RED = 'Red', 'Red'
        BLUE = 'Blue', 'Blue'
        GREEN = 'Green', 'Green'
        YELLOW = 'Yellow', 'Yellow'
        ORANGE = 'Orange', 'Orange'
        PURPLE = 'Purple', 'Purple'
        PINK = 'Pink', 'Pink'
        BROWN = 'Brown', 'Brown'
        GRAY = 'Gray', 'Gray'
        GOLD = 'Gold', 'Gold'
        SILVER = 'Silver', 'Silver'
        BEIGE = 'Beige', 'Beige'
        OTHER = 'Other', 'Other'



    name = models.CharField(max_length=200)
    category = models.CharField(max_length=20, choices=Category.choices, default=Category.CLOTHES)
    gender = models.CharField(max_length=20, choices=Gender.choices, default=Gender.MALE)
    color = models.CharField(max_length=20, choices=Color.choices, default=Color.BLACK)
    price = models.FloatField()
    stock = models.IntegerField()

    def __str__(self):
        return self.name

We have a very basic Product with a few attributes. Now, we'll implement a search form and a view to give us search capabilities.

#forms.py
from django import forms

class ProductSearchForm(forms.Form):
    name = forms.CharField(label='Name', max_length=100, required=False,
            widget=forms.TextInput(attrs={'placeholder': 'Search by name'}
    ))

This is a very simple form that allows us to search for products by name. Let's write the view for handling those requests:

# views.py
def home(request):
    name = request.GET.get('name')
    category = request.GET.get('category')
    gender = request.GET.get('gender')
    products = Product.objects.all()
    if name:
        products = Product.objects.filter(name__icontains=name)

    form = ProductSearchForm()
    context = {'products': products, 'form': form}
    return render(request, "core/home.html", context)

This function-based view returns a list of products and a form. As you can see, the products returned depend on whether the user searched for products using the search form we provided. If the user searches for the name of a product, we filter the Product model by doing a case-insensitive lookup and return the results.

We have some data in the database we can use to search for products. If we search for the name 'jacket' we get back the only entry with the name Jacket.

As you can see, this is quite easy and straightforward. However, we have to repeat the same logic for any field in our model that we want to query. Imagine if we had a model with dozens of fields. It would not be fun at all trying to write the logic for every model field. Thanks to Django-filter, we don't have to do all that.

Django-filter

Django-filter provides a simple way to filter down a queryset based on parameters a user provides.

  • Django-filter Documentation

First, you'll need to go to the Django-filter documentation to follow the installation instructions.

We are now going to see how we can recreate the same thing we did using Django-filter. To use Django-filter in our project, we will subclass the Filterset class. Think of it like Django's ModelForm, but for filtering our model instead of only producing forms.

# forms.py
import django_filters
from django import forms
from .models import Product

class ProductFilter(django_filters.FilterSet):
    class Meta:
        model = Product
        fields = ['name', 'category', 'gender', 'price', 'color']

Django-filter mimics Django's ModelForm, so the code is pretty similar. By just subclassing django_filters.FilterSet, specifying a model, and exposing the fields we want to use search parameters, we get a powerful search functionality.

Now let's see how to use it in our views...

# views.py
from django.shortcuts import render
from .forms import ProductFilter
from .models import Product

def home(request):
    products = Product.objects.all()
    product_filter = ProductFilter(request.GET, queryset=products)

    context = {'products': product_filter.qs, 'form': product_filter.form}
    return render(request, "core/home.html", context)

The ProductFilter class we created in our forms.py file takes a request and a queryset. When we pass these two arguments to the ProductFilter class, we get a queryset and a form that can be rendered in our templates. We can access both the form and queryset as form and qs attributes, respectively. These are then passed as context to our template.

And boom! Just like that, we get back a search form with all the fields we specified in our ProductFilter Meta class:

fields = ['name', 'category', 'gender', 'price', 'color']

One thing to note here, though, by default Django-filter uses an exact lookup expression. This means that we have to type in the exact term we are looking up or else we don't get back what we want.

The exact lookup expression is case-sensitive, that's why even though we searched for "jacket," we got back nothing because it's all in lowercase. Now, if we type it in correctly, we get back a result:

This might not be desirable for our needs, so Django-filter gives us lookup expression options. Let's fix that by making the lookup expression case-insensitive. There are a few ways we can do this.

# forms.py

class ProductFilter(django_filters.FilterSet):
    class Meta:
        model = Product
        fields = {
            'name': ['icontains'], 
            'category': ['exact'], 
            'gender': ['exact'], 
            'price': ['gt', 'lt'],
            'color': ['exact'],
        }

We have to convert the fields into a dictionary where the key is the model field we are querying and its corresponding value is a list of lookup expressions. This gives us much control over how we want our users to filter their search.

We can also specify lookup expressions for fields defined in ProductFilter class:

#forms.py

import django_filters
from django import forms
from .models import Product

class ProductFilter(django_filters.FilterSet):

    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains',
            label='Name', 
            widget=forms.TextInput(attrs={'placeholder': 'Search by name'}
    ))

    class Meta:
        model = Product
        fields = ['name', 'category', 'price', 'gender', 'color']

Django-filter mirrors much of what ModelForm does, that's why it gives us the ability to customize the look and feel of the form it generates while also allowing us to set the filters we want on the fields.

One thing I've not mentioned yet is the price field. If you noticed, we have a price is greater than and a price is less than field in the form on the browser. This is because when we specified two lookup expressions, 'price': ['gt', 'lt'], Django-filter split the price field into two. This is also achievable by using the RangeFilter:

# forms.py

class ProductFilter(django_filters.FilterSet):
    price = django_filters.RangeFilter()

Django-filter With Class-Based Views

We can also use Django-filter with class-based views:

#views.py

from django.views.generic import ListView
from .forms import ProductFilter
from .models import Product

class ProductListView(ListView):
    queryset = Product.objects.all()
    template_name = "core/home.html"
    context_object_name = "products"
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        product_filter = ProductFilter(self.request.GET, queryset=queryset)
        self.form = product_filter.form
        return product_filter.qs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["form"] = self.form
        return context

Integrating Django-filter with generic class-based views is also easy. We define a queryset and specify the filter class we want to use for the view. We also assign the form to the class**—**self.form so that we could throw it into our context for our template to be able to render it. And just like that, we have fully integrated Django-filter with generic class-based views.

Django-filter also comes with a FilterView class that we can inherit from:

#views.py
from django_filters.views import FilterView

class ProductFilterView(FilterView):
    model = Product
    filterset_class = ProductFilter
    template_name = "core/home.html"
    context_object_name = "products"
    paginate_by = 10

With this, we get the same functionality we get from using a Generic Class-Based View. This is because FilterView extends Django's generic View class. When we use FilterView, we have to specify a filterset_class.

You may be wondering how we can render the form in your template since we didn't pass anything to the context. FilterView assigns the form to a variable called filter. So, we just need to reference filter in our template to render the form, like so:

<!-- core/home.html --> 
<form>
   <div> 
        {{filter.form.as_p}}
   </div>
   <button type="submit">Search</button>   
</form>

Using Django-filter with Django REST Framework (DRF)

We can also easily integrate Django-filter with DRF. Go to the DRF documentation and follow the instructions on how to set up Django-filter. Once you finish setting it up, you'll need to create a serializers.py file and create a serializer class for your model:

#serializers.py
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['name', 'category', 'gender', 'color', 'price', 'stock']

We'll now create a view to process our requests:

#views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics
from .serializers import ProductSerializer
from .forms import ProductFilter

class ProductListAPIView(generics.ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ProductFilter

We have to make sure we specify the filter_backends and filterset_class fields for Django to pick up our filters.

As you can see, with just a few lines of code, we are able to implement powerful filters for our queries. It's also worth pointing out that Django-filter provides us with a Filter button we can use to search for products using the browsable API:

Conclusion

Django-filter arms us with a powerful tool to quickly and easily create filters for our projects by doing all the heavy lifting. This was just a quick guide, but you can do many great things with this package.