Django-filter: NullBooleanField и значение None

Созданный на 14 окт. 2016  ·  3Комментарии  ·  Источник: carltongibson/django-filter

Привет, сегодня я столкнулся с чем-то очень интересным, касающимся 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 в начале :)

Спасибо,

Самый полезный комментарий

Спасибо @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

Обратите внимание на разницу между кортежами выбора.

Все 3 Комментарий

Привет @GuillaumeCisco - похоже, здесь есть три проблемы:

  1. BooleanWidget не работает с NullBooleanField s, так как не проверяет нулевые значения.

Вероятно, должен быть NullBooleanWidget , который вмещает дополнительную опцию.

  1. Вы хотите переопределить текстовые метки для вариантов (вкл/выкл вместо да/нет).

Это, вероятно, лучше всего решается созданием подкласса класса виджета и установкой свойства choices вручную. BooleanWidget самом деле не может принять аргумент выбора, так как он ожидает конкретных значений ( 'true' / 'false' вместо True / False ) . То же самое относится и к потенциальному NullBooleanWidget .

  1. Есть проблемы с фильтрацией по 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

Обратите внимание на разницу между кортежами выбора.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги