Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 365
mart1n Avatar asked Jun 11 '18 13:06

mart1n


2 Answers

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']
like image 109
Benoit Blanchon Avatar answered Sep 28 '22 08:09

Benoit Blanchon


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.

like image 45
mart1n Avatar answered Sep 28 '22 08:09

mart1n