summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Graham <timograham@gmail.com>2017-05-06 10:56:28 -0400
committerTim Graham <timograham@gmail.com>2018-03-20 12:10:10 -0400
commit5fa4f40f45fcdbb7e48489ed3039a314b5c961d0 (patch)
tree272b8798d2c2a054f56d8613a42453bce30f92c0
parent73f7d1755ff1da3aac687c7b046e4b5028e505db (diff)
Fixed #29227 -- Allowed BooleanField to be null=True.
Thanks Lynn Cyrin for contributing to the patch, and Nick Pope for review.
-rw-r--r--django/contrib/admin/filters.py2
-rw-r--r--django/contrib/admin/utils.py4
-rw-r--r--django/db/backends/oracle/utils.py1
-rw-r--r--django/db/models/fields/__init__.py44
-rw-r--r--docs/ref/checks.txt3
-rw-r--r--docs/ref/contrib/admin/index.txt5
-rw-r--r--docs/ref/models/fields.txt21
-rw-r--r--docs/releases/2.1.txt4
-rw-r--r--docs/topics/forms/modelforms.txt4
-rw-r--r--tests/admin_filters/models.py3
-rw-r--r--tests/admin_filters/tests.py56
-rw-r--r--tests/admin_utils/tests.py4
-rw-r--r--tests/admin_views/models.py4
-rw-r--r--tests/annotations/tests.py4
-rw-r--r--tests/backends/oracle/tests.py2
-rw-r--r--tests/bulk_create/models.py3
-rw-r--r--tests/datatypes/models.py3
-rw-r--r--tests/datatypes/tests.py4
-rw-r--r--tests/expressions_case/models.py3
-rw-r--r--tests/expressions_case/tests.py13
-rw-r--r--tests/generic_relations_regress/models.py2
-rw-r--r--tests/inspectdb/models.py2
-rw-r--r--tests/invalid_models_tests/test_ordinary_fields.py18
-rw-r--r--tests/managers_regress/models.py2
-rw-r--r--tests/model_fields/models.py3
-rw-r--r--tests/model_fields/test_booleanfield.py23
-rw-r--r--tests/postgres_tests/migrations/0002_create_test_models.py2
-rw-r--r--tests/postgres_tests/models.py2
-rw-r--r--tests/queries/models.py2
-rw-r--r--tests/serializers/models/data.py6
-rw-r--r--tests/serializers/test_data.py3
-rw-r--r--tests/validation/test_error_messages.py4
32 files changed, 158 insertions, 98 deletions
diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
index d21abca244..633a26b67a 100644
--- a/django/contrib/admin/filters.py
+++ b/django/contrib/admin/filters.py
@@ -243,7 +243,7 @@ class BooleanFieldListFilter(FieldListFilter):
'query_string': changelist.get_query_string({self.lookup_kwarg: lookup}, [self.lookup_kwarg2]),
'display': title,
}
- if isinstance(self.field, models.NullBooleanField):
+ if self.field.null:
yield {
'selected': self.lookup_val2 == 'True',
'query_string': changelist.get_query_string({self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]),
diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
index b8df69c1d8..73bcd8bcbb 100644
--- a/django/contrib/admin/utils.py
+++ b/django/contrib/admin/utils.py
@@ -394,8 +394,8 @@ def display_for_field(value, field, empty_value_display):
if getattr(field, 'flatchoices', None):
return dict(field.flatchoices).get(value, empty_value_display)
- # NullBooleanField needs special-case null-handling, so it comes
- # before the general null test.
+ # BooleanField needs special-case null-handling, so it comes before the
+ # general null test.
elif isinstance(field, (models.BooleanField, models.NullBooleanField)):
return _boolean_icon(value)
elif value is None:
diff --git a/django/db/backends/oracle/utils.py b/django/db/backends/oracle/utils.py
index 6c81d5cd7b..3b26112071 100644
--- a/django/db/backends/oracle/utils.py
+++ b/django/db/backends/oracle/utils.py
@@ -41,6 +41,7 @@ class BulkInsertMapper:
types = {
'BigIntegerField': NUMBER,
'BinaryField': BLOB,
+ 'BooleanField': NUMBER,
'DateField': DATE,
'DateTimeField': TIMESTAMP,
'DecimalField': NUMBER,
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index ea31096374..59cc963b18 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -988,41 +988,16 @@ class BooleanField(Field):
empty_strings_allowed = False
default_error_messages = {
'invalid': _("'%(value)s' value must be either True or False."),
+ 'invalid_nullable': _("'%(value)s' value must be either True, False, or None."),
}
description = _("Boolean (Either True or False)")
- def __init__(self, *args, **kwargs):
- kwargs['blank'] = True
- super().__init__(*args, **kwargs)
-
- def check(self, **kwargs):
- return [
- *super().check(**kwargs),
- *self._check_null(**kwargs),
- ]
-
- def _check_null(self, **kwargs):
- if getattr(self, 'null', False):
- return [
- checks.Error(
- 'BooleanFields do not accept null values.',
- hint='Use a NullBooleanField instead.',
- obj=self,
- id='fields.E110',
- )
- ]
- else:
- return []
-
- def deconstruct(self):
- name, path, args, kwargs = super().deconstruct()
- del kwargs['blank']
- return name, path, args, kwargs
-
def get_internal_type(self):
return "BooleanField"
def to_python(self, value):
+ if self.null and value in self.empty_values:
+ return None
if value in (True, False):
# if value is 1 or 0 than it's equal to True or False, but we want
# to return a true bool for semantic reasons.
@@ -1032,7 +1007,7 @@ class BooleanField(Field):
if value in ('f', 'False', '0'):
return False
raise exceptions.ValidationError(
- self.error_messages['invalid'],
+ self.error_messages['invalid_nullable' if self.null else 'invalid'],
code='invalid',
params={'value': value},
)
@@ -1044,15 +1019,16 @@ class BooleanField(Field):
return self.to_python(value)
def formfield(self, **kwargs):
- # Unlike most fields, BooleanField figures out include_blank from
- # self.null instead of self.blank.
if self.choices:
include_blank = not (self.has_default() or 'initial' in kwargs)
defaults = {'choices': self.get_choices(include_blank=include_blank)}
else:
- defaults = {'form_class': forms.BooleanField}
- defaults.update(kwargs)
- return super().formfield(**defaults)
+ form_class = forms.NullBooleanField if self.null else forms.BooleanField
+ # In HTML checkboxes, 'required' means "must be checked" which is
+ # different from the choices case ("must select some value").
+ # required=False allows unchecked checkboxes.
+ defaults = {'form_class': form_class, 'required': False}
+ return super().formfield(**{**defaults, **kwargs})
class CharField(Field):
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index 6310500d0c..1b9fa18328 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -138,7 +138,8 @@ Model fields
* **fields.E007**: Primary keys must not have ``null=True``.
* **fields.E008**: All ``validators`` must be callable.
* **fields.E100**: ``AutoField``\s must set primary_key=True.
-* **fields.E110**: ``BooleanField``\s do not accept null values.
+* **fields.E110**: ``BooleanField``\s do not accept null values. *This check
+ appeared before support for null values was added in Django 2.1.*
* **fields.E120**: ``CharField``\s must define a ``max_length`` attribute.
* **fields.E121**: ``max_length`` must be a positive integer.
* **fields.W122**: ``max_length`` is ignored when used with ``IntegerField``.
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index face1297ac..cc0cf87c8a 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -606,9 +606,8 @@ subclass::
and add that method's name to ``list_display``. (See below for more
on custom methods in ``list_display``.)
- * If the field is a ``BooleanField`` or ``NullBooleanField``, Django
- will display a pretty "on" or "off" icon instead of ``True`` or
- ``False``.
+ * If the field is a ``BooleanField``, Django will display a pretty "on" or
+ "off" icon instead of ``True`` or ``False``.
* If the string given is a method of the model, ``ModelAdmin`` or a
callable, Django will HTML-escape the output by default. To escape
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index afd0d6ff0c..adb12aac63 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -61,9 +61,6 @@ set ``blank=True`` if you wish to permit empty values in forms, as the
When using the Oracle database backend, the value ``NULL`` will be stored to
denote the empty string regardless of this attribute.
-If you want to accept :attr:`~Field.null` values with :class:`BooleanField`,
-use :class:`NullBooleanField` instead.
-
``blank``
---------
@@ -442,15 +439,18 @@ case it can't be included in a :class:`~django.forms.ModelForm`.
A true/false field.
-The default form widget for this field is a
-:class:`~django.forms.CheckboxInput`.
-
-If you need to accept :attr:`~Field.null` values then use
-:class:`NullBooleanField` instead.
+The default form widget for this field is :class:`~django.forms.CheckboxInput`,
+or :class:`~django.forms.NullBooleanSelect` if :attr:`null=True <Field.null>`.
The default value of ``BooleanField`` is ``None`` when :attr:`Field.default`
isn't defined.
+.. versionchanged:: 2.1
+
+ In older versions, this field doesn't permit ``null=True``, so you have to
+ use :class:`NullBooleanField` instead. Using the latter is now discouraged
+ as it's likely to be deprecated in a future version of Django.
+
``CharField``
-------------
@@ -1008,9 +1008,8 @@ values are stored as null.
.. class:: NullBooleanField(**options)
-Like a :class:`BooleanField`, but allows ``NULL`` as one of the options. Use
-this instead of a :class:`BooleanField` with ``null=True``. The default form
-widget for this field is a :class:`~django.forms.NullBooleanSelect`.
+Like :class:`BooleanField` with ``null=True``. Use that instead of this field
+as it's likely to be deprecated in a future version of Django.
``PositiveIntegerField``
------------------------
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 4d8a9a1d42..ec2a3266a1 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -223,6 +223,10 @@ Models
* :meth:`.QuerySet.order_by` and :meth:`distinct(*fields) <.QuerySet.distinct>`
now support using field transforms.
+* :class:`~django.db.models.BooleanField` can now be ``null=True``. This is
+ encouraged instead of :class:`~django.db.models.NullBooleanField`, which will
+ likely be deprecated in the future.
+
Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index 553271e598..ec7c7a0587 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -67,7 +67,9 @@ Model field Form field
``True`` on the model field, otherwise not
represented in the form.
-:class:`BooleanField` :class:`~django.forms.BooleanField`
+:class:`BooleanField` :class:`~django.forms.BooleanField`, or
+ :class:`~django.forms.NullBooleanField` if
+ ``null=True``.
:class:`CharField` :class:`~django.forms.CharField` with
``max_length`` set to the model field's
diff --git a/tests/admin_filters/models.py b/tests/admin_filters/models.py
index d9ce5c7cfd..ae78282d34 100644
--- a/tests/admin_filters/models.py
+++ b/tests/admin_filters/models.py
@@ -28,7 +28,8 @@ class Book(models.Model):
verbose_name='Employee',
blank=True, null=True,
)
- is_best_seller = models.NullBooleanField(default=0)
+ is_best_seller = models.BooleanField(default=0, null=True)
+ is_best_seller2 = models.NullBooleanField(default=0)
date_registered = models.DateField(null=True)
# This field name is intentionally 2 characters long (#16080).
no = models.IntegerField(verbose_name='number', blank=True, null=True)
diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py
index 200c8c8db1..554b1a4e23 100644
--- a/tests/admin_filters/tests.py
+++ b/tests/admin_filters/tests.py
@@ -142,6 +142,10 @@ class BookAdmin(ModelAdmin):
ordering = ('-id',)
+class BookAdmin2(ModelAdmin):
+ list_filter = ('year', 'author', 'contributors', 'is_best_seller2', 'date_registered', 'no')
+
+
class BookAdminWithTupleBooleanFilter(BookAdmin):
list_filter = (
'year',
@@ -267,18 +271,22 @@ class ListFiltersTests(TestCase):
self.djangonaut_book = Book.objects.create(
title='Djangonaut: an art of living', year=2009,
author=self.alfred, is_best_seller=True, date_registered=self.today,
+ is_best_seller2=True,
)
self.bio_book = Book.objects.create(
title='Django: a biography', year=1999, author=self.alfred,
is_best_seller=False, no=207,
+ is_best_seller2=False,
)
self.django_book = Book.objects.create(
title='The Django Book', year=None, author=self.bob,
is_best_seller=None, date_registered=self.today, no=103,
+ is_best_seller2=None,
)
self.guitar_book = Book.objects.create(
title='Guitar for dummies', year=2002, is_best_seller=True,
date_registered=self.one_week_ago,
+ is_best_seller2=True,
)
self.guitar_book.contributors.set([self.bob, self.lisa])
@@ -738,6 +746,54 @@ class ListFiltersTests(TestCase):
self.assertIs(choice['selected'], True)
self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
+ def test_booleanfieldlistfilter_nullbooleanfield(self):
+ modeladmin = BookAdmin2(Book, site)
+
+ request = self.request_factory.get('/')
+ changelist = modeladmin.get_changelist_instance(request)
+
+ request = self.request_factory.get('/', {'is_best_seller2__exact': 0})
+ changelist = modeladmin.get_changelist_instance(request)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_queryset(request)
+ self.assertEqual(list(queryset), [self.bio_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][3]
+ self.assertEqual(filterspec.title, 'is best seller2')
+ choice = select_by(filterspec.choices(changelist), "display", "No")
+ self.assertIs(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?is_best_seller2__exact=0')
+
+ request = self.request_factory.get('/', {'is_best_seller2__exact': 1})
+ changelist = modeladmin.get_changelist_instance(request)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_queryset(request)
+ self.assertEqual(list(queryset), [self.guitar_book, self.djangonaut_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][3]
+ self.assertEqual(filterspec.title, 'is best seller2')
+ choice = select_by(filterspec.choices(changelist), "display", "Yes")
+ self.assertIs(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?is_best_seller2__exact=1')
+
+ request = self.request_factory.get('/', {'is_best_seller2__isnull': 'True'})
+ changelist = modeladmin.get_changelist_instance(request)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_queryset(request)
+ self.assertEqual(list(queryset), [self.django_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][3]
+ self.assertEqual(filterspec.title, 'is best seller2')
+ choice = select_by(filterspec.choices(changelist), "display", "Unknown")
+ self.assertIs(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?is_best_seller2__isnull=True')
+
def test_fieldlistfilter_underscorelookup_tuple(self):
"""
Ensure ('fieldpath', ClassName ) lookups pass lookup_allowed checks
diff --git a/tests/admin_utils/tests.py b/tests/admin_utils/tests.py
index 4d56313ae1..d2697ca87e 100644
--- a/tests/admin_utils/tests.py
+++ b/tests/admin_utils/tests.py
@@ -166,6 +166,10 @@ class UtilsTests(SimpleTestCase):
expected = '<img src="%sadmin/img/icon-unknown.svg" alt="None">' % settings.STATIC_URL
self.assertHTMLEqual(display_value, expected)
+ display_value = display_for_field(None, models.BooleanField(null=True), self.empty_value)
+ expected = '<img src="%sadmin/img/icon-unknown.svg" alt="None" />' % settings.STATIC_URL
+ self.assertHTMLEqual(display_value, expected)
+
display_value = display_for_field(None, models.DecimalField(), self.empty_value)
self.assertEqual(display_value, self.empty_value)
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index 38565b0a7f..59228876c5 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -456,7 +456,7 @@ class Post(models.Model):
default=datetime.date.today,
help_text="Some help text for the date (with unicode ŠĐĆŽćžšđ)"
)
- public = models.NullBooleanField()
+ public = models.BooleanField(null=True, blank=True)
def awesomeness_level(self):
return "Very awesome."
@@ -692,7 +692,7 @@ class OtherStory(models.Model):
class ComplexSortedPerson(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
- is_employee = models.NullBooleanField()
+ is_employee = models.BooleanField(null=True)
class PluggableSearchPerson(models.Model):
diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
index d7759c8552..f7f8474329 100644
--- a/tests/annotations/tests.py
+++ b/tests/annotations/tests.py
@@ -521,13 +521,15 @@ class NonAggregateAnnotationTestCase(TestCase):
books = Book.objects.annotate(
is_book=Value(True, output_field=BooleanField()),
is_pony=Value(False, output_field=BooleanField()),
- is_none=Value(None, output_field=NullBooleanField()),
+ is_none=Value(None, output_field=BooleanField(null=True)),
+ is_none_old=Value(None, output_field=NullBooleanField()),
)
self.assertGreater(len(books), 0)
for book in books:
self.assertIs(book.is_book, True)
self.assertIs(book.is_pony, False)
self.assertIsNone(book.is_none)
+ self.assertIsNone(book.is_none_old)
def test_annotation_in_f_grouped_by_annotation(self):
qs = (
diff --git a/tests/backends/oracle/tests.py b/tests/backends/oracle/tests.py
index 77de9cf279..333d224b3f 100644
--- a/tests/backends/oracle/tests.py
+++ b/tests/backends/oracle/tests.py
@@ -50,7 +50,7 @@ class Tests(unittest.TestCase):
def test_boolean_constraints(self):
"""Boolean fields have check constraints on their values."""
- for field in (BooleanField(), NullBooleanField()):
+ for field in (BooleanField(), NullBooleanField(), BooleanField(null=True)):
with self.subTest(field=field):
field.set_attributes_from_name('is_nice')
self.assertIn('"IS_NICE" IN (0,1)', field.db_check(connection))
diff --git a/tests/bulk_create/models.py b/tests/bulk_create/models.py
index c0ea527a62..c4329fbfc9 100644
--- a/tests/bulk_create/models.py
+++ b/tests/bulk_create/models.py
@@ -74,7 +74,8 @@ class NullableFields(models.Model):
duration_field = models.DurationField(null=True, default=datetime.timedelta(1))
float_field = models.FloatField(null=True, default=3.2)
integer_field = models.IntegerField(null=True, default=2)
- null_boolean_field = models.NullBooleanField(null=True, default=False)
+ null_boolean_field = models.BooleanField(null=True, default=False)
+ null_boolean_field_old = models.NullBooleanField(null=True, default=False)
positive_integer_field = models.PositiveIntegerField(null=True, default=3)
positive_small_integer_field = models.PositiveSmallIntegerField(null=True, default=4)
small_integer_field = models.SmallIntegerField(null=True, default=5)
diff --git a/tests/datatypes/models.py b/tests/datatypes/models.py
index 570b2ba8e0..6e31a3a453 100644
--- a/tests/datatypes/models.py
+++ b/tests/datatypes/models.py
@@ -9,7 +9,8 @@ from django.db import models
class Donut(models.Model):
name = models.CharField(max_length=100)
is_frosted = models.BooleanField(default=False)
- has_sprinkles = models.NullBooleanField()
+ has_sprinkles = models.BooleanField(null=True)
+ has_sprinkles_old = models.NullBooleanField()
baked_date = models.DateField(null=True)
baked_time = models.TimeField(null=True)
consumed_at = models.DateTimeField(null=True)
diff --git a/tests/datatypes/tests.py b/tests/datatypes/tests.py
index 52f24fe051..924d796121 100644
--- a/tests/datatypes/tests.py
+++ b/tests/datatypes/tests.py
@@ -12,14 +12,18 @@ class DataTypesTestCase(TestCase):
d = Donut(name='Apple Fritter')
self.assertFalse(d.is_frosted)
self.assertIsNone(d.has_sprinkles)
+ self.assertIsNone(d.has_sprinkles_old)
d.has_sprinkles = True
+ d.has_sprinkles_old = True
self.assertTrue(d.has_sprinkles)
+ self.assertTrue(d.has_sprinkles_old)
d.save()
d2 = Donut.objects.get(name='Apple Fritter')
self.assertFalse(d2.is_frosted)
self.assertTrue(d2.has_sprinkles)
+ self.assertTrue(d2.has_sprinkles_old)
def test_date_type(self):
d = Donut(name='Apple Fritter')
diff --git a/tests/expressions_case/models.py b/tests/expressions_case/models.py
index 7c1a8d75b7..570b8c1849 100644
--- a/tests/expressions_case/models.py
+++ b/tests/expressions_case/models.py
@@ -25,7 +25,8 @@ class CaseTestModel(models.Model):
if Image:
image = models.ImageField(null=True)
generic_ip_address = models.GenericIPAddressField(null=True)
- null_boolean = models.NullBooleanField()
+ null_boolean = models.BooleanField(null=True)
+ null_boolean_old = models.NullBooleanField()
positive_integer = models.PositiveIntegerField(null=True)
positive_small_integer = models.PositiveSmallIntegerField(null=True)
slug = models.SlugField(default='')
diff --git a/tests/expressions_case/tests.py b/tests/expressions_case/tests.py
index a688dc073d..94731491e0 100644
--- a/tests/expressions_case/tests.py
+++ b/tests/expressions_case/tests.py
@@ -820,6 +820,19 @@ class CaseExpressionTests(TestCase):
transform=attrgetter('integer', 'null_boolean')
)
+ def test_update_null_boolean_old(self):
+ CaseTestModel.objects.update(
+ null_boolean_old=Case(
+ When(integer=1, then=True),
+ When(integer=2, then=False),
+ ),
+ )
+ self.assertQuerysetEqual(
+ CaseTestModel.objects.all().order_by('pk'),
+ [(1, True), (2, False), (3, None), (2, False), (3, None), (3, None), (4, None)],
+ transform=attrgetter('integer', 'null_boolean_old')
+ )
+
def test_update_positive_integer(self):
CaseTestModel.objects.update(
positive_integer=Case(
diff --git a/tests/generic_relations_regress/models.py b/tests/generic_relations_regress/models.py
index 5d8716aa0c..f9cdb1b549 100644
--- a/tests/generic_relations_regress/models.py
+++ b/tests/generic_relations_regress/models.py
@@ -169,7 +169,7 @@ class HasLinkThing(HasLinks):
class A(models.Model):
- flag = models.NullBooleanField()
+ flag = models.BooleanField(null=True)
content_type = models.ForeignKey(ContentType, models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
diff --git a/tests/inspectdb/models.py b/tests/inspectdb/models.py
index 1a001b653e..8c660658cd 100644
--- a/tests/inspectdb/models.py
+++ b/tests/inspectdb/models.py
@@ -44,7 +44,7 @@ class ColumnTypes(models.Model):
id = models.AutoField(primary_key=True)
big_int_field = models.BigIntegerField()
bool_field = models.BooleanField(default=False)
- null_bool_field = models.NullBooleanField()
+ null_bool_field = models.BooleanField(null=True)
char_field = models.CharField(max_length=10)
null_char_field = models.CharField(max_length=10, blank=True, null=True)
date_field = models.DateField()
diff --git a/tests/invalid_models_tests/test_ordinary_fields.py b/tests/invalid_models_tests/test_ordinary_fields.py
index a3c3a70808..2411abc930 100644
--- a/tests/invalid_models_tests/test_ordinary_fields.py
+++ b/tests/invalid_models_tests/test_ordinary_fields.py
@@ -39,24 +39,6 @@ class AutoFieldTests(SimpleTestCase):
@isolate_apps('invalid_models_tests')
-class BooleanFieldTests(SimpleTestCase):
-
- def test_nullable_boolean_field(self):
- class Model(models.Model):
- field = models.BooleanField(null=True)
-
- field = Model._meta.get_field('field')
- self.assertEqual(field.check(), [
- Error(
- 'BooleanFields do not accept null values.',
- hint='Use a NullBooleanField instead.',
- obj=field,
- id='fields.E110',
- ),
- ])
-
-
-@isolate_apps('invalid_models_tests')
class CharFieldTests(TestCase):
def test_valid_field(self):
diff --git a/tests/managers_regress/models.py b/tests/managers_regress/models.py
index 8d5725c5b7..36cd4e7572 100644
--- a/tests/managers_regress/models.py
+++ b/tests/managers_regress/models.py
@@ -119,7 +119,7 @@ class Child7(Parent):
# RelatedManagers
class RelatedModel(models.Model):
test_gfk = GenericRelation('RelationModel', content_type_field='gfk_ctype', object_id_field='gfk_id')
- exact = models.NullBooleanField()
+ exact = models.BooleanField(null=True)
def __str__(self):
return str(self.pk)
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index 1996011512..13d4843632 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -101,7 +101,8 @@ class Post(models.Model):
class NullBooleanModel(models.Model):
- nbfield = models.NullBooleanField()
+ nbfield = models.BooleanField(null=True, blank=True)
+ nbfield_old = models.NullBooleanField()
class BooleanModel(models.Model):
diff --git a/tests/model_fields/test_booleanfield.py b/tests/model_fields/test_booleanfield.py
index f295f24980..72c9293d93 100644
--- a/tests/model_fields/test_booleanfield.py
+++ b/tests/model_fields/test_booleanfield.py
@@ -24,12 +24,18 @@ class BooleanFieldTests(TestCase):
self._test_get_prep_value(models.BooleanField())
def test_nullbooleanfield_get_prep_value(self):
+ self._test_get_prep_value(models.BooleanField(null=True))
+
+ def test_nullbooleanfield_old_get_prep_value(self):
self._test_get_prep_value(models.NullBooleanField())
def test_booleanfield_to_python(self):
self._test_to_python(models.BooleanField())
def test_nullbooleanfield_to_python(self):
+ self._test_to_python(models.BooleanField(null=True))
+
+ def test_nullbooleanfield_old_to_python(self):
self._test_to_python(models.NullBooleanField())
def test_booleanfield_choices_blank(self):
@@ -42,6 +48,8 @@ class BooleanFieldTests(TestCase):
self.assertEqual(f.formfield().choices, choices)
def test_nullbooleanfield_formfield(self):
+ f = models.BooleanField(null=True)
+ self.assertIsInstance(f.formfield(), forms.NullBooleanField)
f = models.NullBooleanField()
self.assertIsInstance(f.formfield(), forms.NullBooleanField)
@@ -54,13 +62,15 @@ class BooleanFieldTests(TestCase):
b2.refresh_from_db()
self.assertIs(b2.bfield, False)
- b3 = NullBooleanModel.objects.create(nbfield=True)
+ b3 = NullBooleanModel.objects.create(nbfield=True, nbfield_old=True)
b3.refresh_from_db()
self.assertIs(b3.nbfield, True)
+ self.assertIs(b3.nbfield_old, True)
- b4 = NullBooleanModel.objects.create(nbfield=False)
+ b4 = NullBooleanModel.objects.create(nbfield=False, nbfield_old=False)
b4.refresh_from_db()
self.assertIs(b4.nbfield, False)
+ self.assertIs(b4.nbfield_old, False)
# When an extra clause exists, the boolean conversions are applied with
# an offset (#13293).
@@ -73,8 +83,8 @@ class BooleanFieldTests(TestCase):
"""
bmt = BooleanModel.objects.create(bfield=True)
bmf = BooleanModel.objects.create(bfield=False)
- nbmt = NullBooleanModel.objects.create(nbfield=True)
- nbmf = NullBooleanModel.objects.create(nbfield=False)
+ nbmt = NullBooleanModel.objects.create(nbfield=True, nbfield_old=True)
+ nbmf = NullBooleanModel.objects.create(nbfield=False, nbfield_old=False)
m1 = FksToBooleans.objects.create(bf=bmt, nbf=nbmt)
m2 = FksToBooleans.objects.create(bf=bmf, nbf=nbmf)
@@ -88,8 +98,10 @@ class BooleanFieldTests(TestCase):
mc = FksToBooleans.objects.select_related().get(pk=m2.id)
self.assertIs(mb.bf.bfield, True)
self.assertIs(mb.nbf.nbfield, True)
+ self.assertIs(mb.nbf.nbfield_old, True)
self.assertIs(mc.bf.bfield, False)
self.assertIs(mc.nbf.nbfield, False)
+ self.assertIs(mc.nbf.nbfield_old, False)
def test_null_default(self):
"""
@@ -105,6 +117,7 @@ class BooleanFieldTests(TestCase):
nb = NullBooleanModel()
self.assertIsNone(nb.nbfield)
+ self.assertIsNone(nb.nbfield_old)
nb.save() # no error
@@ -120,5 +133,5 @@ class ValidationTest(SimpleTestCase):
NullBooleanField shouldn't throw a validation error when given a value
of None.
"""
- nullboolean = NullBooleanModel(nbfield=None)
+ nullboolean = NullBooleanModel(nbfield=None, nbfield_old=None)
nullboolean.full_clean()
diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py
index cbc984d485..57465612b5 100644
--- a/tests/postgres_tests/migrations/0002_create_test_models.py
+++ b/tests/postgres_tests/migrations/0002_create_test_models.py
@@ -168,7 +168,7 @@ class Migration(migrations.Migration):
name='AggregateTestModel',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('boolean_field', models.NullBooleanField()),
+ ('boolean_field', models.BooleanField(null=True)),
('char_field', models.CharField(max_length=30, blank=True)),
('integer_field', models.IntegerField(null=True)),
]
diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py
index 35e76d0622..d5865818e7 100644
--- a/tests/postgres_tests/models.py
+++ b/tests/postgres_tests/models.py
@@ -156,7 +156,7 @@ class AggregateTestModel(models.Model):
"""
char_field = models.CharField(max_length=30, blank=True)
integer_field = models.IntegerField(null=True)
- boolean_field = models.NullBooleanField()
+ boolean_field = models.BooleanField(null=True)
class StatTestModel(models.Model):
diff --git a/tests/queries/models.py b/tests/queries/models.py
index 64f426c4a7..587d2e683e 100644
--- a/tests/queries/models.py
+++ b/tests/queries/models.py
@@ -676,7 +676,7 @@ class Student(models.Model):
class Classroom(models.Model):
name = models.CharField(max_length=20)
- has_blackboard = models.NullBooleanField()
+ has_blackboard = models.BooleanField(null=True)
school = models.ForeignKey(School, models.CASCADE)
students = models.ManyToManyField(Student, related_name='classroom')
diff --git a/tests/serializers/models/data.py b/tests/serializers/models/data.py
index b62f85a868..533ccf6830 100644
--- a/tests/serializers/models/data.py
+++ b/tests/serializers/models/data.py
@@ -18,7 +18,7 @@ class BinaryData(models.Model):
class BooleanData(models.Model):
- data = models.BooleanField(default=False)
+ data = models.BooleanField(default=False, null=True)
class CharData(models.Model):
@@ -226,10 +226,6 @@ class IntegerPKData(models.Model):
class GenericIPAddressPKData(models.Model):
data = models.GenericIPAddressField(primary_key=True)
-# This is just a Boolean field with null=True, and we can't test a PK value of NULL.
-# class NullBooleanPKData(models.Model):
-# data = models.NullBooleanField(primary_key=True)
-
class PositiveIntegerPKData(models.Model):
data = models.PositiveIntegerField(primary_key=True)
diff --git a/tests/serializers/test_data.py b/tests/serializers/test_data.py
index 467292bff1..9f0e311d62 100644
--- a/tests/serializers/test_data.py
+++ b/tests/serializers/test_data.py
@@ -200,6 +200,7 @@ test_data = [
(data_obj, 2, BinaryData, None),
(data_obj, 5, BooleanData, True),
(data_obj, 6, BooleanData, False),
+ (data_obj, 7, BooleanData, None),
(data_obj, 10, CharData, "Test Char Data"),
(data_obj, 11, CharData, ""),
(data_obj, 12, CharData, "None"),
@@ -334,8 +335,6 @@ The end."""),
(pk_obj, 682, IntegerPKData, 0),
# (XX, ImagePKData
(pk_obj, 695, GenericIPAddressPKData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"),
- # (pk_obj, 700, NullBooleanPKData, True),
- # (pk_obj, 701, NullBooleanPKData, False),
(pk_obj, 720, PositiveIntegerPKData, 123456789),
(pk_obj, 730, PositiveSmallIntegerPKData, 12),
(pk_obj, 740, SlugPKData, "this-is-a-slug"),
diff --git a/tests/validation/test_error_messages.py b/tests/validation/test_error_messages.py
index d2fc3e4939..0869d0fc10 100644
--- a/tests/validation/test_error_messages.py
+++ b/tests/validation/test_error_messages.py
@@ -23,6 +23,10 @@ class ValidationMessagesTest(TestCase):
f = models.BooleanField()
self._test_validation_messages(f, 'fõo', ["'fõo' value must be either True or False."])
+ def test_nullable_boolean_field_raises_error_message(self):
+ f = models.BooleanField(null=True)
+ self._test_validation_messages(f, 'fõo', ["'fõo' value must be either True, False, or None."])
+
def test_float_field_raises_error_message(self):
f = models.FloatField()
self._test_validation_messages(f, 'fõo', ["'fõo' value must be a float."])