Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combining filters for multi-valued relationships with AND #351

Open
nimame opened this issue Jan 23, 2021 · 2 comments
Open

Combining filters for multi-valued relationships with AND #351

nimame opened this issue Jan 23, 2021 · 2 comments

Comments

@nimame
Copy link

nimame commented Jan 23, 2021

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.

@nimame
Copy link
Author

nimame commented Jan 24, 2021

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.

@wellbranding
Copy link

Is there are any better way?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants