Reputation: 6213
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
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.
Upvotes: 2
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
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
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
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
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
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
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