Skip to content

Commit

Permalink
Merge NullBooleanField with BooleanField(allow_null=True) (encode#7122)
Browse files Browse the repository at this point in the history
* Make `NullBooleanField` subclass `BooleanField`

This removes a lot of the redundancy that was in place becuase we
were not doing this. This maintains the `None` initial value that
was previously present, as well as disallowing `allow_null` to be
passed in.

* Remove special case for mapping `NullBooleanField`

In newer versions of Django, the `NullBooleanField` is handled the
same way as a `BooleanField(null=True)`. Given that we also support
that combination, and that our own `NullBooleanField` behaves in the
same manner, it makes sense to remove the special casing that exists
for it.

* Add test for BooleanField(null=True, choices)

* Remove special case for NullBooleanField

* Adjust mapping tests for NullBooleanField

* Fixed linting error

* Raise deprecation warning when NullBooleanField is used

* Fix linting issue in imports
  • Loading branch information
kevin-brown authored May 13, 2020
1 parent 089162e commit e888fc1
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 46 deletions.
4 changes: 4 additions & 0 deletions rest_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ class RemovedInDRF313Warning(DeprecationWarning):

class RemovedInDRF314Warning(PendingDeprecationWarning):
pass


class RemovedInDRF314Warning(PendingDeprecationWarning):
pass
55 changes: 12 additions & 43 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
from django.utils.translation import gettext_lazy as _
from pytz.exceptions import InvalidTimeError

from rest_framework import ISO_8601, RemovedInDRF313Warning
from rest_framework import (
ISO_8601, RemovedInDRF313Warning, RemovedInDRF314Warning
)
from rest_framework.compat import ProhibitNullCharactersValidator
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings
Expand Down Expand Up @@ -740,54 +742,21 @@ def to_representation(self, value):
return bool(value)


class NullBooleanField(Field):
default_error_messages = {
'invalid': _('Must be a valid boolean.')
}
class NullBooleanField(BooleanField):
initial = None
TRUE_VALUES = {
't', 'T',
'y', 'Y', 'yes', 'YES',
'true', 'True', 'TRUE',
'on', 'On', 'ON',
'1', 1,
True
}
FALSE_VALUES = {
'f', 'F',
'n', 'N', 'no', 'NO',
'false', 'False', 'FALSE',
'off', 'Off', 'OFF',
'0', 0, 0.0,
False
}
NULL_VALUES = {'null', 'Null', 'NULL', '', None}

def __init__(self, **kwargs):
warnings.warn(
"The `NullBooleanField` is deprecated and will be removed starting "
"with 3.14. Instead use the `BooleanField` field and set "
"`null=True` which does the same thing.",
RemovedInDRF314Warning, stacklevel=2
)

assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
kwargs['allow_null'] = True
super().__init__(**kwargs)

def to_internal_value(self, data):
try:
if data in self.TRUE_VALUES:
return True
elif data in self.FALSE_VALUES:
return False
elif data in self.NULL_VALUES:
return None
except TypeError: # Input is an unhashable type
pass
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
elif value in self.FALSE_VALUES:
return False
return bool(value)
super().__init__(**kwargs)


# String types...
Expand Down
2 changes: 1 addition & 1 deletion rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ class ModelSerializer(Serializer):
models.FloatField: FloatField,
models.ImageField: ImageField,
models.IntegerField: IntegerField,
models.NullBooleanField: NullBooleanField,
models.NullBooleanField: BooleanField,
models.PositiveIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
models.SlugField: SlugField,
Expand Down
2 changes: 1 addition & 1 deletion rest_framework/utils/field_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def get_field_kwargs(field_name, model_field):
if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False

if model_field.null and not isinstance(model_field, models.NullBooleanField):
if model_field.null:
kwargs['allow_null'] = True

if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
Expand Down
23 changes: 22 additions & 1 deletion tests/test_model_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class Meta:
email_field = EmailField(max_length=100)
float_field = FloatField()
integer_field = IntegerField()
null_boolean_field = NullBooleanField(required=False)
null_boolean_field = BooleanField(allow_null=True, required=False)
positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField()
slug_field = SlugField(allow_unicode=False, max_length=100)
Expand Down Expand Up @@ -236,6 +236,27 @@ class Meta:

self.assertEqual(repr(NullableBooleanSerializer()), expected)

def test_nullable_boolean_field_choices(self):
class NullableBooleanChoicesModel(models.Model):
CHECKLIST_OPTIONS = (
(None, 'Unknown'),
(True, 'Yes'),
(False, 'No'),
)

field = models.BooleanField(null=True, choices=CHECKLIST_OPTIONS)

class NullableBooleanChoicesSerializer(serializers.ModelSerializer):
class Meta:
model = NullableBooleanChoicesModel
fields = ['field']

serializer = NullableBooleanChoicesSerializer(data=dict(
field=None,
))
self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.errors, {})

def test_method_field(self):
"""
Properties and methods on the model should be allowed as `Meta.fields`
Expand Down

0 comments on commit e888fc1

Please sign in to comment.