djangodjango-filtermanytomanyfield

How to filter ManyToMany fields using django-filter


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.


Solution

  • 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.