Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django-filter with DRF - How to do 'and' when applying multiple values with the same lookup?

This is a slightly simplified example of the filterset I'm using, which I'm using with the DjangoFilterBackend for Django Rest Framework. I'd like to be able to send a request to /api/bookmarks/?title__contains=word1&title__contains=word2 and have results returned that contain both words, but currently it ignores the first parameter and only filters for word2.

Any help would be very appreciated!

class BookmarkFilter(django_filters.FilterSet):

    class Meta:
        model = Bookmark
        fields = {
            'title': ['startswith', 'endswith', 'contains', 'exact', 'istartswith', 'iendswith', 'icontains', 'iexact'],
        }

class BookmarkViewSet(viewsets.ModelViewSet):
    serializer_class = BookmarkSerializer
    permission_classes = (IsAuthenticated,)
    filter_backends = (DjangoFilterBackend,)
    filter_class = BookmarkFilter
    ordering_fields = ('title', 'date', 'modified')
    ordering = '-modified'
    page_size = 10
like image 386
ergusto Avatar asked Dec 17 '16 00:12

ergusto


People also ask

How do you use DjangoFilterBackend?

The DjangoFilterBackend class is used to filter the queryset based on a specified set of fields. This backend class automatically creates a FilterSet (django_filters. rest_framework. FilterSet) class for the given fields.

What is the purpose of filter () method in Django?

The filter() method is used to filter you search, and allows you to return only the rows that matches the search term.

How do I filter Queryset in Django REST framework?

The simplest way to filter the queryset of any view that subclasses GenericAPIView is to override the . get_queryset() method. Overriding this method allows you to customize the queryset returned by the view in a number of different ways.


1 Answers

The main problem is that you need a filter that understands how to operate on multiple values. There are basically two options:

  • Use MultipleChoiceFilter (not recommended for this instance)
  • Write a custom filter class

Using MultipleChoiceFilter

class BookmarkFilter(django_filters.FilterSet):
    title__contains = django_filters.MultipleChoiceFilter(
        name='title',
        lookup_expr='contains',
        conjoined=True,  # uses AND instead of OR
        choices=[???],
    )

    class Meta:
        ...

While this retains your desired syntax, the problem is that you have to construct a list of choices. I'm not sure if you can simplify/reduce the possible choices, but off the cuff it seems like you would need to fetch all titles from the database, split the titles into distinct words, then create a set to remove duplicates. This seems like it would be expensive/slow depending on how many records you have.

Custom Filter

Alternatively, you can create a custom filter class - something like the following:

class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
    def filter(self, qs, value):
        # value is either a list or an 'empty' value
        values = value or []

        for value in values:
            qs = super(MultiValueCharFilter, self).filter(qs, value)

        return qs


class BookmarkFilter(django_filters.FilterSet):
    title__contains = MultiValueCharFilter(name='title', lookup_expr='contains')

    class Meta:
        ...

Usage (notice that the values are comma-separated):

GET /api/bookmarks/?title__contains=word1,word2

Result:

qs.filter(title__contains='word1').filter(title__contains='word2')

The syntax is changed a bit, but the CSV-based filter doesn't need to construct an unnecessary set of choices.

Note that it isn't really possible to support the ?title__contains=word1&title__contains=word2 syntax as the widget can't render a suitable html input. You would either need to use SelectMultiple (which again, requires choices), or use javascript on the client to add/remove additional text inputs with the same name attribute.


Without going into too much detail, filters and filtersets are just an extension of Django's forms.

  • A Filter has a form Field, which in turn has a Widget.
  • A FilterSet is composed of Filters.
  • A FilterSet generates an inner form based on its filters' fields.

Responsibilities of each filter component:

  • The widget retrieves the raw value from the data QueryDict.
  • The field validates the raw value.
  • The filter constructs the filter() call to the queryset, using the validated value.

In order to apply multiple values for the same filter, you would need a filter, field, and widget that understand how to operate on multiple values.


The custom filter achieves this by mixing in BaseCSVFilter, which in turn mixes in a "comma-separation => list" functionality into the composed field and widget classes.

I'd recommend looking at the source code for the CSV mixins, but in short:

  • The widget splits the incoming value into a list of values.
  • The field validates the entire list of values by validating individual values on the 'main' field class (such as CharField or IntegerField). The field also derives the mixed in widget.
  • The filter simply derives the mixed in field class.

The CSV filter was intended to be used with in and range lookups, which accept a list of values. In this case, contains expects a single value. The filter() method fixes this by iterating over the values and chaining together individual filter calls.

like image 127
Sherpa Avatar answered Oct 06 '22 18:10

Sherpa