9

Using django-filters, I see various solutions for how to submit multiple arguments of the same type in a single query string, for example for multiple IDs. They all suggest using a separate field that contains a comma-separated list of values, e.g.:

http://example.com/api/cities?ids=1,2,3

Is there a general solution for using a single parameter but submitted one or more times? E.g.:

http://example.com/api/cities?id=1&id=2&id=3

I tried using MultipleChoiceFilter, but it expects actual choices to be defined whereas I want to pass arbitrary IDs (some of which may not even exist in the DB).

3
  • What do you mean with some not even exist in the db? What should happen then? Commented Jun 11, 2018 at 13:56
  • @WillemVanOnsem A standard IN lookup should be used. If you request id=1&id=2 and resource with ID 2 does not exist, only the first one would be returned in the list of results. If neither exist, an empty list would be returned. Commented Jun 11, 2018 at 14:04
  • 1
    Does this answer your question? How do you use the django-filter package with a list of parameters? Commented Jun 30, 2021 at 18:00

8 Answers 8

9

Here is a reusable solution using a custom Filter and a custom Field.

The custom Field reuses Django's MultipleChoiceField but replaces the validation functions. Instead, it validates using another Field class that we pass to the constructor.

from django.forms.fields import MultipleChoiceField

class MultipleValueField(MultipleChoiceField):
    def __init__(self, *args, field_class, **kwargs):
        self.inner_field = field_class()
        super().__init__(*args, **kwargs)

    def valid_value(self, value):
        return self.inner_field.validate(value)

    def clean(self, values):
        return values and [self.inner_field.clean(value) for value in values]

The custom Filter uses MultipleValueField and forwards the field_class argument. It also sets the default value of lookup_expr to in.

from django_filters.filters import Filter

class MultipleValueFilter(Filter):
    field_class = MultipleValueField

    def __init__(self, *args, field_class, **kwargs):
        kwargs.setdefault('lookup_expr', 'in')
        super().__init__(*args, field_class=field_class, **kwargs)

To use this filter, simply create a MultipleValueFilter with the appropriate field_class. For example, to filter City by id, we can use a IntegerField, like so:

from django.forms.fields import IntegerField

class CityFilterSet(FilterSet):
    id = MultipleValueFilter(field_class=IntegerField)
    name = filters.CharFilter(lookup_expr='icontains')

    class Meta:
        model = City
        fields = ['name']
Sign up to request clarification or add additional context in comments.

1 Comment

thanks for this. I've posted an improvement to allow for lookup_exprs such as iexact. stackoverflow.com/a/68162262/1321009
7

As option

class CityFilterSet(django_filters.FilterSet):
    id = django_filters.NumberFilter(method='filter_id')

    def filter_id(self, qs, name, value):
        return qs.filter(id__in=self.request.GET.getlist('id'))

Comments

4

Solved using a custom filter, inspired by Jerin's answer:

class ListFilter(Filter):
    def filter(self, queryset, value):
        try:
            request = self.parent.request
        except AttributeError:
            return None

        values = request.GET.getlist(self.name)
        values = {int(item) for item in values if item.isdigit()}

        return super(ListFilter, self).filter(queryset, Lookup(values, 'in'))

If the values were to be non-digit, e.g. color=blue&color=red then the isdigit() validation is of course not necessary.

1 Comment

Seems weird to me that this is not default behavior in django-filters yet... It is a common use case.
2

I would recommend you to use a custom filter, as below

from django_filters.filters import Filter
from rest_framework.serializers import ValidationError
from django_filters.fields import Lookup


class ListFilter(Filter):
    def filter(self, queryset, value):
        list_values = value.split(',')
        if not all(item.isdigit() for item in list_values):
            raise ValidationError('All values in %s the are not integer' % str(list_values))
        return super(ListFilter, self).filter(queryset, Lookup(list_values, 'in'))

1 Comment

Right, that's the solution for when you want to pass multiple values as ids=1,2,3, but I'm specifically looking for how to handle multiple arguments of the same name, so id=1&id=2&id=3.
2

I have some problems with this solution

So I changed it a bit:

class ListFilter(Filter):
    def __init__(self, query_param, *args, **kwargs):
        super(ListFilter, self).__init__(*args, **kwargs)
        # url = /api/cities/?id=1&id=2&id=3 or /api/cities/?id=1,2,3
        # or /api/cities/?id=1&id=2&id=3?id=4,5,6
        self.query_param = query_param 
        self.lookup_expr = 'in'

    def filter(self, queryset, value):
        try:
            request = self.parent.request
        except AttributeError:
            return None

        values = set()
        query_list = request.GET.getlist(self.query_param)
        for v in query_list:
            values = values.union(set(v.split(',')))
        values = set(map(int, values))

        return super(ListFilter, self).filter(queryset, values)
class CityFilter(filterset.FilterSet):
    id = ListFilter(field_name='id', query_param='id')
    name = filters.CharFilter(field_name='name', lookup_expr='icontains')

    class Meta:
        model = City
        fields = ['name']

If you want to use custom query param name - change query_param arg.

Comments

2

On django-filter version 22.1 we can use django_filters.AllValuesMultipleFilter().

import django_filters


class CityFilterSet(django_filters.FilterSet):
    id = django_filters.AllValuesMultipleFilter(label='id')

On browsable API it can show the current id options it can use as values. enter image description here

1 Comment

Love it when there's a native way to do things.
0

Benoit Blanchon had imo the best

But I've tried to improve on it to allow for more types of lookup_expr's. See his answer for the complete code snippets.

from django.db.models import Q

class MultipleValueFilter(Filter):
    field_class = MultipleValueField

    def __init__(self, *args, field_class, **kwargs):
        kwargs.setdefault('lookup_expr', 'in')
        super().__init__(*args, field_class=field_class, **kwargs)

    def filter(self, qs, value):
        # if it's not a list then let the parent deal with it
        if self.lookup_expr == 'in' or not isinstance(value, list):
            return super().filter(qs, value)

        # empty list
        if not value:
            return qs
        if self.distinct:
            qs = qs.distinct()

        lookup = '%s__%s' % (self.field_name, self.lookup_expr)
        filters = Q()
        for v in value:
            filters |= Q(**{lookup: v})
        qs = self.get_method(qs)(filters)
        return qs

With this change you can now use iexact for example. You can also use gte, etc, but those will probably not make much sense.

Comments

-3

http://example.com/api/cities?ids=1,2,3

get_ids=1,2,3
id_list = list(get_ides]
Mdele_name.objects.filter(id__in= id_list)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.