使用 MultipleChoiceFilter 时动态重新加载选项

Reload choices dynamically when using 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 过滤器不接受这些参数。

根据 dirkgroten 的评论,我尝试使用 linked question 中的建议。这最终变成了

resource_date = filters.ModelMultipleChoiceFilter(
    field_name='dated_resource__date',
    to_field_name='date',
    queryset=resource_models.DatedResource.objects.all(),
    label="Resource Date"
)

这也不是我想要的,因为现在的 HTML 表单现在是 a) 显示每个 DatedResourcestr 表示,而不是 DatedResource.date 字段和 b) 它们不是唯一的(例如,如果我有两个具有相同 dateDatedResource 对象,它们的两个 str 表示都出现在列表中。这也是不可持续的,因为我有 200k+ DatedResources,页面在尝试加载它们时挂起(与 values_list 过滤器相比,它能够在几秒钟内提取所有不同的日期。

我已经调查了你的问题,我有以下建议

问题

你答对了问题。 MultipleChoiceFilter 的选择是在您 运行 server.Thats 时静态计算的,为什么在 DatedResource.

中插入新实例时它们不会动态更新

要使其正常工作,您必须动态地向 MultipleChoiceFilter 提供选择。我在文档中进行了搜索,但没有找到与此相关的任何信息。所以这是我的解决方案。

解决方法

您必须扩展 MultipleChoiceFilter 并创建您自己的过滤器 class。我创建了这个,就在这里。

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

现在您可以使用此 class 作为替换并将选择作为 lambda 函数传递。

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

每当创建过滤器实例时,选择都会动态更新。如果需要默认行为,您还可以将选择静态(没有 lambda 函数)传递给此字段。

一个简单的解决方案是 覆盖过滤器集 class__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")

注意: 在过滤器集 class

的字段定义中提供 choices=[]

结果

我使用以下依赖项测试并验证了此解决方案
1.Python3.6
2.Django 2.1
3.DRF 3.8.2
4.django-过滤器 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-filterDRF.

现在,我通过 Django 管理控制台填充了一些数据。之后,相册api变成如下,

我得到 release_date 作为


然后,I added new entry through Django admin -- (Screenshot) 和我刷新 DRF API 端点,可能的选择如下所示,