philipn/django-rest-framework-filters

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 ANDs, 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?