Django-filter: NullBooleanField 和 None 值

创建于 2016-10-14  ·  3评论  ·  资料来源: carltongibson/django-filter

您好,我今天遇到了一些非常有趣的关于 NullBooleanField 和用于过滤 db 中的空值的空 ('', 'None') 值。
作为记录,我使用 django rest 框架。

这是我的经验:

我创建了一个 Django 模型:

class MyModel(models.Model):
    is_solved = models.NullBooleanField()

在 django rest 框架中,我声明了我的视图:

class MyModelFilter(filters.FilterSet):
    class Meta:
        model = MyModel
        fields = ('is_solved',)


class MyModelViewSet(ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
    filter_backends = (filters.DjangoFilterBackend,)
    filter_class = MyModelFilter

有了这个,我在字段is_solved上使用 django rest 框架进行了过滤。
过滤器使用带有标签/值的选择小部件:

choices = (('', _('Unknown')),
           ('true', _('Yes')),
           ('false', _('No')))

如此处所述: https ://github.com/carltongibson/django-filter/blob/develop/django_filters/widgets.py#L103

当我使用此过滤器时,如果我选择Unknown ,则会返回所有内容......所以没有过滤......
我想要的是在db中只有is_solved设置为null的对象。

此外,我想改变is_solved的显示方式,所以我对我的模型做了一些修改。
新模式:

CHOICES = (
    (None, "OFF"),
    (True, "YES"),
    (False, "NO")
)
class MyModel(models.Model):
    is_solved = models.NullBooleanField(choices=CHOICES)

通过此修改,django rest 框架中的过滤器为我提供了正确的 Select Widget,但它现在使用具有自己的小部件的 ChoiceField。
这也更有意义,因为现在我在创建模型而不是文本输入时有一个选择框。
但是当我选择OFF进行过滤时,url 将 query_parameter 设置为?is_solved= :/
无论如何,它适用于 True 和 False。

因此,为了使其与 django rest 框架一起使用,我将选择更改为:

CHOICES = (
    ('None', "OFF"),
    (True, "YES"),
    (False, "NO")
)

注意'None'而不是None 。 没关系,我现在有一个查询参数?is_solved=None
但是现在,这个过滤器没有返回任何东西......列表是空的......

让我把手伸进 django_filters 代码:)

关键在这部分代码中:
https://github.com/carltongibson/django-filter/blob/develop/django_filters/filters.py#L172

    def filter(self, qs, value):
        if isinstance(value, Lookup):
            lookup = six.text_type(value.lookup_type)
            value = value.value
        else:
            lookup = self.lookup_expr
        if value in EMPTY_VALUES:
            return qs
        if self.distinct:
            qs = qs.distinct()
        qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
        return qs

这段小代码进行过滤。
我发现 dkango QuerySet 行为非常有趣。

其中重要的几行是:

if value in EMPTY_VALUES:
    return qs

如果我们有一个空值,即[], (), u'', {}, None ,则返回非过滤查询
但是我们的值是'None'所以没关系,所有的先例代码都没有将'None'值转换为None 。 但是使用没有choicesNullBooleanField #$ 会做一些完全不同的事情,因为该字段不被视为 ChoiceField :
来自 django 核心的代码:

class NullBooleanField(BooleanField):
    """
    A field whose valid values are None, True and False. Invalid values are
    cleaned to None.
    """
    widget = NullBooleanSelect

    def to_python(self, value):
        """
        Explicitly checks for the string 'True' and 'False', which is what a
        hidden field will submit for True and False, for 'true' and 'false',
        which are likely to be returned by JavaScript serializations of forms,
        and for '1' and '0', which is what a RadioField will submit. Unlike
        the Booleanfield we need to explicitly check for True, because we are
        not using the bool() function
        """
        if value in (True, 'True', 'true', '1'):
            return True
        elif value in (False, 'False', 'false', '0'):
            return False
        else:
            return None

    def validate(self, value):
        pass

现在事情变得非常有趣......
看看这一行:

qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})

它进行过滤以传递给 django core db Queryset。
我们可以将其翻译为:

qs = qs.filter(is_solved__exact='None')

此操作将返回一个空查询集...
它将产生一个带有is_solved = None的 sql 查询

但是如果我们测试:

qs = qs.filter(is_solved__exact=None)

它仅正确返回 db 中具有null值的实例。
它将产生一个带有is_solved IS NULL的 sql 查询
这是完全相同的测试:

qs = qs.filter(is_solved__isnull=True)

但是, 'True''False'值没有这个问题。 Django 的转录是正确的。

我们可以考虑用isnull覆盖过滤器的 lookup_epxr,但我们的过滤器不适用于TrueFalse值...

在这一点上,我真的不知道如何处理这种行为,django bug? django过滤器遗漏了什么?

无论如何,我已经能够通过重写 django 过滤器的filter函数来使其工作:

    def filter(self, qs, value):
        from django.db.models import NullBooleanField

        if isinstance(value, Lookup):
            lookup = six.text_type(value.lookup_type)
            value = value.value
        else:
            lookup = self.lookup_expr
        if value in EMPTY_VALUES:
            return qs
        if self.distinct:
            qs = qs.distinct()

        # if original field is a NullBooleanField, we need to transform 'None' value as None
        if isinstance(self.model._meta.get_field(self.name), NullBooleanField) and value in ('None',):
            value = None

        qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
        return qs

这看起来像一个黑客,它现在不让我高兴......
如果有人有兴趣思考这个问题,我会很高兴听到的! :D

现在,对这个修改有什么想法吗?
现在,我想我将使用带有选择选项的IntegerField来不必使用分叉版本的 django 过滤器...因为我认为这不是 django 过滤器问题的开始:)

谢谢,

最有用的评论

谢谢@carltongibson ,也谢谢@rpkilby
刚刚拉了开发分支。

NullBooleanField 现在正在使用具有此配置的 django rest 框架:
Model :

CHOICES = (
    (None, "OFF"),
    (True, "YES"),
    (False, "NO")
)
class MyModel(models.Model):
    is_solved = models.NullBooleanField(choices=CHOICES)

View

choices = (
    (True, "YES"),
    (False, "NO")
)


class MyModelFilter(filters.FilterSet):

    is_solved = ChoiceFilter(null_label='OFF', choices=choices)

    class Meta:
        model = MyModel
        fields = ('is_solved',)


class MyModelViewSet(ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
    filter_backends = (filters.DjangoFilterBackend,)
    filter_class = MyModelFilter

注意选择元组之间的区别。

所有3条评论

@GuillaumeCisco - 这里似乎存在三个问题:

  1. BooleanWidget不适用于NullBooleanField s,因为它不验证空值。

可能应该有一个NullBooleanWidget来容纳附加选项。

  1. 您想要覆盖选项的文本标签(开/关而不是是/否)。

这可能最好通过子类化小部件类并手动设置choices属性来处理。 BooleanWidget不能真正接受选择参数,因为它需要特定的值( 'true' / 'false'而不是True / False ) . 这同样适用于潜在的NullBooleanWidget

  1. None过滤存在问题,因为它没有通过空值检查。

这里需要一个神奇的值,例如'None''null'filter()方法必须考虑到这一点。

我认为#519 涵盖了大部分内容。

@GuillaumeCisco请查看。 如果有您认为应该涵盖的特定测试用例,您是否可以打开 PR 添加该测试用例,我们可以进行审核。

谢谢。

谢谢@carltongibson ,也谢谢@rpkilby
刚刚拉了开发分支。

NullBooleanField 现在正在使用具有此配置的 django rest 框架:
Model :

CHOICES = (
    (None, "OFF"),
    (True, "YES"),
    (False, "NO")
)
class MyModel(models.Model):
    is_solved = models.NullBooleanField(choices=CHOICES)

View

choices = (
    (True, "YES"),
    (False, "NO")
)


class MyModelFilter(filters.FilterSet):

    is_solved = ChoiceFilter(null_label='OFF', choices=choices)

    class Meta:
        model = MyModel
        fields = ('is_solved',)


class MyModelViewSet(ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
    filter_backends = (filters.DjangoFilterBackend,)
    filter_class = MyModelFilter

注意选择元组之间的区别。

此页面是否有帮助?
0 / 5 - 0 等级