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,
Hola, @GuillaumeCisco : parece que hay tres problemas aquí:
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.
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.
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.
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
:View
:Tenga en cuenta la diferencia entre las tuplas de opciones.