The Ultimate Tutorial for Django REST Framework: Filtering (Part 5)

Dominik Kozaczko - Backend Engineer

Dominik Kozaczko

25 April 2019, 5 min read

thumbnail post

What's inside

  1. Let’s get to filtering
  2. Writing your own FilterSet definition

I’m back with another part of my tutorial for Django REST framework.

Be sure to catch up with the work we’ve completed in other parts of the series:

Today, I wanted to take a closer look at another problem: filtering.

Note 1: Check out this exhaustive article that covers some best practices, I refer to it a lot throughout this part.

Note 2: You can find the project code we’re working on in this series in this repository.

Ready to start working on filtering? Let’s jump in!

Let’s get to filtering

In the previous part of this series, we limited the amount of simultaneously downloaded data by pagination. This time, let's think about how we can filter and search our resources easily.

The rental list endpoint (/api/v1/borrowed/) displays all items regardless of whether they’ve been returned or not.

It makes sense to filter this list according to the field status 'returned'. By doing that, we can check which items haven’t been returned yet. The parameter specifying such filtering will be transmitted via GET, e.g.

$ curl http://127.0.0.1:8000/api/v1/borrowings/?missing=true

We can complete this task relatively simply in the `get_queryset` method.

class BorrowedViewset(viewsets.ModelViewSet):
   queryset = models.Borrowed.objects.all()
   serializer_class = serializers.BorrowedSerializer
   permission_classes = [IsOwner]

   def get_queryset(self):
       qs = super().get_queryset()
       only_missing = str(self.request.query_params.get('missing')).lower()
       if only_missing in ['true', '1']:
           return qs.filter(returned__isnull=True)
       return qs 

BTW. Let me remind you that the 'returned' field is a date field. If it contains NULL, it means that the item hasn’t been returned yet. That’s why we use filtering here.

Such an implementation is sufficient for simple use-cases. But with more variables that we may want to filter, it can quickly become difficult to manage this mess. There must be a better way of handling this, right?

Fortunately, there is. The Django REST Framework allows developers to use the django-filter library which drastically simplifies the definition and management of filters.

First, we need to install the library using pip:

$ pip install django-filter  # Note the lack of “s” at the end!

Then we need to update our settings:

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)...
} 

The easiest way to complete the task would be adding the fields by which we want to filter to the attribute 'filterset_fields` in the appropriate view, e.g.

class BorrowedViewset(viewsets.ModelViewSet):
   queryset = models.Borrowed.objects.all()
   serializer_class = serializers.BorrowedSerializer
   permission_classes = [IsOwner]
   filterset_fields = ('to_who', )  # here 

This will let us filer by the person who borrowed an item from us.

$ curl http://127.0.0.1:8000/api/v1/borrowings/?to_who=1

[{"id":1,"what":1,"to_who":1,"when":"2018-01-01T12:00:00Z","returned":null},
{"id":3,"what":1,"to_who":1,"when":"2019-04-17T06:35:22.000236Z","returned":null},
{"id":4,"what":1,"to_who":1,"when":"2019-04-17T06:35:36.546848Z","returned":null}]

Unfortunately, this method has a very serious limitation: we can only give specific values except for NULL.

A partial solution to this problem is replacing the filterset_fields with a dictionary. The keys are field names, and the value is a list of acceptable subfilters compatible with the Django notation, e.g.:

class BorrowedViewset(viewsets.ModelViewSet):
   queryset = models.Borrowed.objects.all()
   serializer_class = serializers.BorrowedSerializer
   permission_classes = [IsOwner]
   filterset_fields = {
       'returned': ['exact', 'lte', 'gte', 'isnull']
   } 

This allows to filter the list of rental items by the return date, including some useful sub-filters like `lte` and `gte,` as well as `isnull` to display the backlog:

$ curl http://127.0.0.1:8000/api/v1/borrowings/?returned__isnull=True

[{"id":1,"what":1,"to_who":1,"when":"2018-01-01T12:00:00Z","returned":null},
{"id":2,"what":2,"to_who":2,"when":"2019-04-16T18:46:16.646649Z","returned":"2019-04-16T18:46:13Z"},
{"id":3,"what":1,"to_who":1,"when":"2019-04-17T06:35:22.000236Z","returned":null},
{"id":4,"what":1,"to_who":1,"when":"2019-04-17T06:35:36.546848Z","returned":null}]

Moreover, this solution allows developers to display filtering options when browsing the API in the HTML mode (the hints can sometimes be confusing though).

Django filters

Writing your own FilterSet definition

Everything I described above is nothing compared to what we can achieve by writing our own FilterSet definition.

Let's start with a simple example.

To see how it works, we will implement the previous functionality of searching for unreturned items. But we’re going to do that in a way that doesn’t reveal the Django notation underneath.

To achieve that, we need to use the BooleanFilter filter that will parse the transferred value. In its parameters, we define the field we want to view and the specific expression to which the value will be passed:

class BorrowedFilterSet(django_filters.FilterSet):
   missing = django_filters.BooleanFilter(field_name='returned', lookup_expr='isnull')

   class Meta:
       model = models.Borrowed
       fields = ['what', 'to_who', 'missing'] 

Then we pass our BorrowedFilterSet to the ViewSet:

class BorrowedViewset(viewsets.ModelViewSet):
   queryset = models.Borrowed.objects.all()
   serializer_class = serializers.BorrowedSerializer
   permission_classes = [IsOwner]
   filterset_class = BorrowedFilterSet  # here 

Thanks to that, we can now do this:

$ curl http://127.0.0.1:8000/api/v1/borrowings/?missing=True

The result should be the same as previously.

Creating a field that allows filtering of outdated rental items is only a bit more work. We will also create a BooleanFilter field, but this time we will pass it the name of the method (it can also be callable) which will perform the filtering on the passed QuerySet.

The entire thing may look like this (note that I omit part of the code from the previous example for clarity, so remember to add a field to Meta.fields):

class BorrowedFilterSet(django_filters.FilterSet):
   overdue = django_filters.BooleanFilter(method='get_overdue', field_name='returned')

   def get_overdue(self, queryset, field_name, value, ):
       if value:
           return queryset.filter(when__lte=pendulum.now().subtract(months=2))
       return queryset 

As a homework assignment, try to simplify this fragment by adding the filtering of expired rental items to the QuerySet/Model Manager of the model, just like in third part of our series.

Of course, there are more filter types available, and the easiest way is to explore them is by reading the documentation.

As always, you can find the code of this part (together with the solution of the homework assignment) in the repository.

Stay tuned for more Django REST Framework insights. In the next part of the series, we will deal with functional endpoints and nesting.

Dominik Kozaczko - Backend Engineer

Dominik Kozaczko

Backend Engineer

Dominik has been fascinated with computers throughout his entire life. His two passions are coding and teaching - he is a programmer AND a teacher. He specializes mostly in backend development and training junior devs. He chose to work with Sunscrapers because the company profoundly supports the open-source community. In his free time, Dominik is an avid gamer.

Tags

django
django rest framework
python

Share

Let's talk

Discover how software, data, and AI can accelerate your growth. Let's discuss your goals and find the best solutions to help you achieve them.

Hi there, we use cookies to provide you with an amazing experience on our site. If you continue without changing the settings, we’ll assume that you’re happy to receive all cookies on Sunscrapers website. You can change your cookie settings at any time.