diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f5bae7347..f859658a1 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -289,6 +289,10 @@ class BooleanField(Field): TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) + def __init__(self, **kwargs): + assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.' + super(BooleanField, self).__init__(**kwargs) + def to_internal_value(self, data): if data in self.TRUE_VALUES: return True @@ -297,7 +301,38 @@ class BooleanField(Field): self.fail('invalid', input=data) def to_representation(self, value): - if value is None: + if value in self.TRUE_VALUES: + return True + elif value in self.FALSE_VALUES: + return False + return bool(value) + + +class NullBooleanField(Field): + default_error_messages = { + 'invalid': _('`{input}` is not a valid boolean.') + } + default_empty_html = None + TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) + FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) + NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None)) + + def __init__(self, **kwargs): + assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.' + kwargs['allow_null'] = True + super(NullBooleanField, self).__init__(**kwargs) + + def to_internal_value(self, data): + if data in self.TRUE_VALUES: + return True + elif data in self.FALSE_VALUES: + return False + elif data in self.NULL_VALUES: + return None + self.fail('invalid', input=data) + + def to_representation(self, value): + if value in self.NULL_VALUES: return None if value in self.TRUE_VALUES: return True diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 949f59157..d8d72a4c6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -333,7 +333,7 @@ class ModelSerializer(Serializer): models.FloatField: FloatField, models.ImageField: ImageField, models.IntegerField: IntegerField, - models.NullBooleanField: BooleanField, + models.NullBooleanField: NullBooleanField, models.PositiveIntegerField: IntegerField, models.PositiveSmallIntegerField: IntegerField, models.SlugField: SlugField, diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 1c718ccba..c208afdc5 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -74,7 +74,7 @@ def get_field_kwargs(field_name, model_field): kwargs['choices'] = model_field.flatchoices return kwargs - if model_field.null: + if model_field.null and not isinstance(model_field, models.NullBooleanField): kwargs['allow_null'] = True if model_field.blank: diff --git a/tests/test_fields.py b/tests/test_fields.py index 6bf9aed4a..91f3f5db7 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -124,7 +124,8 @@ class TestBooleanField(FieldValues): False: False, } invalid_inputs = { - 'foo': ['`foo` is not a valid boolean.'] + 'foo': ['`foo` is not a valid boolean.'], + None: ['This field may not be null.'] } outputs = { 'true': True, @@ -140,6 +141,32 @@ class TestBooleanField(FieldValues): field = fields.BooleanField() +class TestNullBooleanField(FieldValues): + """ + Valid and invalid values for `BooleanField`. + """ + valid_inputs = { + 'true': True, + 'false': False, + 'null': None, + True: True, + False: False, + None: None + } + invalid_inputs = { + 'foo': ['`foo` is not a valid boolean.'], + } + outputs = { + 'true': True, + 'false': False, + 'null': None, + True: True, + False: False, + None: None + } + field = fields.NullBooleanField() + + # String types... class TestCharField(FieldValues): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 731ed2fbd..f74750247 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -90,7 +90,7 @@ class TestRegularFieldMappings(TestCase): email_field = EmailField(max_length=100) float_field = FloatField() integer_field = IntegerField() - null_boolean_field = BooleanField(allow_null=True) + null_boolean_field = NullBooleanField() positive_integer_field = IntegerField() positive_small_integer_field = IntegerField() slug_field = SlugField(max_length=100)