Django-filter: Valor NullBooleanField e Nenhum

Criado em 14 out. 2016  ·  3Comentários  ·  Fonte: carltongibson/django-filter

Olá, encontrei algo muito interessante hoje sobre NullBooleanField e o valor vazio ('', 'Nenhum') para filtrar valores nulos em db.
Para o registro, eu uso o framework django rest.

Aqui está a minha experiência:

Eu crio um modelo django:

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

E no django rest framework, declaro minha View:

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

Com isso, tenho um filtro no campo is_solved com django rest framework.
O filtro usa um widget de seleção com rótulo/valores:

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

Conforme descrito aqui: https://github.com/carltongibson/django-filter/blob/develop/django_filters/widgets.py#L103

Quando eu uso esse filtro, se eu selecionar Unknown , tudo é retornado... então nada é filtrado...
O que eu queria é ter apenas objetos com is_solved definido como null em db.

Além disso, eu queria mudar a forma is_solved é exibido, então fiz uma pequena modificação no meu Model.
Novo modelo:

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

Com esta modificação, o filtro no django rest framework me dá o Select Widget correto, mas agora ele usa o ChoiceField, com seu próprio widget.
Também faz mais sentido, pois agora tenho um selectBox ao criar um modelo em vez de uma entrada de texto.
Mas quando seleciono OFF para filtrar, o url define um query_parameter como ?is_solved= :/
Ele funciona bem com True e False de qualquer maneira.

Então, para fazê-lo funcionar com o django rest framework, mudei minhas escolhas para:

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

Anote 'None' em vez de None . E tudo bem, agora tenho um parâmetro de consulta ?is_solved=None
mas agora, nada é retornado com este filtro... A lista está vazia...

Me fez colocar minhas mãos no código django_filters :)

A chave está nesta parte do código:
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

Este pequeno pedaço de código faz a filtragem.
E eu encontrei algo muito interessante com o comportamento do dkango QuerySet.

Uma parte das linhas importantes são:

if value in EMPTY_VALUES:
    return qs

A consulta é retornada não filtrada se tivermos um valor vazio, que é [], (), u'', {}, None
Mas nosso valor é 'None' então tudo bem, todo o código precedente não transformou o 'None' para None . Mas usar o NullBooleanField sem choices faz algo totalmente diferente, pois o campo não é considerado um ChoiceField :
código do núcleo do 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

Agora as coisas ficam realmente muito interessantes...
Dê uma olhada nesta linha:

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

Ele faz a filtragem para passar para o django core db Queryset.
Podemos traduzir para:

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

Esta operação retornará um queryset vazio...
Ele irá produzir uma consulta sql com is_solved = None

Mas se testarmos:

qs = qs.filter(is_solved__exact=None)

Ele retorna corretamente apenas as instâncias com valores null em db.
Ele irá produzir uma consulta sql com is_solved IS NULL
Esta é exatamente a mesma coisa para testar:

qs = qs.filter(is_solved__isnull=True)

No entanto, os valores 'True' e 'False' não apresentam esse problema. A transcrição está correta pelo Django.

Podemos pensar em substituir o lookup_epxr do nosso filtro por isnull mas nosso filtro não funcionará com os valores True e False ...

Neste ponto, eu realmente não sei como lidar com esse comportamento, django bug ? filtros django faltando alguma coisa?

De qualquer forma, consegui fazê-lo funcionar reescrevendo a função filter dos filtros django:

    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

Isso parece um hack e não me agrada por enquanto...
Se alguém estiver interessado em pensar sobre este assunto, ficarei feliz em ouvi-lo! :D

Por enquanto, alguma opinião sobre esta modificação?
No momento, acho que vou usar um IntegerField com uma opção de opções para não ter que usar uma versão bifurcada de filtros django ...

Obrigado,

Comentários muito úteis

Obrigado @carltongibson e obrigado também @rpkilby
Acabei de puxar o ramo de desenvolvimento.

O NullBooleanField está trabalhando agora com o django rest framework com esta configuração:
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

Observe a diferença entre as tuplas de escolhas.

Todos 3 comentários

Oi @GuillaumeCisco - parece que há três problemas aqui:

  1. BooleanWidget não funciona com NullBooleanField s, pois não valida valores nulos.

Provavelmente deve haver um NullBooleanWidget que acomoda a opção adicional.

  1. Você deseja substituir os rótulos de texto das opções (ativado/desativado em vez de sim/não).

Isso provavelmente é melhor tratado com uma subclasse da classe widget e definindo a propriedade choices manualmente. BooleanWidget não pode realmente aceitar um argumento de opções, pois espera valores específicos ( 'true' / 'false' em vez de True / False ) . O mesmo se aplicaria a um potencial NullBooleanWidget .

  1. Há problemas ao filtrar por None , pois não passa na verificação de valor vazio.

Um valor mágico como 'None' ou 'null' será necessário aqui. O método filter() terá que levar em conta isso.

Acho que a maior parte disso foi coberta pelo #519.

@GuillaumeCisco , revise isso. Se houver um caso de teste específico que você acha que deve ser coberto, você pode abrir um PR adicionando isso e podemos revisar.

Obrigado.

Obrigado @carltongibson e obrigado também @rpkilby
Acabei de puxar o ramo de desenvolvimento.

O NullBooleanField está trabalhando agora com o django rest framework com esta configuração:
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

Observe a diferença entre as tuplas de escolhas.

Esta página foi útil?
0 / 5 - 0 avaliações