Джанго-фильтры: несколько идентификаторов в одной строке запроса

Используя django-filters, я вижу различные решения для отправки нескольких аргументов одного и того же введите одну строку запроса, например, для нескольких идентификаторов. Все они предлагают использовать отдельное поле, содержащее список значений, разделенных запятыми, например:

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

Есть ли общее решение для использования одного параметра, но отправленного один или несколько раз? Например.:

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

Я попытался использовать MultipleChoiceFilter, но он ожидает, что фактические варианты быть определены, тогда как я хочу передать произвольные идентификаторы (некоторые из которых могут даже не существовать в БД).


person mart1n    schedule 11.06.2018    source источник
comment
Что вы имеете в виду под некоторые даже не существуют в базе данных? Что должно произойти тогда?   -  person Willem Van Onsem    schedule 11.06.2018
comment
@WillemVanOnsem Следует использовать стандартный поиск IN. Если вы запросите id=1&id=2, а ресурс с идентификатором 2 не существует, в списке результатов будет возвращен только первый. Если ни один из них не существует, будет возвращен пустой список.   -  person mart1n    schedule 11.06.2018
comment
Отвечает ли это на ваш вопрос? Как вы используете django -filter package со списком параметров?   -  person Josh Correia    schedule 30.06.2021


Ответы (7)


Вот повторно используемое решение с использованием пользовательского Filter и пользовательского Field.

Пользовательский Field повторно использует Django MultipleChoiceField, но заменяет функции проверки. Вместо этого он проверяет, используя другой класс Field, который мы передаем конструктору.

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]

Пользовательский Filter использует MultipleValueField и пересылает аргумент field_class. Он также устанавливает значение по умолчанию для lookup_expr на 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)

Чтобы использовать этот фильтр, просто создайте MultipleValueFilter с соответствующим field_class. Например, чтобы отфильтровать City по id, мы можем использовать IntegerField вот так:

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']
person Benoit Blanchon    schedule 27.10.2020

Решено с помощью пользовательского фильтра, вдохновленного ответом Джерина:

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

Если значения должны были быть нецифровыми, например. color=blue&color=red то isdigit() валидация, конечно, не нужна.

person mart1n    schedule 12.06.2018
comment
Мне кажется странным, что это еще не поведение по умолчанию в django-filters... Это распространенный вариант использования. - person Leogout; 14.12.2020

Я рекомендую вам использовать настраиваемый фильтр, как показано ниже

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'))
person JPG    schedule 12.06.2018
comment
Правильно, это решение, когда вы хотите передать несколько значений как ids=1,2,3, но я специально ищу, как обрабатывать несколько аргументов с одним и тем же именем, поэтому id=1&id=2&id=3. - person mart1n; 12.06.2018

У меня есть некоторые проблемы с этим решением

Поэтому я немного изменил его:

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

Если вы хотите использовать собственное имя параметра запроса - измените query_param arg.

person Aleksandr Shustrov    schedule 24.06.2019

Как вариант

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

person Sergey Telminov    schedule 21.08.2019

У Бенуа Бланшона было лучшее

Но я попытался улучшить его, чтобы добавить больше типов lookup_expr. Полные фрагменты кода см. в его ответе.

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

С этим изменением теперь вы можете использовать, например, iexact. Вы также можете использовать gte и т. д., но это, вероятно, не будет иметь особого смысла.

person sanchaz    schedule 28.06.2021