Merge NullBooleanField with BooleanField(allow_null=True) (#7122)

* 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
This commit is contained in:
Kevin Brown 2020-05-13 09:59:04 -04:00 committed by GitHub
parent 089162e6e3
commit e888fc11c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 40 additions and 46 deletions

View File

@ -31,3 +31,7 @@ class RemovedInDRF313Warning(DeprecationWarning):
class RemovedInDRF314Warning(PendingDeprecationWarning):
pass
class RemovedInDRF314Warning(PendingDeprecationWarning):
pass

View File

@ -30,7 +30,9 @@ from django.utils.timezone import utc
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
@ -740,55 +742,22 @@ class BooleanField(Field):
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)
# String types...

View File

@ -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,

View File

@ -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))):

View File

@ -182,7 +182,7 @@ class TestRegularFieldMappings(TestCase):
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)
@ -236,6 +236,27 @@ class TestRegularFieldMappings(TestCase):
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`