Привет, сегодня я столкнулся с чем-то очень интересным, касающимся NullBooleanField и пустого ('', 'None') значения для фильтрации нулевых значений в db.
Для справки, я использую django rest framework.
Вот мой опыт:
Я создаю модель джанго:
class MyModel(models.Model):
is_solved = models.NullBooleanField()
А в фреймворке django rest я объявляю свой 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
При этом у меня есть фильтр в поле 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 framework дает мне правильный виджет Select Widget, но теперь он использует ChoiceField со своим собственным виджетом.
Это также имеет больше смысла, так как теперь у меня есть selectBox при создании модели вместо ввода текста.
Но когда я выбираю OFF
для фильтрации, URL-адрес устанавливает параметр query_parameter как ?is_solved=
:/
В любом случае, это хорошо работает с True и False.
Итак, чтобы заставить его работать с инфраструктурой отдыха django, я изменил свой выбор на:
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
. Но использование NullBooleanField
без choices
делает что-то совершенно другое, поскольку поле не считается ChoiceField :
код из ядра джанго:
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.
Мы можем перевести это на:
qs = qs.filter(is_solved__exact='None')
Эта операция вернет пустой набор запросов...
Он создаст sql-запрос с is_solved = None
Но если мы проверим:
qs = qs.filter(is_solved__exact=None)
Он правильно возвращает только экземпляры со значениями null
в db.
Он создаст sql-запрос с is_solved IS NULL
Это то же самое, что нужно проверить:
qs = qs.filter(is_solved__isnull=True)
Однако значения 'True'
и 'False'
не имеют этой проблемы. Транскрипция верна Джанго.
Мы могли бы переопределить lookup_epxr нашего фильтра на isnull
, но наш фильтр не будет работать со значениями True
и False
...
На данный момент я действительно не знаю, как справиться с этим поведением, ошибкой django? фильтры django что-то упустили?
Во всяком случае, я смог заставить его работать, переписав функцию filter
фильтров 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
Это похоже на хак и меня это пока не радует...
Если кому-то интересно подумать по этому вопросу, буду рад услышать! :D
А пока есть мысли по поводу этой модификации?
Прямо сейчас я думаю, что буду использовать IntegerField
с опцией выбора, чтобы не использовать разветвленную версию фильтров django... поскольку я думаю, что это не проблема фильтров django в начале :)
Спасибо,
Привет @GuillaumeCisco - похоже, здесь есть три проблемы:
BooleanWidget
не работает с NullBooleanField
s, так как не проверяет нулевые значения.Вероятно, должен быть NullBooleanWidget
, который вмещает дополнительную опцию.
Это, вероятно, лучше всего решается созданием подкласса класса виджета и установкой свойства choices
вручную. BooleanWidget
самом деле не может принять аргумент выбора, так как он ожидает конкретных значений ( 'true'
/ 'false'
вместо True
/ False
) . То же самое относится и к потенциальному NullBooleanWidget
.
None
, так как она не проходит проверку на пустое значение.Здесь потребуется магическое значение, такое как 'None'
или 'null'
. Метод filter()
должен учитывать это.
Я думаю, что большая часть этого была покрыта № 519.
@GuillaumeCisco , пожалуйста, просмотрите это. Если есть конкретный тестовый пример, который, по вашему мнению, должен быть охвачен, не могли бы вы открыть PR, добавив его, и мы можем его просмотреть.
Спасибо.
Спасибо @carltongibson и вам тоже @rpkilby
Просто потянул ветку разработки.
NullBooleanField теперь работает с инфраструктурой отдыха django с этой конфигурацией:
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
Обратите внимание на разницу между кортежами выбора.
Самый полезный комментарий
Спасибо @carltongibson и вам тоже @rpkilby
Просто потянул ветку разработки.
NullBooleanField теперь работает с инфраструктурой отдыха django с этой конфигурацией:
Model
:View
:Обратите внимание на разницу между кортежами выбора.