Skip to content

Commit

Permalink
(WIP) Fix html form rendering of BooleanFields with allow_null=True e…
Browse files Browse the repository at this point in the history
  • Loading branch information
tbrknt committed Mar 26, 2021
1 parent f83620d commit 73472d3
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 5 deletions.
12 changes: 12 additions & 0 deletions rest_framework/fields.py
Expand Up @@ -720,6 +720,10 @@ class BooleanField(Field):
}
NULL_VALUES = {'null', 'Null', 'NULL', '', None}

@property
def _is_nullable_boolean_field(self):
return self.allow_null

def to_internal_value(self, data):
try:
if data in self.TRUE_VALUES:
Expand All @@ -741,6 +745,14 @@ def to_representation(self, value):
return None
return bool(value)

def iter_options(self):
choices = {
"": _("Unknown"),
True: _("Yes"),
False: _("No"),
}
return iter_options(choices)


class NullBooleanField(BooleanField):
initial = None
Expand Down
7 changes: 5 additions & 2 deletions rest_framework/renderers.py
Expand Up @@ -328,8 +328,11 @@ class HTMLFormRenderer(BaseRenderer):
def render_field(self, field, parent_style):
if isinstance(field._field, serializers.HiddenField):
return ''

style = self.default_style[field].copy()
is_nullable_boolean_field = isinstance(field._field, serializers.BooleanField) and field._field.allow_null
if is_nullable_boolean_field:
style = {'base_template': 'select_boolean.html'}
else:
style = self.default_style[field].copy()
style.update(field.style)
if 'template_pack' not in style:
style['template_pack'] = parent_style.get('template_pack', self.template_pack)
Expand Down
@@ -0,0 +1,25 @@

<div class="form-group">
{% if field.label %}
<label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">
{{ field.label }}
</label>
{% endif %}

<div class="col-sm-10">
<select class="form-control" name="{{ field.name }}">
{% for select in field.iter_options %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endfor %}
</select>
{% if field.errors %}
{% for error in field.errors %}
<span class="help-block">{{ error }}</span>
{% endfor %}
{% endif %}

{% if field.help_text %}
<span class="help-block">{{ field.help_text|safe }}</span>
{% endif %}
</div>
</div>
15 changes: 15 additions & 0 deletions rest_framework/templates/rest_framework/inline/select_boolean.html
@@ -0,0 +1,15 @@
{% load rest_framework %}

<div class="form-group {% if field.errors %}has-error{% endif %}">
{% if field.label %}
<label class="sr-only">
{{ field.label }}
</label>
{% endif %}

<select class="form-control" name="{{ field.name }}">
{% for select in field.iter_options %}
<option value="{{ select.value }}" {% if select.value|as_string == field.value|as_string %}selected{% endif %} >{{ select.display_text }}</option>
{% endfor %}
</select>
</div>
@@ -0,0 +1,23 @@
<div class="form-group {% if field.errors %}has-error{% endif %}">
{% if field.label %}
<label {% if style.hide_label %}class="sr-only"{% endif %}>
{{ field.label }}
</label>
{% endif %}

<select class="form-control" name="{{ field.name }}">
{% for select in field.iter_options %}
<option value="{{ select.value }}" {% if select.value == field.value %}selected{% endif %}>{{ select.display_text }}</option>
{% endfor %}
</select>

{% if field.errors %}
{% for error in field.errors %}
<span class="help-block">{{ error }}</span>
{% endfor %}
{% endif %}

{% if field.help_text %}
<span class="help-block">{{ field.help_text|safe }}</span>
{% endif %}
</div>
7 changes: 6 additions & 1 deletion rest_framework/utils/serializer_helpers.py
Expand Up @@ -76,7 +76,10 @@ def __repr__(self):
)

def as_form_field(self):
value = '' if (self.value is None or self.value is False) else self.value
if getattr(self._field, '_is_nullable_boolean_field', False):
value = '' if self.value is None else self.value
else:
value = '' if (self.value is None or self.value is False) else self.value
return self.__class__(self._field, value, self.errors, self._prefix)


Expand Down Expand Up @@ -129,6 +132,8 @@ def as_form_field(self):
for key, value in self.value.items():
if isinstance(value, (list, dict)):
values[key] = value
elif getattr(self.fields[key], '_is_nullable_boolean_field', False):
values[key] = '' if value is None else value
else:
values[key] = '' if (value is None or value is False) else force_str(value)
return self.__class__(self._field, values, self.errors, self._prefix)
Expand Down
59 changes: 57 additions & 2 deletions tests/test_bound_fields.py
@@ -1,3 +1,4 @@
import pytest
from django.http import QueryDict

from rest_framework import serializers
Expand Down Expand Up @@ -59,11 +60,13 @@ class ExampleSerializer(serializers.Serializer):
def test_as_form_fields(self):
class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField()
nullable_bool_field = serializers.BooleanField(allow_null=True)
null_field = serializers.IntegerField(allow_null=True)

serializer = ExampleSerializer(data={'bool_field': False, 'null_field': None})
serializer = ExampleSerializer(data={'bool_field': False, 'nullable_bool_field': False, 'null_field': None})
assert serializer.is_valid()
assert serializer['bool_field'].as_form_field().value == ''
assert serializer['nullable_bool_field'].as_form_field().value is False
assert serializer['null_field'].as_form_field().value == ''

def test_rendering_boolean_field(self):
Expand All @@ -90,6 +93,55 @@ class ExampleSerializer(serializers.Serializer):
rendered_packed = ''.join(rendered.split())
assert rendered_packed == expected_packed

@pytest.mark.parametrize('bool_field_value', [True, False, None])
def test_rendering_nullable_boolean_field(self, bool_field_value):
from rest_framework.renderers import HTMLFormRenderer

class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField(
allow_null=True,
style={'base_template': 'select_boolean.html', 'template_pack': 'rest_framework/vertical'})

serializer = ExampleSerializer(data={'bool_field': bool_field_value})
assert serializer.is_valid()
renderer = HTMLFormRenderer()
rendered = renderer.render_field(serializer['bool_field'], {})
if bool_field_value is True:
expected_packed = (
'<divclass="form-group">'
'<label>Boolfield</label>'
'<selectclass="form-control"name="bool_field">'
'<optionvalue="">Unknown</option>'
'<optionvalue="True"selected>Yes</option>'
'<optionvalue="False">No</option>'
'</select>'
'</div>'
)
elif bool_field_value is False:
expected_packed = (
'<divclass="form-group">'
'<label>Boolfield</label>'
'<selectclass="form-control"name="bool_field">'
'<optionvalue="">Unknown</option>'
'<optionvalue="True">Yes</option>'
'<optionvalue="False"selected>No</option>'
'</select>'
'</div>'
)
elif bool_field_value is None:
expected_packed = (
'<divclass="form-group">'
'<label>Boolfield</label>'
'<selectclass="form-control"name="bool_field">'
'<optionvalue=""selected>Unknown</option>'
'<optionvalue="True">Yes</option>'
'<optionvalue="False">No</option>'
'</select>'
'</div>'
)
rendered_packed = ''.join(rendered.split())
assert rendered_packed == expected_packed


class CustomJSONField(serializers.JSONField):
pass
Expand Down Expand Up @@ -120,6 +172,7 @@ class ExampleSerializer(serializers.Serializer):
def test_as_form_fields(self):
class Nested(serializers.Serializer):
bool_field = serializers.BooleanField()
nullable_bool_field = serializers.BooleanField(allow_null=True)
null_field = serializers.IntegerField(allow_null=True)
json_field = serializers.JSONField()
custom_json_field = CustomJSONField()
Expand All @@ -129,12 +182,14 @@ class ExampleSerializer(serializers.Serializer):

serializer = ExampleSerializer(
data={'nested': {
'bool_field': False, 'null_field': None,
'bool_field': False, 'nullable_bool_field': False, 'null_field': None,
'json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'},
'custom_json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'},
}})
assert serializer.is_valid()
assert serializer['nested'].as_form_field().value == {'bool_field': '', 'nullable_bool_field': False, 'null_field': '', 'json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'}, 'custom_json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'}}
assert serializer['nested']['bool_field'].as_form_field().value == ''
assert serializer['nested']['nullable_bool_field'].as_form_field().value is False
assert serializer['nested']['null_field'].as_form_field().value == ''
assert serializer['nested']['json_field'].as_form_field().value == '''{
"bool_item": true,
Expand Down

0 comments on commit 73472d3

Please sign in to comment.