Динамически перезагружать варианты при использовании MultipleChoiceFilter - PullRequest
7 голосов
/ 03 апреля 2019

Я пытаюсь построить 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 ...

enter image description here

Сначала это работает нормально, однако, если я создаю новые объекты DatedResource с новыми различными значениями date, мне нужно перезапустить мой веб-сервер, чтобы они были выбраны в качестве действительного выбора в этом фильтре. Я считаю, что это потому, что список choices вычисляется один раз при запуске веб-сервера, а не каждый раз, когда загружается моя страница.

Есть ли способ обойти это? Может быть, благодаря некоторому творческому использованию ModelMultipleChoiceFilter?

Ой!

Edit: Я попробовал простое использование 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 теперь представляет собой а) отображение str представления каждого DatedResource вместо поля DatedResource.date и б) они не являются уникальными (например, если у меня есть два DatedResource объекта с одинаковым date, оба их str представления отображаются в списке. Это также не является устойчивым, потому что у меня 200k + DatedResources, и страница зависает при попытке загрузить их все (по сравнению с фильтр values_list, способный вытягивать все отдельные даты за считанные секунды.

Ответы [ 2 ]

7 голосов
/ 11 апреля 2019

Одним из простых решений будет , переопределяющий метод __init__() класса набора фильтров .

from django_filters import filters, filterset


class FooFilter(filterset.FilterSet):
    <b>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</b>

    resource_date = filters.MultipleChoiceFilter(field_name='dated_resource__date', <b>choices=[]</b>, label="Resource Date")

ПРИМЕЧАНИЕ: укажите choices=[] в вашем поле определения класса набора фильтров


Результаты

Я протестировал и проверил это решение со следующими зависимостями
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 становится как показано ниже,
Album-List API Result
и я получил release_date как
DateChoices Before

Затем Я добавил новую запись через администратора Django - (Снимок экрана) , и я обновил конечную точку API DRF, и возможные варианты выбора стали такими, как показано ниже,
New Choice in FilterList

2 голосов
/ 10 апреля 2019

Я изучил вашу проблему и у меня есть следующие предложения

Проблема

Вы правильно поняли проблему. Варианты 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"
)

Всякий раз, когда будет создан экземпляр фильтра, варианты будут обновляться динамически. Вы также можете передать выбор статически (без лямбда-функции) в это поле, если хотите поведение по умолчанию.

...