Django-filter: NullBooleanFieldおよびNone値

作成日 2016年10月14日  ·  3コメント  ·  ソース: carltongibson/django-filter

こんにちは、私は今日N​​ullBooleanFieldとdbのnull値をフィルタリングするための空の( ''、 'None')値に関して非常に興味深いことに遭遇しました。
記録のために、私はdjangorestフレームワークを使用します。

これが私の経験です:

djangoモデルを作成します:

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

そして、django restフレームワークで、私は自分の見解を宣言します。

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にdjangorestフレームワークのフィルターがあります。
フィルタは、ラベル/値を持つ選択ウィジェットを使用します。

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

ここで説明されているように: https ://github.com/carltongibson/django-filter/blob/develop/django_filters/widgets.py#L103

このフィルターを使用するときにUnknownを選択すると、すべてが返されます...したがって、何もフィルターされません...
私が欲しかったのは、dbでis_solvednullに設定されたオブジェクトのみを持つことです。

さらに、 is_solvedの表示方法を変更したかったので、モデルに少し変更を加えました。
ニューモデル:

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

この変更により、django restフレームワークのフィルターは、正しいSelect Widgetを提供しますが、独自のウィジェットを持つChoiceFieldを使用するようになりました。
また、テキスト入力の代わりにモデルを作成するときにselectBoxがあるので、より理にかなっています。
しかし、フィルタリングにOFFを選択すると、URLはquery_parameterを?is_solved=として設定します:/
とにかく、TrueとFalseでうまく機能します。

そこで、django restフレームワークで動作させるために、選択肢を次のように変更しました。

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

この小さなコードがフィルタリングを行います。
そして、dkangoQuerySetの動作で非常に興味深いものを見つけました。

重要な行の一部は次のとおりです。

if value in EMPTY_VALUES:
    return qs

[], (), u'', {}, Noneである空の値がある場合、クエリはフィルタリングされずに返されます
しかし、私たちの値は'None'なので、問題ありません。すべての先行コードは'None'の値をNoneに変換しませんでした。 ただし、$# choices NullBooleanFieldを使用すると、フィールドがChoiceFieldと見なされないため、まったく異なることが行われます。
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

今、物事は本当に本当に面白いものになります...
この行を見てください:

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

djangoコアdbクエリセットに渡すフィルタリングを行います。
私たちはそれを次のように翻訳することができます:

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

この操作は空のクエリセットを返します...
is_solved = NoneでSQLクエリを生成します

しかし、テストすると:

qs = qs.filter(is_solved__exact=None)

dbにnullの値を持つインスタンスのみを正しく返します。
is_solved IS NULLでSQLクエリを生成します
これは、テストするのとまったく同じことです。

qs = qs.filter(is_solved__isnull=True)

ただし、 'True''False'の値にはこの問題はありません。 転写はDjangoによって正しいです。

フィルタのlookup_epxrをisnullでオーバーライドすることも考えられますが、フィルタはTrueFalseの値では機能しません...

この時点で、私は本当にこの動作を処理する方法を知りません、djangoバグ? 何かが足りないdjangoフィルター?

とにかく、私はdjangoフィルターのfilter関数を書き直すことでそれを機能させることができました:

    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とchoicesオプションを使用して、フォークバージョンのdjangoフィルターを使用する必要がないと思います...これはdjangoフィルターではないと思うので最初に発行します:)

ありがとう、

最も参考になるコメント

@carltongibsonに感謝し、@ rpkilbyにも感謝します
開発ブランチを引っ張っただけです。

NullBooleanFieldは、次の構成でdjangorestフレームワークで動作しています。
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-ここには3つの問題があるようです:

  1. BooleanWidgetは、null値を検証しないため、 NullBooleanFieldでは機能しません。

追加のオプションに対応するNullBooleanWidgetがあるはずです。

  1. 選択肢のテキストラベルを上書きします(yes / noではなくon / off)。

これは、ウィジェットクラスをサブクラス化し、 choicesプロパティを手動で設定することでおそらく最も適切に処理されます。 BooleanWidgetは、特定の値を期待するため、実際には選択肢の引数を受け入れることができません($ True / False $の代わりに$ 'true' / 'false' #$) 。 同じことが潜在的なNullBooleanWidgetにも当てはまります。

  1. 空の値のチェックに合格しないため、 Noneによるフィルタリングに問題があります。

ここでは、 'None''null'などの魔法の値が必要になります。 filter()メソッドはこれを考慮する必要があります。

これの大部分は#519でカバーされていたと思います。

@GuillaumeCiscoはそれを確認してください。 カバーする必要があると思われる特定のテストケースがある場合は、それを追加してPRを開き、レビューすることができます。

ありがとう。

@carltongibsonに感謝し、@ rpkilbyにも感謝します
開発ブランチを引っ張っただけです。

NullBooleanFieldは、次の構成でdjangorestフレームワークで動作しています。
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 評価