I have been trying to get a ModelMultipleChoiceFilter to work for hours and have read both the DRF and Django Filters documentation.
I want to be able to filter a set of Websites based on the tags that have been assigned to them via a ManyToManyField. For example I want to be able to get a list of websites that have been tagged "Cooking" or "Beekeeping".
Here is the relevant snippet of my current models.py:
class SiteTag(models.Model):
"""Site Categories"""
name = models.CharField(max_length=63)
def __str__(self):
return self.name
class Website(models.Model):
"""A website"""
domain = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=2047)
rating = models.IntegerField(default=1, choices=RATING_CHOICES)
tags = models.ManyToManyField(SiteTag)
added = models.DateTimeField(default=timezone.now())
updated = models.DateTimeField(default=timezone.now())
def __str__(self):
return self.domain
And my current views.py snippet:
class WebsiteFilter(filters.FilterSet):
# With a simple CharFilter I can chain together a list of tags using &tag=foo&tag=bar - but only returns site for bar (sites for both foo and bar exist).
tag = django_filters.CharFilter(name='tags__name')
# THE PROBLEM:
tags = django_filters.ModelMultipleChoiceFilter(name='name', queryset=SiteTag.objects.all(), lookup_type="eq")
rating_min = django_filters.NumberFilter(name="rating", lookup_type="gte")
rating_max = django_filters.NumberFilter(name="rating", lookup_type="lte")
class Meta:
model = Website
fields = ('id', 'domain', 'rating', 'rating_min', 'rating_max', 'tag', 'tags')
class WebsiteViewSet(viewsets.ModelViewSet):
"""API endpoint for sites"""
queryset = Website.objects.all()
serializer_class = WebsiteSerializer
filter_class = WebsiteFilter
filter_backends = (filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,)
search_fields = ('domain',)
ordering_fields = ('id', 'domain', 'rating',)
I have just been testing with the querystring [/path/to/sites]?tags=News
and I am 100% sure that the appropriate records exist as they work (as described) with a ?tag
(missing the s
) query.
An example of the other things I have tried is something like:
tags = django_filters.ModelMultipleChoiceFilter(name='tags__name', queryset=Website.objects.all(), lookup_type="in")
How can I return any Website that has a SiteTag that satisfies name == A OR name == B OR name == C
?
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!