Bonjour, j'ai rencontré quelque chose de très intéressant aujourd'hui concernant NullBooleanField et la valeur vide ('', 'None') pour filtrer les valeurs nulles dans db.
Pour mémoire, j'utilise django rest framework.
Voici mon expérience :
Je crée un modèle Django :
class MyModel(models.Model):
is_solved = models.NullBooleanField()
Et dans django rest framework, je déclare ma 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
Avec cela, j'ai un filtre sur le champ is_solved
avec django rest framework.
Le filtre utilise un widget de sélection avec étiquette/valeurs :
choices = (('', _('Unknown')),
('true', _('Yes')),
('false', _('No')))
Comme décrit ici : https://github.com/carltongibson/django-filter/blob/develop/django_filters/widgets.py#L103
Lorsque j'utilise ce filtre, si je sélectionne Unknown
, tout est renvoyé... donc rien n'est filtré...
Ce que je voulais, c'est n'avoir que des objets avec is_solved
définis sur null
dans db.
De plus, je voulais changer la façon dont is_solved
est affiché, j'ai donc fait une petite modification sur mon modèle.
Nouveau modèle:
CHOICES = (
(None, "OFF"),
(True, "YES"),
(False, "NO")
)
class MyModel(models.Model):
is_solved = models.NullBooleanField(choices=CHOICES)
Avec cette modification, le filtre dans le framework django rest me donne le bon widget Select, mais il utilise maintenant le ChoiceField, avec son propre widget.
Cela a également plus de sens, car j'ai maintenant un selectBox lors de la création d'un modèle au lieu d'une entrée de texte.
Mais lorsque je sélectionne OFF
pour le filtrage, l'url définit un query_parameter comme ?is_solved=
:/
Cela fonctionne bien avec vrai et faux de toute façon.
Donc, pour le faire fonctionner avec le framework django rest, j'ai changé mes choix en:
CHOICES = (
('None', "OFF"),
(True, "YES"),
(False, "NO")
)
Notez 'None'
au lieu de None
. Et c'est bon, j'ai maintenant un paramètre de requête ?is_solved=None
mais maintenant, rien n'est retourné avec ce filtre... La liste est vide...
M'a fait mettre les mains dans le code django_filters :)
La clé est dans cette partie du code :
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
Ce petit morceau de code fait le filtrage.
Et j'ai trouvé quelque chose de très très intéressant avec le comportement de dkango QuerySet.
Une partie des lignes importantes sont :
if value in EMPTY_VALUES:
return qs
La requête est retournée non filtrée si nous avons une valeur vide, qui est [], (), u'', {}, None
Mais notre valeur est 'None'
donc ça va, tout le code précédent n'a pas transformé la 'None'
en None
. Mais utiliser le NullBooleanField
sans choices
fait quelque chose de totalement différent car le champ n'est pas considéré comme un ChoiceField :
code du noyau 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
Maintenant, les choses deviennent vraiment très intéressantes...
Jetez un oeil à cette ligne :
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
Il effectue le filtrage pour passer à django core db Queryset.
Nous pouvons le traduire en :
qs = qs.filter(is_solved__exact='None')
Cette opération renverra un ensemble de requêtes vide...
Il produira une requête sql avec is_solved = None
Mais si on teste :
qs = qs.filter(is_solved__exact=None)
Il renvoie correctement uniquement les instances avec des valeurs null
dans db.
Il produira une requête sql avec is_solved IS NULL
C'est exactement la même chose à tester :
qs = qs.filter(is_solved__isnull=True)
Cependant, les valeurs 'True'
et 'False'
n'ont pas ce problème. La transcription est correcte par Django.
Nous pourrions penser à remplacer le lookup_epxr de notre filtre par isnull
mais notre filtre ne fonctionnera pas avec les valeurs True
et False
...
À ce stade, je ne sais vraiment pas comment gérer ce comportement, bug django ? les filtres django manquent quelque chose ?
Quoi qu'il en soit, j'ai réussi à le faire fonctionner en réécrivant la fonction filter
des filtres 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
Cela ressemble à un hack et ça ne me plaît pas pour l'instant...
Si quelqu'un est intéressé à réfléchir à cette question, je serai heureux de l'entendre! :RÉ
Pour l'instant, des avis sur cette modification ?
En ce moment, je pense que je vais utiliser un IntegerField
avec une option de choix pour ne pas avoir à utiliser une version fourchue des filtres Django... car je pense que ce n'est pas un problème de filtres Django au début :)
Merci,
Salut @GuillaumeCisco - il semble qu'il y ait trois problèmes ici :
BooleanWidget
ne fonctionne pas avec NullBooleanField
s, car il ne valide pas les valeurs nulles.Il devrait probablement y avoir un NullBooleanWidget
qui accueille l'option supplémentaire.
Ceci est probablement mieux géré en sous-classant la classe widget et en définissant manuellement la propriété choices
. BooleanWidget
ne peut pas vraiment accepter un argument de choix, car il attend des valeurs spécifiques ( 'true'
/ 'false'
au lieu de True
/ False
) . La même chose s'appliquerait à un potentiel NullBooleanWidget
.
None
, car il ne passe pas la vérification des valeurs vides.Une valeur magique telle que 'None'
ou 'null'
va être nécessaire ici. La méthode filter()
devra en tenir compte.
Je pense que la majeure partie de cela a été couverte par # 519.
@GuillaumeCisco , veuillez revoir cela. S'il y a un cas de test spécifique qui, selon vous, devrait être couvert, pourriez-vous ouvrir un PR en ajoutant cela et nous pourrons l'examiner.
Merci.
Merci @carltongibson et merci aussi @rpkilby
Je viens de tirer la branche develop.
Le NullBooleanField fonctionne maintenant avec le framework django rest avec cette configuration :
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
Notez la différence entre les tuples de choix.
Commentaire le plus utile
Merci @carltongibson et merci aussi @rpkilby
Je viens de tirer la branche develop.
Le NullBooleanField fonctionne maintenant avec le framework django rest avec cette configuration :
Model
:View
:Notez la différence entre les tuples de choix.