Search code examples
djangodjango-rest-frameworkdjango-filter

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


Solution

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