#22406: Inconsistent way to parse boolean value from posted data
--------------------------+--------------------------------------
     Reporter:  xiaowl@…  |                    Owner:  nobody
         Type:  Bug       |                   Status:  closed
    Component:  Forms     |                  Version:  1.6
     Severity:  Normal    |               Resolution:  needsinfo
     Keywords:            |             Triage Stage:  Unreviewed
    Has patch:  1         |      Needs documentation:  0
  Needs tests:  0         |  Patch needs improvement:  0
Easy pickings:  0         |                    UI/UX:  0
--------------------------+--------------------------------------

Comment (by Christian Kreuzberger):

 A similar inconsistency still exists with BooleanFields and
 NullBooleanFields in django.forms:


 django.forms.fields.BooleanField has the following logic:
 {{{
 #!python

 class BooleanField(Field):
     widget = CheckboxInput

     def to_python(self, value):
         """Returns a Python boolean object."""
         # Explicitly check for the string 'False', which is what a hidden
 field
         # will submit for False. Also check for '0', since this is what
         # RadioSelect will provide. Because bool("True") == bool('1') ==
 True,
         # we don't need to handle that explicitly.
         if isinstance(value, six.string_types) and value.lower() in
 ('false', '0'):
             value = False
         else:
             value = bool(value)
         return super(BooleanField, self).to_python(value)
 }}}

 So 'false', 'False',false,'0' will be evaluated to False,
 'true','True',true,'1' will be evaluated to True. I guess '2' and '3' will
 not be converted and will be evaluated as None at this point.

 django.forms.fields.NullBooleanField has a different implementation:
 {{{
 #!python
 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
 }}}


 Essentially, the to_python method will do the same as BooleanFields
 to_python method, but now comes the funny part:

 NullBooleanField uses the NullBooleanSelect widget, while BooleanField
 uses the CheckboxInput widget:

 BooleanField uses django.forms.widget.CheckboxInput which evaluates
 'true','True',True,'1' to True, and 'false','False',False,'0' to False -
 which is fine.

 {{{
 #!python

 class CheckboxInput(Input):
     def value_from_datadict(self, data, files, name):
         if name not in data:
             # A missing value means False because HTML form submission
 does not
             # send results for unselected checkboxes.
             return False
         value = data.get(name)
         # Translate true and false strings to boolean values.
         values = {'true': True, 'false': False}
         if isinstance(value, six.string_types):
             value = values.get(value.lower(), value)
         return bool(value)
 }}}


 But NullBooleanField uses NullBooleanSelect, which as stated above, does
 NOT evaluate 'false' to False, and not 'true' to True. Instead, it
 evalutes '2' and 'True' to True, '3' and 'False' to False...

 {{{
 #!python
 class NullBooleanSelect(Select):
     """
     A Select Widget intended to be used with NullBooleanField.
     """
     def __init__(self, attrs=None):
         choices = (
             ('1', ugettext_lazy('Unknown')),
             ('2', ugettext_lazy('Yes')),
             ('3', ugettext_lazy('No')),
         )
         super(NullBooleanSelect, self).__init__(attrs, choices)

     def format_value(self, value):
         try:
             return {True: '2', False: '3', '2': '2', '3': '3'}[value]
         except KeyError:
             return '1'

     def value_from_datadict(self, data, files, name):
         value = data.get(name)
         return {
             '2': True,
             True: True,
             'True': True,
             '3': False,
             'False': False,
             False: False,
         }.get(value)
 }}}


 Because of this inconsistency, we are experiencing a very funny behaviour
 for NullBooleanField:

 django.forms.forms.BaseForm has the _clean_fields method, which calls the
 widgets `value_from_datadict` method:
 `value = field.widget.value_from_datadict(self.data, self.files,
 self.add_prefix(name))`
 This is done for each field, and it is happening BEFORE the `to_python`
 method of the field is called.


 Essentially, this whole inconsistency leads to a funny behaviour:
 If you use a BooleanField the following HTTP request will be evaluated
 correctly by the form:
 /my/site/?show_only_assigned_tasks=true
 But if you use a NullBooleanField instead of a BooleanField, the above
 request will not work, and the String 'true' will be evaluated as None.


 TL;DR: NullBooleanSelect should be extended with a way to allow 'false'
 and 'true' to be evaluated:

 {{{
 #!python

     def value_from_datadict(self, data, files, name):
         value = data.get(name)
         return {
             '2': True,
             True: True,
             'True': True,
             'true': True,
             '3': False,
             'False': False,
             'false': False,
             False: False,
         }.get(value)
 }}}

-- 
Ticket URL: <https://code.djangoproject.com/ticket/22406#comment:3>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/075.a805b62bc2cca91074599793b7fb9571%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to