Willy's blog

How to filter Polymorphic Models with Django Filters

As the title says, I needed a way to filter my polymorphic models using my already defined rest_framework.FilterSet, and as I didn't find much resources about it I'm sharing my experience here.

First, let's talk about django-polymorphic and django-filters, what are these libraries for.

Django-polymorphic [1]

Simplifies using inherited models in Django projects. When a query is made at the base
model, the inherited model classes are returned.

One of the most important things to understand of polymorphic is, that when you ask for the queryset of the parent, every element is represented with its children classes. Let's see this.

I'll use the models examples provided by the docs.

from polymorphic.models import PolymorphicModel

class Project(PolymorphicModel):
    topic = models.CharField(max_length=30)
    start_date = models.DateField(null=True, blank=True)
    finish_date = models.DateField(null=True, blank=True)

class ArtProject(Project):
    artist = models.CharField(max_length=30)

class ResearchProject(Project):
    supervisor = models.CharField(max_length=30)
>>> Project.objects.create(topic="Department Party")
>>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner")
>>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
>>> Project.objects.all()
[ <Project:         id 1, topic "Department Party">,
  <ArtProject:      id 2, topic "Painting with Tim", artist "T. Turner">,
  <ResearchProject: id 3, topic "Swallow Aerodynamics", supervisor "Dr. Winter"> ]

As you can see, this can be really helpful when using Django Rest Framework (DRF) and model inheritance. It allows to use just one model, in this case Project, in a ModelViewSet and you will receive all the instances for Project, ArtProject and ResearchProject. Take into account that your serializer will have to handle the representation of each of the models.

Django-filter [2]

Is a generic, reusable application to alleviate writing some of the more mundane bits of view code. Specifically, it allows users to filter down a queryset based on a model’s fields.

Fundamentally, when using django filters you'll want to create a class specifying the model's fields by which a queryset can be filtered.

Let's modified a bit the example of the docs:

import django_filters

class ProjectFilter(django_filters.FilterSet):
    topic = django_filters.CharFilter(lookup_expr='iexact')

    class Meta:
        model = Project
        fields = ['start_date', 'finish_date']

And the view should look something like:

def project_list(request):
    f = ProjectFilter(request.GET, queryset=Project.objects.all())
    return render(request, 'my_app/template.html', {'filter': f})

Obviously, this will help reduce the amount of code written, and will be way easier to mantain. The good thing about this library, and what matters to me the most, is that it has a great integration with DRF by providing a DRF-specific FilterSet and a filter backend.

Filtering by a polymorphic model

As we stated at the beginning what I wanted to do is filter by a polymorphic model, because we have different types of projects. This can be easily achieved by reading the docs, no seriously, by essentially using the rest_framework.FilterSet and using a customized filter with Filter.method in our FilterSet.

import django_filters

def get_subclasses_as_choice(klass):
    choices = {subclass.__name__.lower(): subclass
               for subclass in klass.__subclasses__()}
    return choices

class ProjectFilter(django_filters.rest_framework.FilterSet):
    project_type = django_filters.MultipleChoiceFilter(
        method='project_type_filter', choices=get_subclasses_as_choice(Project))

    class Meta:
        model = Project
        fields = ['topic', 'start_date', 'end_date']

    def project_type_filter(self, queryset, name, value):
        project_choices = get_subclasses_as_choice(Project)
        selected_projects = [value for key, value in project_choices.items()
                             if key in value]
        return queryset.instance_of(*selected_projects)

Now, if our querystring includes a key project_type, it will check if the values match any of the choices and it will return the queryset filtered by the specified choices. And that's it, we have successfully filtered polymorphic models. Now we just need to add ProjectFilter to the filter_class in the viewsets.ModelViewSet.