mirror of https://github.com/django/django.git
Fixed #29227 -- Allowed BooleanField to be null=True.
Thanks Lynn Cyrin for contributing to the patch, and Nick Pope for review.
This commit is contained in:
parent
73f7d1755f
commit
5fa4f40f45
|
@ -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]),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -41,6 +41,7 @@ class BulkInsertMapper:
|
|||
types = {
|
||||
'BigIntegerField': NUMBER,
|
||||
'BinaryField': BLOB,
|
||||
'BooleanField': NUMBER,
|
||||
'DateField': DATE,
|
||||
'DateTimeField': TIMESTAMP,
|
||||
'DecimalField': NUMBER,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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``.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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``
|
||||
------------------------
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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='')
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -38,24 +38,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):
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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."])
|
||||
|
|
Loading…
Reference in New Issue