Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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,27 @@
<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 == field.value %}
<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 == field.value %}
<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,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 %} {% if select.value == field.value %}
<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
89 changes: 88 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,88 @@ 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


class CustomJSONField(serializers.JSONField):
pass
Expand Down Expand Up @@ -124,6 +208,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 +218,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 +233,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