Django-filter: NullBooleanField y valor Ninguno

Creado en 14 oct. 2016  ·  3Comentarios  ·  Fuente: carltongibson/django-filter

Hola, encontré algo muy interesante hoy con respecto a NullBooleanField y el valor vacío ('', 'Ninguno') para filtrar valores nulos en db.
Para el registro, uso el marco de descanso de Django.

Aquí está mi experiencia:

Creo un modelo Django:

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

Y en django rest framework, declaro mi Vista:

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

Con esto, tengo un filtro en el campo is_solved con django rest framework.
El filtro usa un widget de selección con etiqueta/valores:

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

Como se describe aquí: https://github.com/carltongibson/django-filter/blob/develop/django_filters/widgets.py#L103

Cuando uso este filtro, si selecciono Unknown , se devuelve todo... por lo que no se filtra nada...
Lo que quería es tener solo objetos con is_solved establecidos en null en db.

Además, quería cambiar la forma en que se muestra is_solved , así que hice una pequeña modificación en mi modelo.
Nuevo modelo:

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

Con esta modificación, el filtro en django rest framework me da el correcto Select Widget, pero ahora usa ChoiceField, con su propio widget.
También tiene más sentido, ya que ahora tengo un cuadro de selección al crear un modelo en lugar de una entrada de texto.
Pero cuando selecciono OFF para filtrar, la URL establece un parámetro de consulta como ?is_solved= :/
Funciona bien con Verdadero y Falso de todos modos.

Entonces, para que funcione con Django Rest Framework, cambié mis opciones a:

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

Tenga en cuenta 'None' en lugar de None . Y está bien, ahora tengo un parámetro de consulta ?is_solved=None
pero ahora, no se devuelve nada con este filtro... La lista está vacía...

Me hizo poner mis manos en el código django_filters :)

La clave está en esta parte del 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 pequeño fragmento de código hace el filtrado.
Y encontré algo muy, muy interesante con el comportamiento de dkango QuerySet.

Una parte de las líneas importantes son:

if value in EMPTY_VALUES:
    return qs

La consulta se devuelve sin filtrar si tenemos un valor vacío, que es [], (), u'', {}, None
Pero nuestro valor es 'None' , así que está bien, todo el código anterior no transformó el 'None' en None . Pero usar NullBooleanField sin choices hace algo totalmente diferente ya que el campo no se considera como ChoiceField:
código del núcleo de 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

Ahora las cosas se ponen realmente muy interesantes...
Echa un vistazo a esta línea:

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

Hace el filtrado para pasar a django core db Queryset.
Podemos traducirlo a:

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

Esta operación devolverá un conjunto de consultas vacío...
Producirá una consulta sql con is_solved = None

Pero si probamos:

qs = qs.filter(is_solved__exact=None)

Devuelve correctamente solo las instancias con valores null en db.
Producirá una consulta sql con is_solved IS NULL
Esto es exactamente lo mismo para probar:

qs = qs.filter(is_solved__isnull=True)

Sin embargo, los valores 'True' y 'False' no tienen este problema. La transcripción es correcta por Django.

Podríamos pensar en anular el lookup_epxr de nuestro filtro por isnull pero nuestro filtro no funcionará con los valores True y False ...

En este punto, realmente no sé cómo manejar este comportamiento, ¿error de django? ¿A los filtros Django les falta algo?

De todos modos, he podido hacer que funcione reescribiendo la función filter de los filtros de 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

Esto parece un hack y no me agrada por ahora...
Si alguien está interesado en pensar en este tema, ¡estaré encantado de escucharlo! :D

Por ahora, ¿alguna idea sobre esta modificación?
En este momento, creo que usaré un IntegerField con una opción de opciones para no tener que usar una versión bifurcada de los filtros Django... ya que creo que no es un problema de filtros Django al principio :)

Gracias,

Comentario más útil

Gracias @carltongibson y gracias también @rpkilby
Acabo de sacar la rama de desarrollo.

NullBooleanField está funcionando ahora con django rest framework con esta configuración:
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

Tenga en cuenta la diferencia entre las tuplas de opciones.

Todos 3 comentarios

Hola, @GuillaumeCisco : parece que hay tres problemas aquí:

  1. BooleanWidget no funciona con NullBooleanField s, ya que no valida valores nulos.

Probablemente debería haber un NullBooleanWidget que se adapte a la opción adicional.

  1. Desea anular las etiquetas de texto para las opciones (activar/desactivar en lugar de sí/no).

Esto probablemente se maneja mejor subclasificando la clase de widget y configurando la propiedad choices manualmente. BooleanWidget realmente no puede aceptar un argumento de opciones, ya que espera valores específicos ( 'true' / 'false' en lugar de True / False ) . Lo mismo se aplicaría a un NullBooleanWidget potencial.

  1. Hay problemas al filtrar por None , ya que no pasa la verificación de valor vacío.

Aquí será necesario un valor mágico como 'None' o 'null' . El método filter() tendrá que dar cuenta de esto.

Creo que la mayor parte de esto fue cubierta por el #519.

@GuillaumeCisco por favor revise eso. Si hay un caso de prueba específico que cree que debería cubrirse, ¿podría abrir un PR para agregarlo y podemos revisarlo?

Gracias.

Gracias @carltongibson y gracias también @rpkilby
Acabo de sacar la rama de desarrollo.

NullBooleanField está funcionando ahora con django rest framework con esta configuración:
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

Tenga en cuenta la diferencia entre las tuplas de opciones.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

loganknecht picture loganknecht  ·  4Comentarios

sassanh picture sassanh  ·  4Comentarios

blueyed picture blueyed  ·  4Comentarios

xtrinch picture xtrinch  ·  4Comentarios

csarcom picture csarcom  ·  3Comentarios