How to filter ManyToMany fields using django-filter

135 views Asked by At

How can I filter a ManyToMany field with django-filter

I would like to display an input field on a template where you can filter Student to get these results:

  1. all of the Students that speak English (Student.languages contains 'English')
  2. all of the Students that speak English and German (Student.languages contains 'English' and 'German')
# models.py
class Student(models.Model):
    name = models.CharField(...)
    languages = models.ManyToManyField(Language)

class Language(models.Model):
    language = models.CharField(...)  # English / German / ...
    level = models.CharField(...)  # B1 / B2 / C1 / ...

#filters.py
import django_filters as filters
from .models import Employee

class EmployeeFilter(filters.FilterSet):
    class Meta:
        model = Employee
        fields = ['name', 'languages']

How should I modify the EmployeeFilter to make it ready to filter the Students according to their spoken languages?

I tried declaring a class variable named languages like so:

class EmployeeFilter(filters.FilterSet):
    languages = filters.ModelChoiceFilter(
         queryset = Languages.objects.all()
    )

    class Meta:
        model = Employee
        fields = ['name', 'languages']

but it did not work, the filter had no effect.

1

There are 1 answers

0
willeM_ Van Onsem On BEST ANSWER

You can filter on the language field of the languages:

class EmployeeFilter(filters.FilterSet):
    languages = filters.CharField(
        field_name='languages__language', lookup_expr='iexact'
    )

    class Meta:
        model = Employee
        fields = ['name', 'languages']

This means that you thus can filter with English or english for example.

You could also use ModelMultipleChoiceFilter for example:

class EmployeeFilter(filters.FilterSet):
    languages = filters.ModelMultipleChoiceFilter(
        field_name='languages__language',
        to_field_name='language',
        conjoined=True,
        queryset=Language.objects.all()
    )

    class Meta:
        model = Employee
        fields = ['name', 'languages']

That being said, the modeling looks weird. One would expect that the level is part of the junction table, not the language itself, so:

from django.db import Models


class Student(models.Model):
    # …
    languages = models.ManyToManyField('Language', through='LanguageSkill')


class Language(models.Model):
    language = models.CharField(max_length=128, unique=True)


class LanguageSkill(models.Model):
    language = models.ForeignKey(Language, on_delete=models.CASCADE)
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    level = models.CharField(
        max_length=2, choices=[('B1', 'B1'), ('B2', 'B2'), ('C1', 'C1')]
    )

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=('language', 'student'), name='unique_lang_per_student'
            )
        ]

You thus then add information in the combination of a student with a language what the skill level is.