Динамически перезагружать варианты при использовании MultipleChoiceFilter
Я пытаюсь построить MultipleChoiceFilter
где варианты выбора представляют собой набор возможных дат, которые существуют в связанной модели (DatedResource
).
Вот с чем я работаю до сих пор...
resource_date = filters.MultipleChoiceFilter(
field_name='dated_resource__date',
choices=[
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
Когда это отображается в виде HTML...
Сначала это работает нормально, однако, если я создаю новый DatedResource
объекты с новыми отличительными date
значения, которые необходимо перезапустить, чтобы их можно было выбрать в качестве действительного выбора в этом фильтре. Я считаю, что это потому, что choices
Список оценивается один раз при запуске веб-сервера, а не каждый раз, когда загружается моя страница.
Есть ли способ обойти это? Может быть, через некоторое творческое использование ModelMultipleChoiceFilter
?
Спасибо!
Изменить: я попробовал несколько простых ModelMultipleChoice
использование, но ударяя некоторые проблемы.
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
queryset=resource_models.DatedResource.objects.all().values_list('date', flat=True).order_by('date').distinct(),
label="Resource Date"
)
Форма HTML отображается просто отлично, однако варианты не являются допустимыми значениями для фильтра. я получил "2019-04-03" is not a valid value.
ошибки проверки, я предполагаю, потому что этот фильтр ожидает datetime.date
объекты. Я думал об использовании coerce
параметр, однако они не принимаются в ModelMultipleChoice
фильтры.
Согласно комментарию Диркгротена, я пытался использовать то, что было предложено в связанном вопросе. Это в конечном итоге что-то вроде
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
to_field_name='date',
queryset=resource_models.DatedResource.objects.all(),
label="Resource Date"
)
Это также не то, что я хочу, так как форма HTML теперь a) отображает str
представление каждого DatedResource
вместо DatedResource.date
поле и б) они не являются уникальными (например, если у меня есть два DatedResource
объекты с одинаковым date
оба их str
представления появляются в списке. Это также не является устойчивым, потому что у меня есть 200k+ DatedResources
и страница зависает при попытке загрузить их все (по сравнению с values_list
фильтр, который может вытянуть все различные даты в считанные секунды.
2 ответа
Одним из простых решений будет переопределение __init__()
метод класса фильтров.
from django_filters import filters, filterset
class FooFilter(filterset.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
self.filters['user'].extra['choices'] = [(d, d.strftime('%Y-%m-%d')) for d in sorted(
resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())]
except (KeyError, AttributeError):
pass
resource_date = filters.MultipleChoiceFilter(field_name='dated_resource__date', choices=[], label="Resource Date")
ПРИМЕЧАНИЕ: предоставить choices=[]
в вашем поле определения класса filterset
Результаты
Я проверил и проверил это решение со следующими зависимостями
1. Python 3.6
2. Джанго 2.1
3. DRF 3.8.2
4. Джанго-фильтр 2.0.0
Я использовал следующий код для воспроизведения поведения
# models.py
from django.db import models
class Musician(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return f'{self.name}'
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_date = models.DateField()
def __str__(self):
return f'{self.name} : {self.artist}'
# serializers.py
from rest_framework import serializers
class AlbumSerializer(serializers.ModelSerializer):
artist = serializers.StringRelatedField()
class Meta:
fields = '__all__'
model = Album
# filters.py
from django_filters import rest_framework as filters
class AlbumFilter(filters.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters['release_date'].extra['choices'] = self.get_album_filter_choices()
def get_album_filter_choices(self):
release_date_list = Album.objects.values_list('release_date', flat=True).distinct()
return [(date, date) for date in release_date_list]
release_date = filters.MultipleChoiceFilter(choices=[])
class Meta:
model = Album
fields = ('release_date',)
# views.py
from rest_framework.viewsets import ModelViewSet
from django_filters import rest_framework as filters
class AlbumViewset(ModelViewSet):
serializer_class = AlbumSerializer
queryset = Album.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_class = AlbumFilter
Здесь я использовал django-filter
с DRF
,
Теперь я заполнил некоторые данные через консоль администратора Django. После этого альбом api становится как показано ниже,
и я получил release_date
как
Затем я добавил новую запись через администратора Django - (Снимок экрана) и обновил конечную точку API DRF, и возможные варианты стали такими, как показано ниже:
Я изучил вашу проблему и у меня есть следующие предложения
Эта проблема
Вы правильно поняли проблему. Выбор для вашего MultipleChoiceFilter
рассчитываются статически всякий раз, когда вы запускаете сервер. Вот почему они не обновляются динамически, когда вы вставляете новый экземпляр в DatedResource
,
Чтобы он работал правильно, вы должны динамически предоставлять MultipleChoiceFilter
, Я искал в документации, но ничего не нашел по этому поводу. Так вот мое решение.
Решение
Вы должны продлить MultipleChoiceFilter
и создайте свой собственный класс фильтра. Я создал это и вот оно.
from typing import Callable
from django_filters.conf import settings
import django_filters
class LazyMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
def get_field_choices(self):
choices = self.extra.get('choices', [])
if isinstance(choices, Callable):
choices = choices()
return choices
@property
def field(self):
if not hasattr(self, '_field'):
field_kwargs = self.extra.copy()
if settings.DISABLE_HELP_TEXT:
field_kwargs.pop('help_text', None)
field_kwargs.update(choices=self.get_field_choices())
self._field = self.field_class(label=self.label, **field_kwargs)
return self._field
Теперь вы можете использовать этот класс в качестве замены и передать выбор в виде лямбда-функции.
resource_date = LazyMultipleChoiceFilter(
field_name='dated_resource__date',
choices=lambda: [
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
Всякий раз, когда будет создан экземпляр фильтра, варианты будут обновляться динамически. Вы также можете передать выбор статически (без лямбда-функции) в это поле, если хотите поведение по умолчанию.