Как использовать 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?

2 ответа

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

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

class ModelChoiceFilter(Filter):
    field_class = forms.ModelChoiceField

class ModelMultipleChoiceFilter(MultipleChoiceFilter):
    field_class = forms.ModelMultipleChoiceField

То есть это меняет field_class к ModelMultipleChoiceField из встроенных форм Джанго.

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

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

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

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

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

Решение, которое работало для меня, состояло в том, чтобы использовать 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: Да, я признаю, что это не весь набор возможных гонок.Но это то, что собирает перепись населения США!

Другие вопросы по тегам