mart1n
mart1n

Reputation: 6213

Django-filters: multiple IDs in a single query string

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).

Upvotes: 9

Views: 10611

Answers (8)

Nathan
Nathan

Reputation: 509

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

Upvotes: 2

sanchaz
sanchaz

Reputation: 133

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.

Upvotes: 0

Benoit Blanchon
Benoit Blanchon

Reputation: 14521

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']

Upvotes: 9

Sergey  Telminov
Sergey Telminov

Reputation: 809

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'))

Upvotes: 7

Aleksandr Shustrov
Aleksandr Shustrov

Reputation: 138

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.

Upvotes: 2

Savai Maheshwari
Savai Maheshwari

Reputation: 171

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)

Upvotes: -3

mart1n
mart1n

Reputation: 6213

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.

Upvotes: 4

JPG
JPG

Reputation: 88499

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'))

Upvotes: 2

Related Questions