Django REST Framework filtering

The quickest and most flexible way to add filtering to Django REST Framework endpoints is by using the django-filter library.

To get started, install django-filter into your project.

Django Filter installation

In your django-tutorial/requirements.txt file you should add the following text:

django-tutorial/requirements.txt

django-filter==22.1

Now, run pip install -r requirements.txt to install Django Filter.

command-line

(myvenv) ~$ pip install -r requirements.txt
Collecting django-filter==22.1 (from -r requirements.txt (line 1))
  Downloading django-filter-22.1-py3-none-any.whl (7.9MB)
Installing collected packages: django-filter
Successfully installed django-filter-22.1

In this tutorial we are using Django Filter version 22.1 - however, the last Django Filter version to support Python 2.7 was 1.1.0, as a result there are some slight differences in how Django Filter is used in Kolibri.

We have seen how to extend Django using our own blog app - but we can also extend Django using installable libraries like Django Filter. To tell Django that we are using Django Filter in our project, we need to add the django_filters app to our INSTALLED_APPS setting.

We do that in the file mysite/settings.py -- open it in your code editor. We need to find INSTALLED_APPS and add a line containing 'django_filters', just above ]. So the final product should look like this:

mysite/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'rest_framework',
    'django_filters',
]

If we wanted to add Django Filter into all of our ViewSets by default, we could also add this to our settings file:

mysite/settings.py

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

For now, we will leave it as is, and add Django Filter support on a ViewSet by ViewSet basis.

Filtering our blog post API

So far in our API, we have had to either list all blog posts, or look at blog posts individually. With a filter, we are able to specify specific fields to filter the values for.

To do this, we have two options - we can either create an explicit FilterSet class, which explicitly defines which fields are filtered and how, or we can use a shortcut property on the ViewSet to define which fields we want to filter by. When we choose the latter option, a FilterSet class is still created, but it is handled automatically by Django Filter, and we don't have to define the class ourselves.

Let's re-open up this file in our code editor to add the properties to our ViewSet:

blog/views.py

from django.shortcuts import render
from django.utils import timezone

from rest_framework import permissions
from rest_framework import serializers
from rest_framework import viewsets

from django_filters.rest_framework import DjangoFilterBackend

from .models import Post

def post_list(request):
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')
    return render(request, 'blog/post_list.html', {'posts': posts})


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ['id', 'author', 'title', 'text', 'created_date', 'published_date']


class PostViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows posts to be viewed or edited.
    """
    queryset = Post.objects.all().order_by('published_date')
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = PostSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['author']

Let's look at the new properties of the PostViewSet class. The filter_backends property, tells Django REST Framework what kind of filtering this ViewSet can do - in this case, we are using the Django Filter Backend, so Django REST Framework will defer to Django Filter to filter the objects we return. The filterset_fields property defines a list of fields that we want to be able to filter by, in this instance we have only specified author which will allow us to filter the returned blog posts by the primary key of the user that we are interested in.

In Django Filter 1.1.0, this property is called filter_fields not filterset_fields, so if you are working on Kolibri, the former is required.

To test that this filtering is working, run the server and go to http://127.0.0.1:8000/api/post?author=1 to view the browsable Django REST Framework API showing the posts, filtered to the user with id of 1.

More complex filters

Having to know the primary key of an author can be sufficient for finding the right results - however, sometimes it might be more useful to be able to do something a bit more complex - like looking up blog posts by matching against the author's name. To do this, we will need to create our own FilterSet to create a more complex lookup expression - the filterset_fields do the equivalent of author=<value> in a Django Queryset filter, so we need to construct a more complex query.

If we were to do this filter in a Django queryset we would do something like Post.objects.filter(author__username__contains=<value>) - here we are doing two things in the filter lookup expression - first we are traversing the foreign key to author using the __username so that we can do a match against the author's username and not just their id value. With the __contains we are adding a lookup expression to say we are not looking for an exact match, but rather just looking for a username that contains the value. So if we looked up with a value of tim then we would match against usernames tinytim and astima.

Let's define a FilterSet to handle this filtering (while also retaining our previous foreign key filtering on author):

blog/views.py

from django.shortcuts import render
from django.utils import timezone

from rest_framework import permissions
from rest_framework import serializers
from rest_framework import viewsets

from django_filters import FilterSet
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.rest_framework import CharFilter

from .models import Post

def post_list(request):
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')
    return render(request, 'blog/post_list.html', {'posts': posts})


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ['id', 'author', 'title', 'text', 'created_date', 'published_date']


class PostFilter(FilterSet):
    username = CharFilter(field_name="author__username", lookup_expr="contains")

    class Meta:
        model = Post
        fields = ['author', 'username']


class PostViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows posts to be viewed or edited.
    """
    queryset = Post.objects.all().order_by('published_date')
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = PostSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = PostFilter

Instead of filterset_fields we have now defined filterset_class and are point to our new PostFilter class - these two options are mutually exclusive, you can have one or the other, but not both.

In Django Filter 1.1.0, the property to set the filter set is called filter_class not filterset_class, so if you are working on Kolibri, the former is required.

Similarly to the PostSerializer, the PostFilter defines a class Meta that shows which model is being referred to, and which fields it should filter by. Because author is a field directly on the Post model, we do not have to specify anything further - but username is not a field of the Post model, so we have to explicitly define the filtering.

If we look at that in more detail we see: username = CharFilter(field_name="author__username", lookup_expr="contains") as the definition. CharFilter is a filter used for filtering by string values. We specify the field_name, which in this case is the expression for traversing the author foreign key and filtering by the username - author__username.

In Django Filter 1.1.0, the property to set the field name is called name not field_name, so if you are working on Kolibri, the former is required.

Lastly, the lookup_expr argument sets the type of lookup that we will be using when we use this filter - in this case we set it as contains so that we can lookup for any post by an author whose username contains some specific characters.

To test that this filtering is working, run the server and go to http://127.0.0.1:8000/api/post?username= to view the browsable Django REST Framework API showing the posts, filtered to your user.

results matching ""

    No results matching ""