Как использовать ModelMultipleChoiceFilter?

Я пытался заставить ModelMultipleChoiceFilter работать часами и прочитал документацию по фильтрам DRF и Django.

Я хочу иметь возможность фильтровать набор веб-сайтов на основе тегов, которые были назначены им через ManyToManyField. Например, я хочу получить список веб-сайтов с тегами «Кулинария» или «Пчеловодство».

Вот соответствующий фрагмент моего текущего models.py:

class SiteTag(models.Model):
    """Site Categories"""
    name = models.CharField(max_length=63)

    def __str__(self):
        return self.name

class Website(models.Model):
    """A website"""
    domain = models.CharField(max_length=255, unique=True)
    description = models.CharField(max_length=2047)
    rating = models.IntegerField(default=1, choices=RATING_CHOICES)
    tags = models.ManyToManyField(SiteTag)
    added = models.DateTimeField(default=timezone.now())
    updated = models.DateTimeField(default=timezone.now())

    def __str__(self):
        return self.domain

И мой текущий фрагмент views.py:

class WebsiteFilter(filters.FilterSet):
    # With a simple CharFilter I can chain together a list of tags using &tag=foo&tag=bar - but only returns site for bar (sites for both foo and bar exist).
    tag = django_filters.CharFilter(name='tags__name')

    # THE PROBLEM:
    tags = django_filters.ModelMultipleChoiceFilter(name='name', queryset=SiteTag.objects.all(), lookup_type="eq")

    rating_min = django_filters.NumberFilter(name="rating", lookup_type="gte")
    rating_max = django_filters.NumberFilter(name="rating", lookup_type="lte")

    class Meta:
        model = Website
        fields = ('id', 'domain', 'rating', 'rating_min', 'rating_max', 'tag', 'tags')

class WebsiteViewSet(viewsets.ModelViewSet):
    """API endpoint for sites"""
    queryset = Website.objects.all()
    serializer_class = WebsiteSerializer
    filter_class = WebsiteFilter
    filter_backends = (filters.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,)
    search_fields = ('domain',)
ordering_fields = ('id', 'domain', 'rating',)

Я только что тестировал строку запроса [/path/to/sites]?tags=News и на 100% уверен, что соответствующие записи существуют, поскольку они работают (как описано) с запросом ?tag (отсутствует s).

Пример других вещей, которые я пробовал, выглядит примерно так:

tags = django_filters.ModelMultipleChoiceFilter(name='tags__name', queryset=Website.objects.all(), lookup_type="in")

Как я могу вернуть любой веб-сайт с SiteTag, удовлетворяющим name == A OR name == B OR name == C?


person Daniel Devine    schedule 06.10.2014    source источник
comment
На данный момент я решил свою проблему, следуя указаниям Можно ли выполнить in lookup_type через анализатор URL-адресов django-filter? и создать собственный фильтр. Мне все еще интересно увидеть решение моей проблемы, так как я уверен, что оно поможет кому-то еще - и в коде, который я не использую, не будет ошибок :)   -  person Daniel Devine    schedule 06.10.2014


Ответы (2)


Я наткнулся на этот вопрос, пытаясь решить почти идентичную вам проблему, и хотя я мог просто написать собственный фильтр, ваш вопрос меня заинтриговал, и мне пришлось копнуть глубже!

Оказывается, ModelMultipleChoiceFilter делает только одно изменение по сравнению с обычным Filter, как видно из исходного кода django_filters ниже:

class ModelChoiceFilter(Filter):
    field_class = forms.ModelChoiceField

class ModelMultipleChoiceFilter(MultipleChoiceFilter):
    field_class = forms.ModelMultipleChoiceField

То есть он изменяет field_class на ModelMultipleChoiceField из встроенных форм Django.

Взглянув на исходный код для ModelMultipleChoiceField, один из обязательных аргументов для __init__() — это queryset, так что вы были на правильном пути.

Другая часть головоломки исходит от метода ModelMultipleChoiceField.clean() со строкой: key = self.to_field_name or 'pk'. Это означает, что по умолчанию он примет любое значение, которое вы ему передадите (например, "cooking"), и попытается найти Tag.objects.filter(pk="cooking"), когда, очевидно, мы хотим, чтобы он посмотрел на имя, и, как мы можем видеть в этой строке, что поле, с которым оно сравнивается, контролируется self.to_field_name.

К счастью, метод Filter.field() django_filters включает следующее при создании фактического поля.

self._field = self.field_class(required=self.required,
    label=self.label, widget=self.widget, **self.extra)

Особо следует отметить **self.extra, который происходит от Filter.__init__(): self.extra = kwargs, поэтому все, что нам нужно сделать, это передать дополнительный to_field_name kwarg в ModelMultipleChoiceFilter, и он будет передан нижележащему ModelMultipleChoiceField.

Итак (пропустите здесь фактическое решение!), фактический код, который вам нужен,

tags = django_filters.ModelMultipleChoiceFilter(
    name='sitetags__name',
    to_field_name='name',
    lookup_type='in',
    queryset=SiteTag.objects.all()
)

Итак, вы были очень близки с кодом, который вы разместили выше! Я не знаю, будет ли это решение актуально для вас, но, надеюсь, оно может помочь кому-то еще в будущем!

person ProfSmiles    schedule 03.09.2015
comment
В итоге проект был отложен, потому что он был слишком спорным, но я уже второй раз пишу подобную функциональность. Я, вероятно, снова буду использовать фильтры Django, поэтому я буду очень счастлив, когда в третий раз получится! Я думаю, что стоит включить ваше решение в официальную документацию. Дайте мне знать, если вас это не устроит (и получение уличного авторитета самостоятельно) - я постараюсь найти время. - person Daniel Devine; 03.09.2015
comment
Я использовал фильтры Django для своего текущего проекта и снова столкнулся с этой проблемой. Спасибо за ответ! - person Daniel Devine; 26.10.2015
comment
Кажется, это помогает, но когда несколько значений передаются через два или более параметров GET, к сожалению, это не работает. - person mlissner; 19.12.2015
comment
Отличный ответ. Я думаю, что может быть ошибка. Вместо: name='sitetags__name' должно быть: name='sitetags'. - person RKI; 09.02.2016
comment
@RKI В случае с ОП, я думаю, вы правы, что сработает только name='sitetags', но я думаю, только потому, что метод __str__ SiteTag - это return self.name. Дополнительный __name просто позволяет вам указать, какое именно поле вы хотите, чтобы оно соответствовало, и позволяет ему по-прежнему работать, когда метод __str__ возвращает что-то более сложное. - person ProfSmiles; 10.02.2016
comment
это решение нужно пометить как правильный ответ. @ProfSmiles большое вам спасибо! - person Anton Manevskiy; 27.12.2016
comment
@ProfSmiles, как вы думаете, вы можете обновить ответ, чтобы он соответствовал текущим версиям? ModelMultipleChoiceFilter не поддерживает имя и lookup_type - person Sai Chander; 29.04.2021

Решение, которое сработало для меня, состояло в том, чтобы использовать файл MultipleChoiceFilter. В моем случае у меня есть судьи, у которых есть гонки, и я хочу, чтобы мой API позволял людям запрашивать, скажем, черных или белых судей.

В итоге получается фильтр:

race = filters.MultipleChoiceFilter(
    choices=Race.RACES,
    action=lambda queryset, value:
        queryset.filter(race__race__in=value)
)

Race - это поле "многие ко многим" из Judge:

class Race(models.Model):
    RACES = (
        ('w', 'White'),
        ('b', 'Black or African American'),
        ('i', 'American Indian or Alaska Native'),
        ('a', 'Asian'),
        ('p', 'Native Hawaiian or Other Pacific Islander'),
        ('h', 'Hispanic/Latino'),
    )
    race = models.CharField(
        choices=RACES,
        max_length=5,
    )

Обычно я не большой поклонник lambda функций, но здесь это имело смысл, потому что это такая маленькая функция. По сути, это устанавливает MultipleChoiceFilter, который передает значения из параметров GET в поле race модели Race. Они передаются в виде списка, поэтому параметр in работает.

Итак, мои пользователи могут:

/api/judges/?race=w&race=b

И они вернут судей, которые идентифицировали себя как черных или белых.

PS: Да, я понимаю, что это не весь набор возможных рас. Но это именно то, что собирает перепись населения США!

person mlissner    schedule 19.12.2015
comment
Разве это не приведет к расам, которые одновременно и черные, и белые? Черный ИЛИ белый должен выглядеть как ?race=w,b нет? - person Marios Yiannakou; 09.07.2021
comment
Я уже не уверен, но он живет уже много лет. - person mlissner; 10.07.2021