4

I have a project that is built using Django REST Framework. I have models Item and Tag, between which there's a many-to-many relationship. When requesting a list of Item instances, I am using ModelMultipleChoiceFilter to filter Item list by tags.

Here's my filters.py:

import django_filters
from .models import Item, Tag

class ItemTagFilter(django_filters.FilterSet):
    tags = django_filters.ModelMultipleChoiceFilter(name='tags__text',
                                                    to_field_name='text',
                                                    queryset=Tag.objects.all(),
                                                    conjoined=False,)

    class Meta:
        model = Item
        fields = ['tags']

As you may notice, since the value conjoined is False by default, I expect any Item instance having any of the Tag texts I request to be included in the resulting list. And it seems to be working for the existing Tag instances recorded in the database.

The problem is, when I enter a non-existent Tag text, an empty list is returned, even if I have sent several Tag texts alongside it which do exist in the database. (i.e. I expect the filter to return the union of Item elements which have any of the tags I send in my request)

I looked into the Django REST Framework documentation and several relevant SO posts like this one, but I could find neither the root cause of the issue, nor a solution to it. I'd appreciate any help.

You may find my models.py and views.py below, if you need further information.

models.py:

from django.db import models

class Tag(models.Model):
    text = models.CharField(max_length = 100, unique = True)
    ...

class Item(models.Model):
    info = models.CharField(max_length = 200)
    tags = models.ManyToManyField(Tag, related_name='items')

views.py:

from rest_framework import generics
from .models import Item
from .filters import ItemTagFilter
import django_filters.rest_framework as filters
...

class ListCreateItemView(generics.ListCreateAPIView):
    queryset = Item.objects.all()
    filter_backends = (filters.DjangoFilterBackend,)
    filter_class = ItemTagFilter
    serializer_class = ItemSerializer
ilim
  • 4,477
  • 7
  • 27
  • 46

1 Answers1

1

I looked a bit at the django_filters code and it seems like there's no configuration/option to make the ModelMultipleChoiceFilter field do what you want; The ModelMultipleChoiceField used by the filter validates the existence of all the related tags using Django's ModelMultipleChoiceField which:

Raise a ValidationError if a given value is invalid (not a valid PK, not in the queryset, etc.)

This means the list from which you choose the tags used for filtering will be empty if any of the supplied tags is invalid.

However, you can bypass this by overriding ModelMultipleChoiceField _check_values method and compute the queryset yourself. I think something like this should work:

class CustomField(django_filters.fields.ModelMultipleChoiceField):
    def _check_values(self, value):
        """
        Override the base class' _check_values method so our queryset is not
        empty if one of the items in value is invalid.
        """
        null = self.null_label is not None and value and self.null_value in value
        if null:
            value = [v for v in value if v != self.null_value]
        field_name = self.to_field_name or 'pk'
        result = list(self.queryset.filter(**{'{}__in'.format(field_name): value}))
        result += [self.null_value] if null else []
        return result


class CustomModelMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
    field_class = CustomField


class ItemTagFilter(django_filters.FilterSet):
    tags = CustomModelMultipleChoiceFilter(name='tags__text',
                                           to_field_name='text',
                                           queryset=Tag.objects.all(),
                                           conjoined=False,)

class Meta:
    model = Item
    fields = ['tags']

Hope this helps!

kirbuchi
  • 2,274
  • 2
  • 23
  • 35