My Site
Categories
Django Rest Framework: Filters with Arguments

I love Django Rest Framework, but there's no doubt that it has some strange design quirks. One of these quirks, in my opinion, is the fact that it does a lot of object initialization for every single request that is made to any of the endpoints.

Most of the behavior attributes that ViewSet require classes instead of instances. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from rest_framework.viewsets import ModelViewSet

from myapp import models
from myapp.drf import authentication, filters, permissions, serializers


class Users(ModelViewSet):
    authentication_classes = (authentication.PasswordAuthentication,)
    permissions_classes = (permissions.IsOwnerPermission,)
    queryset = models.User.objects.all()
    serializer_class = serializers.UserSerializer
    filter_backends = [
        filters.UserCompanyFilter,
    ]

This means that all of the authentication_classes, permissions_classes, serializer_class, and filter_backends need to be instantiated as a part of every single request.

Performance concerns aside, it also presents a few other interesting issues.

Here at Teem, we had desire to make more generic filters, whereby you could pass an argument to the filter's constructor. For example, instead of the UserCompanyFilter above, we could define a more generic filter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class FieldMatchFilter(BaseFilterBackend):
    def __init__(self, model_field):
        self.model_field = model_field

    def filter_queryset(self, request, queryset, view):
        if self.model_field in request.GET:
            queryset = queryset.filter(
                **{self.model_field: request.GET[self.model_field]})

        return queryset

Doing this creates a generic filter that can be used on almost any model for almost any field. The only issue is... DRF, by default, requires a class, not an instance. Doing something like this is totally invalid:

1
2
3
4
5
6
7
8
class Users(ModelViewSet):
    authentication_classes = (authentication.PasswordAuthentication,)
    permissions_classes = (permissions.IsOwnerPermission,)
    queryset = models.User.objects.all()
    serializer_class = serializers.UserSerializer
    filter_backends = [
        filters.FieldMatchFilter('company_id'),
    ]

In DRF, the filter_queryset code looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def filter_queryset(self, queryset):
    """
    Given a queryset, filter it with whichever filter backend is in use.
    You are unlikely to want to override this method, although you may need
    to call it either from a list view, or from a custom `get_object`
    method if you want to apply the configured filtering backend to the
    default queryset.
    """
    for backend in list(self.filter_backends):
        queryset = backend().filter_queryset(self.request, queryset, self)
    return queryset

It loops through each item in filter_backends, instantiates it, and then calls filter_queryset on the instance.

We tried a few different ways to get around this, including making class factories that used Python's type to create a class on the fly. This was very ugly and never felt quite right.

In our latest project, I found a pretty simple and elegant solution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class FieldMatchFilterFactory(BaseFilterBackend):
    def __init__(self, model_field):
        self.model_field = model_field

    def filter_queryset(self, request, queryset, view):
        if self.model_field in request.GET:
            queryset = queryset.filter(
                **{self.model_field: request.GET[self.model_field]})

        return queryset

    def __call__(self):
        return self

This way you can pass an instance in the filter_backends list. When DRF tries to instantiate theclass (that is actually in instance in this case), it will call the special Python method __call__, and that method just returns self, which is the instance. Now calling filter_queryset will work correctly, and DRF doesn't even know the difference.

An added benefit of this method is that the filter is instantiated at app load time, not for every request.

Filed under: Programming
Comments:

No comments have been added yet

Add a comment:
captcha

Optional, for comment reply notifications
 
Note: If you enter your email address, you will be subscribed to this article and will recieve comment updates via email. This is the only thing your address will be used for. A link will be provided at the end of each email that will allow you to unsubscribe should you need to, or you can go to http://synicworld.com//unsubscribe to unsubscribe from any/all updates.