[Fixed]-How to use ModelMultipleChoiceFilter?

29👍

I stumbled across this question while trying to solve a nearly identical problem to yourself, and while I could have just written a custom filter, your question got me intrigued and I had to dig deeper!

It turns out that a ModelMultipleChoiceFilter only makes one change over a normal Filter, as seen in the django_filters source code below:

class ModelChoiceFilter(Filter):
    field_class = forms.ModelChoiceField

class ModelMultipleChoiceFilter(MultipleChoiceFilter):
    field_class = forms.ModelMultipleChoiceField

That is, it changes the field_class to a ModelMultipleChoiceField from Django’s built in forms.

Taking a look at the source code for ModelMultipleChoiceField, one of the required arguments to __init__() is queryset, so you were on the right track there.

The other piece of the puzzle comes from the ModelMultipleChoiceField.clean() method, with a line: key = self.to_field_name or 'pk'. What this means is that by default it will take whatever value you pass to it (eg.,"cooking") and try to look up Tag.objects.filter(pk="cooking"), when obviously we want it to look at the name, and as we can see in that line, what field it compares to is controlled by self.to_field_name.

Luckily, django_filters‘s Filter.field() method includes the following when instantiating the actual field.

self._field = self.field_class(required=self.required,
    label=self.label, widget=self.widget, **self.extra)

Of particular note is the **self.extra, which comes from Filter.__init__(): self.extra = kwargs, so all we need to do is pass an extra to_field_name kwarg to the ModelMultipleChoiceFilter and it will be handed through to the underlying ModelMultipleChoiceField.

So (skip here for the actual solution!), the actual code you want is

tags = django_filters.ModelMultipleChoiceFilter(
    name='sitetags__name',
    to_field_name='name',
    lookup_type='in',
    queryset=SiteTag.objects.all()
)

So you were really close with the code you posted above! I don’t know if this solution will be relevant to you anymore, but hopefully it might help someone else in the future!

0👍

The solution that worked for me was to use a MultipleChoiceFilter. In my case, I have judges that have races, and I want my API to let people query for, say, either black or white judges.

The filter ends up being:

race = filters.MultipleChoiceFilter(
    choices=Race.RACES,
    action=lambda queryset, value:
        queryset.filter(race__race__in=value)
)

Race is a many to many field off of Judge:

class Race(models.Model):
    RACES = (
        ('w', 'White'),
        ('b', 'Black or African American'),
        ('i', 'American Indian or Alaska Native'),
        ('a', 'Asian'),
        ('p', 'Native Hawaiian or Other Pacific Islander'),
        ('h', 'Hispanic/Latino'),
    )
    race = models.CharField(
        choices=RACES,
        max_length=5,
    )

I’m not a huge fan of lambda functions usually, but it made sense here because it’s such a small function. Basically, this sets up a MultipleChoiceFilter that passes the values from the GET parameters to the race field of the Race model. They’re passed in as a list, so that’s why the in parameter works.

So, my users can do:

/api/judges/?race=w&race=b

And they’ll get back judges that have identified as either black or white.

PS: Yes, I recognize that this isn’t the entire set of possible races. But it is what the U.S. census collects!

Leave a comment