How to use ModelMultipleChoiceFilter?

I am trying to get ModelMultipleChoiceFilter to work for several hours and read the DRF and Django Filters documentation.

I want to be able to filter a set of websites based on the tags that were assigned to them through ManyToManyField. For example, I want to get a list of websites with the tags "Cooking" or "Beekeeping."

Here is the corresponding 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 just tested with querystring [/path/to/sites]?tags=News , and I am 100% sure that the relevant entries exist when they work (as described) with the query ?tag (missing s ).

An example of other things I've tried is something like:

 tags = django_filters.ModelMultipleChoiceFilter(name='tags__name', queryset=Website.objects.all(), lookup_type="in") 

How can I return any website with SiteTag that satisfies name == A OR name == B OR name == C ?

+7
python django django-rest-framework django-filter
source share
2 answers

I came across this question, trying to solve an almost identical problem for myself, and although I could just write my own filter, your question made me intrigued, and I had to dig deeper!

It turns out that ModelMultipleChoiceFilter only makes one change over the normal Filter , as shown 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 field_class to ModelMultipleChoiceField from the built-in forms of Django.

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

Another piece of the puzzle comes from the ModelMultipleChoiceField.clean() method, with a line: key = self.to_field_name or 'pk' . This means that by default it will take whatever value you pass it (for example, "cooking" ) and try to find Tag.objects.filter(pk="cooking") when we obviously want it to look at name, and, as we see, in this line, in which field it is compared, self.to_field_name controlled.

Fortunately, the django_filters Filter.field() method includes when creating an instance of the actual field.

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

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

So (skip here for a real solution!) The code you need

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

So you were very close with the code you provided above! I don’t know if this solution will be more relevant for you, but I hope this can help someone else in the future!

+11
source share

The solution that worked for me was to use MultipleChoiceFilter . In my case, I have judges who have races, and I want my API to allow people to request, say, black or white judges.

Filter ends:

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

Race is many, many fields from 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 am not a big fan of lambda functions, as a rule, but it made sense here because it is such a small function. This basically sets the MultipleChoiceFilter , which passes the values ​​from the GET parameters to the Race field of the Race model. They are passed as a list, so why the in parameter works.

So, my users can:

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

And they will bring back judges who are identified as black or white.

PS: Yes, I understand that this is not the whole set of possible races. But this is what the United States Census collects.

+1
source share

All Articles