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