1 of 13

Reusable Filtering for Django & DRF

A generalised approach to multi-tenant systems

Jonathan Moss

2 of 13

The Requirement

A common requirement when building Django applications is to be able to filter data on a per user basis.

For example:

  • You are building a multi-tenant system
  • You have different levels of user (Admin, Manager, Customer)

3 of 13

The Problem

  • Filtering data in views is easy, you just need to override the get_queryset() method
  • However, this means:
    • Having to do it in every view
    • Thus repeating yourself
    • Easy to forget/overlook
    • Doesn’t automatically apply outside of views (e.g. other business logic)
    • Thus repeating yourself

4 of 13

The Plan

What I’m going to show you is one approach to solving this requirement in a generic, reusable way

  • The approach is universally applicable within Django applications
  • It has multiple layers which may seem overkill
  • A simplified approach is also shown at the end if you only care about filtering in views

5 of 13

Filtering QuerySet Mixin

from django.core.exceptions import ImproperlyConfigured���class UserRelatedQuerySetMixin(object):� user_filter_key = None�� def for_user(self, user):if not self.user_filter_key:raise ImproperlyConfigured("A user_filter_key is required"

)� kwargs = {self.user_filter_key: user}try:return self.filter(**kwargs)except AttributeError:return self.none()

  • A reusable for_user method that can be added to any QuerySet

6 of 13

Creating a Custom QuerySet

from django.db.models.query import QuerySet��from .mixins import UserRelatedQuerySetMixin��class CustomerQuerySet(UserRelatedQuerySetMixin, QuerySet):� user_filter_key = 'owner'

  • You must specify the user_filter_key
  • This is the model path to the user field we want to filter by
  • This could end up going through several relationships if necessary
    • e.g. “account__groups__users”

7 of 13

Using the Custom QuerySet

from django.db import models�from .querysets import CustomerQuerySet��class Customer(models.Model):�� name = models.CharField(max_length=128, blank=True)...�� objects = CustomerQuerySet.as_manager()

  • We replace the automatically provided objects manager in the model with our custom one
  • We use the as_manager() method to allow us to use a QuerySet as a manager.
  • This ensures our custom for_user method is available on both the manager and the subsequent QuerySet it produces

8 of 13

Reusable View Mixin

class FilterByUserMixin(object):�� def get_queryset(self):� qs = super().get_queryset()return qs.for_user(user=self.request.user)

  • A simple mixin that overrides the normal get_queryset() method and calls our custom QuerySet method for_user()
  • Works equally well in a Generic Class based view or a DRF generic view
  • Since filters can be chained, your views can also add additional filter by further extending this method*

* Just make sure it calls super first!

9 of 13

Using our Mixin in a DRF View

from rest_framework import viewsets��from .mixins import FilterByUserMixin�from .models import Customer�from .serializers import CustomerSerializer��class CustomerViewSet(FilterByUserMixin, viewsets.ModelViewSet):� model = Customer� serializer_class = CustomerSerializer

  • Our Customer ViewSet will now always filter by User no matter what sort of action it is performing
  • Works exactly the same in a regular Django class based view
  • Even works when the user is not logged in (they will get an empty queryset

10 of 13

A Simple Practical Example

A slide deck is not the ideal place to explain these concepts.

As such I have put together a simple example project to illustrate this approach.

You can find the project at:

https://github.com/commoncode/filtering-example

11 of 13

Simplifying

class FilterByUserMixin(object):� user_filter_key = 'user'�� def get_queryset(self):� qs = super().get_queryset()if not self.user_filter_key:raise ImproperlyConfigured(

"A user_filter_key is required"

)� kwargs = {self.user_filter_key: self.request.user}try:return qs.filter(**kwargs)except AttributeError:return qs.none()

If your views are all that matters to you then you can always use a simpler approach:

12 of 13

The Summary

  • We have seen a reusable approach to filtering in Django
  • It makes use of mixins for QuerySets and Views*
  • Some examples of how to use the 2 mixins to create custom querysets and extends the functionality of views
  • A simplified, combined approach that just uses a view mixin

I hope you can see from this quick overview of the approach that it provides a great deal of flexibility inside and out of Django views.

I also hope you see that by using small, discrete mixins, that isolated unit testing of the code is also made fairly straightforward.

* If you are not a fan of mixins, sub-classing can be used to elicit the same effect

13 of 13

Questions?

Keep in Contact