Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/topics/html-and-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ select.html | `ChoiceField` or relational field types
radio.html | `ChoiceField` or relational field types | inline, hide_label
select_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | hide_label
checkbox_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | inline, hide_label
checkbox.html | `BooleanField` | hide_label
checkbox.html | `BooleanField` with `allow_null=False` | hide_label
select_boolean.html | `BooleanField` with `allow_null=True` | hide_label
fieldset.html | Nested serializer | hide_label
list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label
8 changes: 8 additions & 0 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,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)


# String types...

Expand Down
2 changes: 2 additions & 0 deletions rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@ def render_field(self, field, parent_style):
return ''

style = self.default_style[field].copy()
if isinstance(field._field, serializers.BooleanField) and field._field.allow_null:
style['base_template'] = 'select_boolean.html'
style.update(field.style)
if 'template_pack' not in style:
style['template_pack'] = parent_style.get('template_pack', self.template_pack)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% load rest_framework %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
{% 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 %} {% if select.value|as_string == field.value|as_string %}
<option value="{{ select.value }}" selected>
{{ select.display_text }}
</option>
{% else %}
<option value="{{ select.value }}">{{ select.display_text }}</option>
{% endif %} {% 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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% load rest_framework %}
<label class="sr-only">{{ field.label }}</label>
<select class="form-control" name="{{ field.name }}">
{% for select in field.iter_options %} {% if select.value|as_string == field.value|as_string %}
<option value="{{ select.value }}" selected>{{ select.display_text }}</option>
{% else %}
<option value="{{ select.value }}">{{ select.display_text }}</option>
{% endif %} {% endfor %}
</select>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% load rest_framework %}
<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 %} {% if select.value|as_string == field.value|as_string %}
<option value="{{ select.value }}" selected>
{{ select.display_text }}
</option>
{% else %}
<option value="{{ select.value }}">{{ select.display_text }}</option>
{% endif %} {% 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
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ def __repr__(self):
)

def as_form_field(self):
value = '' if (self.value is None or self.value is False) else self.value
if self._field.allow_null:
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 @@ -142,6 +145,8 @@ def as_form_field(self):
for key, value in self.value.items():
if isinstance(value, (list, dict)):
values[key] = value
elif self.fields[key].allow_null:
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
113 changes: 112 additions & 1 deletion tests/test_bound_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ def test_as_form_fields(self):
class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField()
null_field = serializers.IntegerField(allow_null=True)
nullable_bool_field = serializers.BooleanField(allow_null=True)

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

def test_rendering_boolean_field(self):
from rest_framework.renderers import HTMLFormRenderer
Expand All @@ -91,6 +93,112 @@ class ExampleSerializer(serializers.Serializer):
rendered_packed = ''.join(rendered.split())
assert rendered_packed == expected_packed

def test_rendering_nullable_boolean_field(self):
from rest_framework.renderers import HTMLFormRenderer

class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField(allow_null=True)

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

def test_rendering_nullable_boolean_field_with_explicit_style(self):
from rest_framework.renderers import HTMLFormRenderer

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

serializer = ExampleSerializer(data={'bool_field': True})
assert serializer.is_valid()
renderer = HTMLFormRenderer()
rendered = renderer.render_field(serializer['bool_field'], {})
expected_packed = (
'<divclass="form-group">'
'<divclass="checkbox">'
'<label>'
'<inputtype="checkbox"name="bool_field"value="true"checked>'
'Boolfield'
'</label>'
'</div>'
'</div>'
)
rendered_packed = ''.join(rendered.split())
assert rendered_packed == expected_packed

def test_rendering_nullable_boolean_field_template_packs(self):
from rest_framework.renderers import HTMLFormRenderer

class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField(allow_null=True)

cases = [
(True, '<optionvalue="True"selected>Yes</option>'),
(False, '<optionvalue="False"selected>No</option>'),
(None, '<optionvalue=""selected>Unknown</option>'),
]
for template_pack in ('rest_framework/vertical', 'rest_framework/horizontal', 'rest_framework/inline'):
renderer = HTMLFormRenderer()
renderer.template_pack = template_pack
for value, selected_option in cases:
serializer = ExampleSerializer(data={'bool_field': value})
assert serializer.is_valid()
rendered = renderer.render_field(serializer['bool_field'], {})
rendered_packed = ''.join(rendered.split())
assert selected_option in rendered_packed

def test_rendering_nullable_boolean_field_on_redisplay(self):
from rest_framework.renderers import HTMLFormRenderer

class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField(allow_null=True)
other = serializers.CharField()

# After a failed validation the bound value is the raw submitted
# string, not a bool; the submitted option must stay selected.
cases = [
('True', '<optionvalue="True"selected>Yes</option>'),
('False', '<optionvalue="False"selected>No</option>'),
('', '<optionvalue=""selected>Unknown</option>'),
]
for template_pack in ('rest_framework/vertical', 'rest_framework/horizontal', 'rest_framework/inline'):
renderer = HTMLFormRenderer()
renderer.template_pack = template_pack
for raw_value, selected_option in cases:
serializer = ExampleSerializer(data={'bool_field': raw_value})
assert not serializer.is_valid()
rendered = renderer.render_field(serializer['bool_field'], {})
rendered_packed = ''.join(rendered.split())
assert selected_option in rendered_packed


class CustomJSONField(serializers.JSONField):
pass
Expand Down Expand Up @@ -124,6 +232,7 @@ class Nested(serializers.Serializer):
null_field = serializers.IntegerField(allow_null=True)
json_field = serializers.JSONField()
custom_json_field = CustomJSONField()
nullable_bool_field = serializers.BooleanField(allow_null=True)

class ExampleSerializer(serializers.Serializer):
nested = Nested()
Expand All @@ -133,6 +242,7 @@ class ExampleSerializer(serializers.Serializer):
'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'},
'nullable_bool_field': False,
}})
assert serializer.is_valid()
assert serializer['nested']['bool_field'].as_form_field().value == ''
Expand All @@ -147,6 +257,7 @@ class ExampleSerializer(serializers.Serializer):
"number": 1,
"text_item": "text"
}'''
assert serializer['nested'].as_form_field().value['nullable_bool_field'] is False

def test_rendering_nested_fields_with_none_value(self):
from rest_framework.renderers import HTMLFormRenderer
Expand Down
18 changes: 18 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,24 @@ class TestNullableBooleanField(TestBooleanField):
}
field = serializers.BooleanField(allow_null=True)

@pytest.mark.parametrize(
'input_value,output_value',
[
('', None),
('True', True),
('False', False),
]
)
def test_nullable_boolean_html(self, input_value, output_value):
class TestSerializer(serializers.Serializer):
bool_field = serializers.BooleanField(allow_null=True)

data = QueryDict(mutable=True)
data.update({'bool_field': input_value})
serializer = TestSerializer(data=data)
assert serializer.is_valid()
assert serializer.validated_data['bool_field'] is output_value


# String types...

Expand Down
Loading