Django-filter: قيمة NullBooleanField و None

تم إنشاؤها على ١٤ أكتوبر ٢٠١٦  ·  3تعليقات  ·  مصدر: carltongibson/django-filter

مرحبًا ، لقد واجهت شيئًا مثيرًا للاهتمام اليوم بخصوص NullBooleanField والقيمة الفارغة ('' ، 'None') لتصفية القيم الفارغة في db.
للتسجيل ، أستخدم إطار django rest.

ها هي تجربتي:

أقوم بإنشاء نموذج 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 مع إطار عمل django rest.
يستخدم الفلتر أداة تحديد مع تسمية / قيم:

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

كما هو موضح هنا: https://github.com/carltongibson/django-filter/blob/develop/django_filters/widgets.py#L103

عندما أستخدم هذا الفلتر ، إذا حددت Unknown ، فسيتم إرجاع كل شيء ... لذلك لا تتم تصفية أي شيء ...
ما أردت هو الحصول على كائنات فقط مع تعيين is_solved على null بالديسيبل.

علاوة على ذلك ، أردت تغيير طريقة عرض 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 يعين معلمة الاستعلام على النحو ?is_solved= : /
إنه يعمل بشكل جيد مع الصواب والخطأ على أي حال.

لذلك لجعله يعمل مع إطار عمل 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

هذه القطعة الصغيرة من الكود تقوم بالترشيح.
وقد وجدت شيئًا مثيرًا للاهتمام للغاية في سلوك dkango QuerySet.

جزء واحد من السطور المهمة هو:

if value in EMPTY_VALUES:
    return qs

يتم إرجاع الاستعلام بدون تصفية إذا كانت لدينا قيمة فارغة وهي [], (), u'', {}, None
لكن قيمتنا هي 'None' لذا لا بأس ، كل الكود السابق لم يحول قيمة 'None' إلى None . لكن استخدام NullBooleanField بدون choices يحدث شيئًا مختلفًا تمامًا لأن الحقل لا يعتبر حقل 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 core db Queryset.
يمكننا ترجمته إلى:

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

ستعيد هذه العملية مجموعة استعلام فارغة ...
سينتج استعلام SQL مع is_solved = None

لكن إذا اختبرنا:

qs = qs.filter(is_solved__exact=None)

تقوم بإرجاع المثيلات بشكل صحيح فقط مع قيم null بالديسيبل.
سينتج استعلام SQL مع is_solved IS NULL
هذا هو الشيء نفسه الذي يجب اختباره بالضبط:

qs = qs.filter(is_solved__isnull=True)

ومع ذلك ، لا تحتوي قيم 'True' و 'False' على هذه المشكلة. النسخ صحيح بواسطة Django.

يمكننا التفكير في تجاوز 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

هذا يبدو وكأنه اختراق ولا يسعدني في الوقت الحالي ...
إذا كان أحدهم مهتمًا بالتفكير في هذه المسألة ، فسأكون سعيدًا لسماع ذلك! :د

في الوقت الحالي ، أي أفكار حول هذا التعديل؟
في الوقت الحالي ، أعتقد أنني سأستخدم IntegerField مع خيار خيارات لعدم الاضطرار إلى استخدام نسخة متشعبة من مرشحات django ... لأنني أعتقد أنها ليست مشكلة في البداية لفلاتر django :)

شكرا،

التعليق الأكثر فائدة

شكرا لك @ carltongibson وشكرا لك أيضا rpkilby
فقط سحبت فرع التنمية.

يعمل NullBooleanField الآن مع إطار عمل django rest مع هذا التكوين:
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 يرجى مراجعة ذلك. إذا كانت هناك حالة اختبار معينة تعتقد أنه يجب تغطيتها ، فهل يمكنك فتح بيان عام مضيفًا ذلك ويمكننا مراجعته.

شكرا.

شكرا لك @ carltongibson وشكرا لك أيضا rpkilby
فقط سحبت فرع التنمية.

يعمل NullBooleanField الآن مع إطار عمل django rest مع هذا التكوين:
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 التقييمات