如何使用ModelMultipleChoiceFilter?

19

我已经尝试了几个小时来让ModelMultipleChoiceFilter工作,并阅读了DRF和Django Filters文档。

我想能够根据通过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,我百分之百确定适当的记录存在,因为它们可以使用?tag(缺少s)查询正常工作(如描述的那样)。

我尝试过的其他内容示例:

tags = django_filters.ModelMultipleChoiceFilter(name='tags__name', queryset=Website.objects.all(), lookup_type="in")

我如何返回任何具有满足 name == A OR name == B OR name == C 的 SiteTag 的网站?


我通过遵循Possible to do an in lookup_type through the django-filter URL parser?的方法并创建自定义过滤器,暂时解决了我的问题。虽然如此,我仍然希望看到我的问题的解决方案,因为我相信它会对其他人有所帮助 - 而我不使用的代码也不会有错误 :) - Daniel Devine
2个回答

29

我在尝试解决一个与你的问题几乎相同的问题时,偶然发现了这个问题。虽然我可以只编写一个自定义过滤器,但你的问题引起了我的兴趣,让我不得不深入探究!

事实证明,ModelMultipleChoiceFilter 与普通的 Filter 只有一个更改,如下所示的 django_filters 源代码:

class ModelChoiceFilter(Filter):
    field_class = forms.ModelChoiceField

class ModelMultipleChoiceFilter(MultipleChoiceFilter):
    field_class = forms.ModelMultipleChoiceField

也就是说,它将 field_class 从 Django 内置表单中更改为 ModelMultipleChoiceField

查看 ModelMultipleChoiceField 的源代码,其中 __init__() 的一个必需参数是 queryset,所以你的想法是正确的。

拼图的另一部分来自于 ModelMultipleChoiceField.clean() 方法,其中有一行:key = self.to_field_name or 'pk'。这意味着默认情况下,它将采用您传递给它的任何值(例如"cooking"),尝试查找 Tag.objects.filter(pk="cooking"),显然我们希望它查找名称,而且正如我们在那行中看到的,它比较的字段由 self.to_field_name 控制。

幸运的是,django_filtersFilter.field() 方法在实例化实际字段时包括以下内容。

self._field = self.field_class(required=self.required,
    label=self.label, widget=self.widget, **self.extra)

需要特别注意的是self.extra,它来自于Filter.__init__()中的self.extra = kwargs,因此我们只需要在ModelMultipleChoiceFilter中传递一个额外的to_field_name关键字参数,它将通过到底层的ModelMultipleChoiceField

所以(跳过这里以获取实际解决方案!),你想要的实际代码是:

tags = django_filters.ModelMultipleChoiceFilter(
    name='sitetags__name',
    to_field_name='name',
    lookup_type='in',
    queryset=SiteTag.objects.all()
)

所以,你刚才发布的代码非常接近正确!我不知道这个解决方案对你来说是否仍然相关,但希望它能帮助其他人在将来找到答案!


1
这个项目最终被搁置了,因为它引起了太多争议 - 但这已经是我第二次编写这种功能了。我很可能会再次使用Django Filters,所以当第三次出现时,我会非常高兴!我认为把你的解决方案纳入官方文档中是值得的。如果你不想做这件事(并且获得街头信用),请告诉我 - 我会尽力找时间。 - Daniel Devine
1
我在我的当前项目中使用了Django Filters,并再次遇到了这个问题。感谢您的答案! - Daniel Devine
这似乎有所帮助,但是当通过两个或多个GET参数提供多个值时,不幸的是它不起作用。 - mlissner
很好的回答。我认为可能有一个错误。应该是:name='sitetags',而不是:name='sitetags__name'。 - RKI
2
@RKI在OP的情况下,我认为你是正确的,只有name='sitetags'会起作用,但我认为这仅仅是因为SiteTag__str__方法是return self.name。额外的__name只允许您指定要匹配的确切字段,并且在__str__方法返回更复杂的内容时仍然可以工作。 - Grace B
显示剩余3条评论

0
我使用了一个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

他们将得到被认定为黑人或白人的法官。
附注:是的,我知道这并不是所有可能种族的完整集合。但这是美国人口普查收集的内容!

这样不会获取既是黑人又是白人的种族吗?黑人或白人应该像?race=w,b,对吧? - Mario
我不再确定了,但它已经上线运行多年了。 - mlissner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接