Opened 10 months ago

Closed 10 months ago

Last modified 10 months ago

#35126 closed Bug (invalid)

forms.NullBooleanField's validation logic is surprising

Reported by: Jeremy Lainé Owned by: Gaurav sah
Component: Forms Version: 5.0
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Jeremy Lainé)

Reading NullBooleanField's code lead me to believe that it would clean "1" to True, and "0" to False", just like BooleanField does:

https://github.com/django/django/blob/10c7c7320baf1c655fcb91202169d77725c9c4bd/django/forms/fields.py#L850

A simple field-level test works:

>>> from django import forms
>>> field = forms.NullBooleanField()
>>> field.clean("1")
True
>>> field.clean("0")
False

But using this in an actual form fails, note the difference between the BooleanField and NullBooleanField:

>>> class DemoForm(forms.Form):
...    field_a = forms.BooleanField()
...    field_b = forms.NullBooleanField()
...
>>> form = DemoForm({"field_a": "1", "field_b": "1"})
>>> form.is_valid()
>>> form.cleaned_data
{'field_a': True, 'field_b': None}

The problem is that by default NullBooleanField uses a NullBooleanSelect which mangles the submitted data for some obscure backwards-compatibility reason:

https://github.com/django/django/blob/10c7c7320baf1c655fcb91202169d77725c9c4bd/django/forms/widgets.py#L816

Change History (6)

comment:1 by Jeremy Lainé, 10 months ago

Description: modified (diff)

comment:2 by Gaurav sah, 10 months ago

Owner: changed from nobody to Gaurav sah
Status: newassigned

comment:3 by Mariusz Felisiak, 10 months ago

Resolution: invalid
Status: assignedclosed

It's clearly documented with what kind of widgets (and set of choices) you can use NullBooleanField():

"NullBooleanField may be used with widgets such as Select or RadioSelect by providing the widget choices:"

I don't see anything to fix here.

comment:4 by Jeremy Lainé, 10 months ago

I'm a bit surprised by this answer, it seems somewhat besides the point. I'm not arguing you cannot use other widgets, I'm saying the default widget alters the validation behaviour in an unexpected fashion.

I would expect NullBooleanField to behave exactly like BooleanField for values which normalise to True / False, and validate other values to None. This is exactly what NullBooleanField.to_python expresses too. However this is not the case out of the box: "0" and "1" get validated to None because of what looks like an oversight in NullBooleanWidget.

This was especially surprising in my use case where I don't even use widgets at all, and purely use forms to validate data.

comment:5 by David Sanders, 10 months ago

Hi Jeremy,

If you'd like, it'd be better to start a discussion either on Discord or on the Django forum as only a limited number of people will see your points raised. Mariusz is quite busy and may be unlikely to respond :) Additionally someone may be able to explain the reasons why NullBooleanSelect diverges in that way.

comment:6 by Tim Graham, 10 months ago

It seems like a useful improvement, if feasible. Jeremy, it would have been most helpful if you did some investigation regarding the backwards compatibility comment rather than simply describing it as "some obscure reason." Using git blame leads to 35a08b8541c856a51b2ab718e0a2fe060debfa2a (#17210). If you're able to provide a proof of concept solution, feel free to ping me (@timgraham) on a draft GitHub pull request.

Note: See TracTickets for help on using tickets.
Back to Top