Combining filters for multi-valued relationships with `AND`
nimame opened this issue · 2 comments
In my DRF project, I'm trying to implement filtering for multi-valued relationships that combines filters with logical AND
s, like in:
a) Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)
instead of OR
like in:
b) Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
The following results in b):
from rest_framework import viewsets, serializers
from rest_framework_filters.backends import RestFrameworkFilterBackend
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
class BlogViewSet(viewsets.ModelViewSet):
serializer_class = BlogSerializer
filter_backends = (RestFrameworkFilterBackend, )
filterset_fields = {
'entry__headline': ['contains'],
'entry__pub_date': ['year__exact']
}
Is there a way to get a) without specifying the filter explicitly? If no, could someone provide an example for the explicit filter based on the one I provided?
Thank you.
I ended up adding a mixin that overwrites the filter_queryset
method of the FilterSet
class, based on the solution suggested here and here.
from django.db.models import QuerySet
from django_filters.constants import EMPTY_VALUES
from rest_framework_filters import FilterSet
class FilterSetMixin:
def filter_queryset(self, queryset):
"""
Overrides the basic method, so that instead of iterating over the queryset with multiple `.filter()`
calls, one for each filter, it accumulates the lookup expressions and applies them all in a single
`.filter()` call - to filter with an explicit "AND" in many to many relationships.
"""
filter_kwargs = {}
for name, value in self.form.cleaned_data.items():
if value not in EMPTY_VALUES:
lookup = '%s__%s' % (self.filters[name].field_name, self.filters[name].lookup_expr)
filter_kwargs.update({lookup: value})
queryset = queryset.filter(**filter_kwargs)
assert isinstance(queryset, QuerySet), \
"Expected '%s.%s' to return a QuerySet, but got a %s instead." \
% (type(self).__name__, name, type(queryset).__name__)
queryset = self.filter_related_filtersets(queryset)
return queryset
class BlogFilterSet(FilterSetMixin, FilterSet):
class Meta:
model = Blog
fields = {
'entry__headline': ['contains'],
'entry__pub_date': ['year__exact']
}
class BlogViewSet(viewsets.ModelViewSet):
serializer_class = BlogSerializer
filter_backends = (RestFrameworkFilterBackend, )
filterset_class = BlogFilterSet
It feels like this should be the default behavior. But maybe I'm missing something.
Is there are any better way?