带 DRF 的 Django-filter - 在使用相同的查找应用多个值时如何做 'and'?
Django-filter with DRF - How to do 'and' when applying multiple values with the same lookup?
这是我使用的过滤器集的一个稍微简化的示例,我将其与 Django Rest Framework 的 DjangoFilterBackend 一起使用。我希望能够向 /api/bookmarks/?title__contains=word1&title__contains=word2
发送请求并返回包含这两个词的结果,但目前它忽略了第一个参数,只过滤了 word2。
任何帮助将不胜感激!
class BookmarkFilter(django_filters.FilterSet):
class Meta:
model = Bookmark
fields = {
'title': ['startswith', 'endswith', 'contains', 'exact', 'istartswith', 'iendswith', 'icontains', 'iexact'],
}
class BookmarkViewSet(viewsets.ModelViewSet):
serializer_class = BookmarkSerializer
permission_classes = (IsAuthenticated,)
filter_backends = (DjangoFilterBackend,)
filter_class = BookmarkFilter
ordering_fields = ('title', 'date', 'modified')
ordering = '-modified'
page_size = 10
主要问题是您需要一个了解如何对多个值进行操作的过滤器。基本上有两种选择:
- 使用
MultipleChoiceFilter
(不推荐用于此实例)
- 编写自定义过滤器class
使用MultipleChoiceFilter
class BookmarkFilter(django_filters.FilterSet):
title__contains = django_filters.MultipleChoiceFilter(
name='title',
lookup_expr='contains',
conjoined=True, # uses AND instead of OR
choices=[???],
)
class Meta:
...
虽然这保留了您想要的语法,但问题是您必须构建一个选项列表。我不确定您是否可以 simplify/reduce 可能的选择,但即兴表演似乎您需要从数据库中获取所有标题,将标题拆分为不同的词,然后创建一个集合以删除重复项。这似乎是 expensive/slow,具体取决于您有多少条记录。
自定义Filter
或者,您可以创建自定义过滤器 class - 如下所示:
class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
def filter(self, qs, value):
# value is either a list or an 'empty' value
values = value or []
for value in values:
qs = super(MultiValueCharFilter, self).filter(qs, value)
return qs
class BookmarkFilter(django_filters.FilterSet):
title__contains = MultiValueCharFilter(name='title', lookup_expr='contains')
class Meta:
...
用法(注意值为comma-separated):
GET /api/bookmarks/?title__contains=word1,word2
结果:
qs.filter(title__contains='word1').filter(title__contains='word2')
语法略有改变,但CSV-based过滤器不需要构建不必要的选择集。
请注意,实际上不可能支持 ?title__contains=word1&title__contains=word2
语法,因为小部件无法呈现合适的 html 输入。您可能需要使用 SelectMultiple
(同样需要选择),或者在客户端上使用 javascript 以 add/remove 具有相同 name
属性的其他文本输入。
无需赘述,过滤器和过滤器集只是 Django 表单的扩展。
- 一个
Filter
有一个形式 Field
,它又有一个 Widget
。
- 一个
FilterSet
由Filter
组成。
- A
FilterSet
根据其过滤器字段生成内部表单。
每个过滤器组件的职责:
- 小部件从
data
QueryDict
中检索原始值。
- 该字段验证原始值。
- 过滤器使用经过验证的值构建对查询集的
filter()
调用。
为了对同一个过滤器应用多个值,您需要一个了解如何对多个值进行操作的过滤器、字段和小部件。
自定义过滤器通过混入 BaseCSVFilter
实现这一点,后者又将 "comma-separation => list" 功能混入组合字段和小部件 classes。
我建议查看 CSV 混合宏的源代码,但简而言之:
- widget 将传入值拆分为值列表。
- field 通过验证 'main' 字段 class 上的单个值(例如
CharField
或 IntegerField
)来验证整个值列表。该字段还派生了混合的小部件。
- filter 简单地导出混合字段 class.
CSV 过滤器旨在与 in
和 range
查找一起使用,它们接受值列表。在这种情况下,contains
需要一个值。 filter()
方法通过遍历值并将各个过滤器调用链接在一起来解决此问题。
您可以像这样创建自定义列表字段:
from django.forms.widgets import SelectMultiple
from django import forms
class ListField(forms.Field):
widget = SelectMultiple
def __init__(self, field, *args, **kwargs):
super(ListField, self).__init__( *args, **kwargs)
self.field = field
def validate(self, value):
super(ListField, self).validate(value)
for val in value:
self.field.validate(val)
def run_validators(self, value):
for val in value:
self.field.run_validators(val)
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
return [self.field.to_python(val) for val in value]
并使用 MultipleChoiceFilter 创建自定义过滤器:
class ContainsListFilter(django_filters.MultipleChoiceFilter):
field_class = ListField
def get_filter_predicate(self, v):
name = '%s__contains' % self.name
try:
return {name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {name: v}
之后您可以使用自定义过滤器创建 FilterSet:
from django.forms import CharField
class StorageLocationFilter(django_filters.FilterSet):
title_contains = ContainsListFilter(field=CharField())
为我工作。希望对你有用。
这是一个可以正常工作的示例代码:
它支持 - product?name=p1,p2,p3 并将 return 名称为 (p1,p2,p3)
的产品
def resolve_csvfilter(queryset, name, value):
lookup = { f'{name}__in': value.split(",") }
queryset = queryset.filter(**lookup)
return queryset
class ProductFilterSet(FilterSet):
name = CharFilter(method=resolve_csvfilter)
class Meta:
model = Product
fields = ['name']
参考:https://django-filter.readthedocs.io/en/master/guide/usage.html#customize-filtering-with-filter-method
https://github.com/carltongibson/django-filter/issues/137
这是我使用的过滤器集的一个稍微简化的示例,我将其与 Django Rest Framework 的 DjangoFilterBackend 一起使用。我希望能够向 /api/bookmarks/?title__contains=word1&title__contains=word2
发送请求并返回包含这两个词的结果,但目前它忽略了第一个参数,只过滤了 word2。
任何帮助将不胜感激!
class BookmarkFilter(django_filters.FilterSet):
class Meta:
model = Bookmark
fields = {
'title': ['startswith', 'endswith', 'contains', 'exact', 'istartswith', 'iendswith', 'icontains', 'iexact'],
}
class BookmarkViewSet(viewsets.ModelViewSet):
serializer_class = BookmarkSerializer
permission_classes = (IsAuthenticated,)
filter_backends = (DjangoFilterBackend,)
filter_class = BookmarkFilter
ordering_fields = ('title', 'date', 'modified')
ordering = '-modified'
page_size = 10
主要问题是您需要一个了解如何对多个值进行操作的过滤器。基本上有两种选择:
- 使用
MultipleChoiceFilter
(不推荐用于此实例) - 编写自定义过滤器class
使用MultipleChoiceFilter
class BookmarkFilter(django_filters.FilterSet):
title__contains = django_filters.MultipleChoiceFilter(
name='title',
lookup_expr='contains',
conjoined=True, # uses AND instead of OR
choices=[???],
)
class Meta:
...
虽然这保留了您想要的语法,但问题是您必须构建一个选项列表。我不确定您是否可以 simplify/reduce 可能的选择,但即兴表演似乎您需要从数据库中获取所有标题,将标题拆分为不同的词,然后创建一个集合以删除重复项。这似乎是 expensive/slow,具体取决于您有多少条记录。
自定义Filter
或者,您可以创建自定义过滤器 class - 如下所示:
class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
def filter(self, qs, value):
# value is either a list or an 'empty' value
values = value or []
for value in values:
qs = super(MultiValueCharFilter, self).filter(qs, value)
return qs
class BookmarkFilter(django_filters.FilterSet):
title__contains = MultiValueCharFilter(name='title', lookup_expr='contains')
class Meta:
...
用法(注意值为comma-separated):
GET /api/bookmarks/?title__contains=word1,word2
结果:
qs.filter(title__contains='word1').filter(title__contains='word2')
语法略有改变,但CSV-based过滤器不需要构建不必要的选择集。
请注意,实际上不可能支持 ?title__contains=word1&title__contains=word2
语法,因为小部件无法呈现合适的 html 输入。您可能需要使用 SelectMultiple
(同样需要选择),或者在客户端上使用 javascript 以 add/remove 具有相同 name
属性的其他文本输入。
无需赘述,过滤器和过滤器集只是 Django 表单的扩展。
- 一个
Filter
有一个形式Field
,它又有一个Widget
。 - 一个
FilterSet
由Filter
组成。 - A
FilterSet
根据其过滤器字段生成内部表单。
每个过滤器组件的职责:
- 小部件从
data
QueryDict
中检索原始值。 - 该字段验证原始值。
- 过滤器使用经过验证的值构建对查询集的
filter()
调用。
为了对同一个过滤器应用多个值,您需要一个了解如何对多个值进行操作的过滤器、字段和小部件。
自定义过滤器通过混入 BaseCSVFilter
实现这一点,后者又将 "comma-separation => list" 功能混入组合字段和小部件 classes。
我建议查看 CSV 混合宏的源代码,但简而言之:
- widget 将传入值拆分为值列表。
- field 通过验证 'main' 字段 class 上的单个值(例如
CharField
或IntegerField
)来验证整个值列表。该字段还派生了混合的小部件。 - filter 简单地导出混合字段 class.
CSV 过滤器旨在与 in
和 range
查找一起使用,它们接受值列表。在这种情况下,contains
需要一个值。 filter()
方法通过遍历值并将各个过滤器调用链接在一起来解决此问题。
您可以像这样创建自定义列表字段:
from django.forms.widgets import SelectMultiple
from django import forms
class ListField(forms.Field):
widget = SelectMultiple
def __init__(self, field, *args, **kwargs):
super(ListField, self).__init__( *args, **kwargs)
self.field = field
def validate(self, value):
super(ListField, self).validate(value)
for val in value:
self.field.validate(val)
def run_validators(self, value):
for val in value:
self.field.run_validators(val)
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
return [self.field.to_python(val) for val in value]
并使用 MultipleChoiceFilter 创建自定义过滤器:
class ContainsListFilter(django_filters.MultipleChoiceFilter):
field_class = ListField
def get_filter_predicate(self, v):
name = '%s__contains' % self.name
try:
return {name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {name: v}
之后您可以使用自定义过滤器创建 FilterSet:
from django.forms import CharField
class StorageLocationFilter(django_filters.FilterSet):
title_contains = ContainsListFilter(field=CharField())
为我工作。希望对你有用。
这是一个可以正常工作的示例代码: 它支持 - product?name=p1,p2,p3 并将 return 名称为 (p1,p2,p3)
的产品def resolve_csvfilter(queryset, name, value):
lookup = { f'{name}__in': value.split(",") }
queryset = queryset.filter(**lookup)
return queryset
class ProductFilterSet(FilterSet):
name = CharFilter(method=resolve_csvfilter)
class Meta:
model = Product
fields = ['name']
参考:https://django-filter.readthedocs.io/en/master/guide/usage.html#customize-filtering-with-filter-method https://github.com/carltongibson/django-filter/issues/137