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]),
|
'query_string': changelist.get_query_string({self.lookup_kwarg: lookup}, [self.lookup_kwarg2]),
|
||||||
'display': title,
|
'display': title,
|
||||||
}
|
}
|
||||||
if isinstance(self.field, models.NullBooleanField):
|
if self.field.null:
|
||||||
yield {
|
yield {
|
||||||
'selected': self.lookup_val2 == 'True',
|
'selected': self.lookup_val2 == 'True',
|
||||||
'query_string': changelist.get_query_string({self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]),
|
'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):
|
if getattr(field, 'flatchoices', None):
|
||||||
return dict(field.flatchoices).get(value, empty_value_display)
|
return dict(field.flatchoices).get(value, empty_value_display)
|
||||||
# NullBooleanField needs special-case null-handling, so it comes
|
# BooleanField needs special-case null-handling, so it comes before the
|
||||||
# before the general null test.
|
# general null test.
|
||||||
elif isinstance(field, (models.BooleanField, models.NullBooleanField)):
|
elif isinstance(field, (models.BooleanField, models.NullBooleanField)):
|
||||||
return _boolean_icon(value)
|
return _boolean_icon(value)
|
||||||
elif value is None:
|
elif value is None:
|
||||||
|
|
|
@ -41,6 +41,7 @@ class BulkInsertMapper:
|
||||||
types = {
|
types = {
|
||||||
'BigIntegerField': NUMBER,
|
'BigIntegerField': NUMBER,
|
||||||
'BinaryField': BLOB,
|
'BinaryField': BLOB,
|
||||||
|
'BooleanField': NUMBER,
|
||||||
'DateField': DATE,
|
'DateField': DATE,
|
||||||
'DateTimeField': TIMESTAMP,
|
'DateTimeField': TIMESTAMP,
|
||||||
'DecimalField': NUMBER,
|
'DecimalField': NUMBER,
|
||||||
|
|
|
@ -988,41 +988,16 @@ class BooleanField(Field):
|
||||||
empty_strings_allowed = False
|
empty_strings_allowed = False
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _("'%(value)s' value must be either True or False."),
|
'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)")
|
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):
|
def get_internal_type(self):
|
||||||
return "BooleanField"
|
return "BooleanField"
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
|
if self.null and value in self.empty_values:
|
||||||
|
return None
|
||||||
if value in (True, False):
|
if value in (True, False):
|
||||||
# if value is 1 or 0 than it's equal to True or False, but we want
|
# 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.
|
# to return a true bool for semantic reasons.
|
||||||
|
@ -1032,7 +1007,7 @@ class BooleanField(Field):
|
||||||
if value in ('f', 'False', '0'):
|
if value in ('f', 'False', '0'):
|
||||||
return False
|
return False
|
||||||
raise exceptions.ValidationError(
|
raise exceptions.ValidationError(
|
||||||
self.error_messages['invalid'],
|
self.error_messages['invalid_nullable' if self.null else 'invalid'],
|
||||||
code='invalid',
|
code='invalid',
|
||||||
params={'value': value},
|
params={'value': value},
|
||||||
)
|
)
|
||||||
|
@ -1044,15 +1019,16 @@ class BooleanField(Field):
|
||||||
return self.to_python(value)
|
return self.to_python(value)
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs):
|
||||||
# Unlike most fields, BooleanField figures out include_blank from
|
|
||||||
# self.null instead of self.blank.
|
|
||||||
if self.choices:
|
if self.choices:
|
||||||
include_blank = not (self.has_default() or 'initial' in kwargs)
|
include_blank = not (self.has_default() or 'initial' in kwargs)
|
||||||
defaults = {'choices': self.get_choices(include_blank=include_blank)}
|
defaults = {'choices': self.get_choices(include_blank=include_blank)}
|
||||||
else:
|
else:
|
||||||
defaults = {'form_class': forms.BooleanField}
|
form_class = forms.NullBooleanField if self.null else forms.BooleanField
|
||||||
defaults.update(kwargs)
|
# In HTML checkboxes, 'required' means "must be checked" which is
|
||||||
return super().formfield(**defaults)
|
# 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):
|
class CharField(Field):
|
||||||
|
|
|
@ -138,7 +138,8 @@ Model fields
|
||||||
* **fields.E007**: Primary keys must not have ``null=True``.
|
* **fields.E007**: Primary keys must not have ``null=True``.
|
||||||
* **fields.E008**: All ``validators`` must be callable.
|
* **fields.E008**: All ``validators`` must be callable.
|
||||||
* **fields.E100**: ``AutoField``\s must set primary_key=True.
|
* **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.E120**: ``CharField``\s must define a ``max_length`` attribute.
|
||||||
* **fields.E121**: ``max_length`` must be a positive integer.
|
* **fields.E121**: ``max_length`` must be a positive integer.
|
||||||
* **fields.W122**: ``max_length`` is ignored when used with ``IntegerField``.
|
* **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
|
and add that method's name to ``list_display``. (See below for more
|
||||||
on custom methods in ``list_display``.)
|
on custom methods in ``list_display``.)
|
||||||
|
|
||||||
* If the field is a ``BooleanField`` or ``NullBooleanField``, Django
|
* If the field is a ``BooleanField``, Django will display a pretty "on" or
|
||||||
will display a pretty "on" or "off" icon instead of ``True`` or
|
"off" icon instead of ``True`` or ``False``.
|
||||||
``False``.
|
|
||||||
|
|
||||||
* If the string given is a method of the model, ``ModelAdmin`` or a
|
* If the string given is a method of the model, ``ModelAdmin`` or a
|
||||||
callable, Django will HTML-escape the output by default. To escape
|
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
|
When using the Oracle database backend, the value ``NULL`` will be stored to
|
||||||
denote the empty string regardless of this attribute.
|
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``
|
``blank``
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -442,15 +439,18 @@ case it can't be included in a :class:`~django.forms.ModelForm`.
|
||||||
|
|
||||||
A true/false field.
|
A true/false field.
|
||||||
|
|
||||||
The default form widget for this field is a
|
The default form widget for this field is :class:`~django.forms.CheckboxInput`,
|
||||||
:class:`~django.forms.CheckboxInput`.
|
or :class:`~django.forms.NullBooleanSelect` if :attr:`null=True <Field.null>`.
|
||||||
|
|
||||||
If you need to accept :attr:`~Field.null` values then use
|
|
||||||
:class:`NullBooleanField` instead.
|
|
||||||
|
|
||||||
The default value of ``BooleanField`` is ``None`` when :attr:`Field.default`
|
The default value of ``BooleanField`` is ``None`` when :attr:`Field.default`
|
||||||
isn't defined.
|
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``
|
``CharField``
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -1008,9 +1008,8 @@ values are stored as null.
|
||||||
|
|
||||||
.. class:: NullBooleanField(**options)
|
.. class:: NullBooleanField(**options)
|
||||||
|
|
||||||
Like a :class:`BooleanField`, but allows ``NULL`` as one of the options. Use
|
Like :class:`BooleanField` with ``null=True``. Use that instead of this field
|
||||||
this instead of a :class:`BooleanField` with ``null=True``. The default form
|
as it's likely to be deprecated in a future version of Django.
|
||||||
widget for this field is a :class:`~django.forms.NullBooleanSelect`.
|
|
||||||
|
|
||||||
``PositiveIntegerField``
|
``PositiveIntegerField``
|
||||||
------------------------
|
------------------------
|
||||||
|
|
|
@ -223,6 +223,10 @@ Models
|
||||||
* :meth:`.QuerySet.order_by` and :meth:`distinct(*fields) <.QuerySet.distinct>`
|
* :meth:`.QuerySet.order_by` and :meth:`distinct(*fields) <.QuerySet.distinct>`
|
||||||
now support using field transforms.
|
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
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,9 @@ Model field Form field
|
||||||
``True`` on the model field, otherwise not
|
``True`` on the model field, otherwise not
|
||||||
represented in the form.
|
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
|
:class:`CharField` :class:`~django.forms.CharField` with
|
||||||
``max_length`` set to the model field's
|
``max_length`` set to the model field's
|
||||||
|
|
|
@ -28,7 +28,8 @@ class Book(models.Model):
|
||||||
verbose_name='Employee',
|
verbose_name='Employee',
|
||||||
blank=True, null=True,
|
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)
|
date_registered = models.DateField(null=True)
|
||||||
# This field name is intentionally 2 characters long (#16080).
|
# This field name is intentionally 2 characters long (#16080).
|
||||||
no = models.IntegerField(verbose_name='number', blank=True, null=True)
|
no = models.IntegerField(verbose_name='number', blank=True, null=True)
|
||||||
|
|
|
@ -142,6 +142,10 @@ class BookAdmin(ModelAdmin):
|
||||||
ordering = ('-id',)
|
ordering = ('-id',)
|
||||||
|
|
||||||
|
|
||||||
|
class BookAdmin2(ModelAdmin):
|
||||||
|
list_filter = ('year', 'author', 'contributors', 'is_best_seller2', 'date_registered', 'no')
|
||||||
|
|
||||||
|
|
||||||
class BookAdminWithTupleBooleanFilter(BookAdmin):
|
class BookAdminWithTupleBooleanFilter(BookAdmin):
|
||||||
list_filter = (
|
list_filter = (
|
||||||
'year',
|
'year',
|
||||||
|
@ -267,18 +271,22 @@ class ListFiltersTests(TestCase):
|
||||||
self.djangonaut_book = Book.objects.create(
|
self.djangonaut_book = Book.objects.create(
|
||||||
title='Djangonaut: an art of living', year=2009,
|
title='Djangonaut: an art of living', year=2009,
|
||||||
author=self.alfred, is_best_seller=True, date_registered=self.today,
|
author=self.alfred, is_best_seller=True, date_registered=self.today,
|
||||||
|
is_best_seller2=True,
|
||||||
)
|
)
|
||||||
self.bio_book = Book.objects.create(
|
self.bio_book = Book.objects.create(
|
||||||
title='Django: a biography', year=1999, author=self.alfred,
|
title='Django: a biography', year=1999, author=self.alfred,
|
||||||
is_best_seller=False, no=207,
|
is_best_seller=False, no=207,
|
||||||
|
is_best_seller2=False,
|
||||||
)
|
)
|
||||||
self.django_book = Book.objects.create(
|
self.django_book = Book.objects.create(
|
||||||
title='The Django Book', year=None, author=self.bob,
|
title='The Django Book', year=None, author=self.bob,
|
||||||
is_best_seller=None, date_registered=self.today, no=103,
|
is_best_seller=None, date_registered=self.today, no=103,
|
||||||
|
is_best_seller2=None,
|
||||||
)
|
)
|
||||||
self.guitar_book = Book.objects.create(
|
self.guitar_book = Book.objects.create(
|
||||||
title='Guitar for dummies', year=2002, is_best_seller=True,
|
title='Guitar for dummies', year=2002, is_best_seller=True,
|
||||||
date_registered=self.one_week_ago,
|
date_registered=self.one_week_ago,
|
||||||
|
is_best_seller2=True,
|
||||||
)
|
)
|
||||||
self.guitar_book.contributors.set([self.bob, self.lisa])
|
self.guitar_book.contributors.set([self.bob, self.lisa])
|
||||||
|
|
||||||
|
@ -738,6 +746,54 @@ class ListFiltersTests(TestCase):
|
||||||
self.assertIs(choice['selected'], True)
|
self.assertIs(choice['selected'], True)
|
||||||
self.assertEqual(choice['query_string'], '?is_best_seller__isnull=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):
|
def test_fieldlistfilter_underscorelookup_tuple(self):
|
||||||
"""
|
"""
|
||||||
Ensure ('fieldpath', ClassName ) lookups pass lookup_allowed checks
|
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
|
expected = '<img src="%sadmin/img/icon-unknown.svg" alt="None">' % settings.STATIC_URL
|
||||||
self.assertHTMLEqual(display_value, expected)
|
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)
|
display_value = display_for_field(None, models.DecimalField(), self.empty_value)
|
||||||
self.assertEqual(display_value, self.empty_value)
|
self.assertEqual(display_value, self.empty_value)
|
||||||
|
|
||||||
|
|
|
@ -456,7 +456,7 @@ class Post(models.Model):
|
||||||
default=datetime.date.today,
|
default=datetime.date.today,
|
||||||
help_text="Some help text for the date (with unicode ŠĐĆŽćžšđ)"
|
help_text="Some help text for the date (with unicode ŠĐĆŽćžšđ)"
|
||||||
)
|
)
|
||||||
public = models.NullBooleanField()
|
public = models.BooleanField(null=True, blank=True)
|
||||||
|
|
||||||
def awesomeness_level(self):
|
def awesomeness_level(self):
|
||||||
return "Very awesome."
|
return "Very awesome."
|
||||||
|
@ -692,7 +692,7 @@ class OtherStory(models.Model):
|
||||||
class ComplexSortedPerson(models.Model):
|
class ComplexSortedPerson(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
age = models.PositiveIntegerField()
|
age = models.PositiveIntegerField()
|
||||||
is_employee = models.NullBooleanField()
|
is_employee = models.BooleanField(null=True)
|
||||||
|
|
||||||
|
|
||||||
class PluggableSearchPerson(models.Model):
|
class PluggableSearchPerson(models.Model):
|
||||||
|
|
|
@ -521,13 +521,15 @@ class NonAggregateAnnotationTestCase(TestCase):
|
||||||
books = Book.objects.annotate(
|
books = Book.objects.annotate(
|
||||||
is_book=Value(True, output_field=BooleanField()),
|
is_book=Value(True, output_field=BooleanField()),
|
||||||
is_pony=Value(False, 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)
|
self.assertGreater(len(books), 0)
|
||||||
for book in books:
|
for book in books:
|
||||||
self.assertIs(book.is_book, True)
|
self.assertIs(book.is_book, True)
|
||||||
self.assertIs(book.is_pony, False)
|
self.assertIs(book.is_pony, False)
|
||||||
self.assertIsNone(book.is_none)
|
self.assertIsNone(book.is_none)
|
||||||
|
self.assertIsNone(book.is_none_old)
|
||||||
|
|
||||||
def test_annotation_in_f_grouped_by_annotation(self):
|
def test_annotation_in_f_grouped_by_annotation(self):
|
||||||
qs = (
|
qs = (
|
||||||
|
|
|
@ -50,7 +50,7 @@ class Tests(unittest.TestCase):
|
||||||
|
|
||||||
def test_boolean_constraints(self):
|
def test_boolean_constraints(self):
|
||||||
"""Boolean fields have check constraints on their values."""
|
"""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):
|
with self.subTest(field=field):
|
||||||
field.set_attributes_from_name('is_nice')
|
field.set_attributes_from_name('is_nice')
|
||||||
self.assertIn('"IS_NICE" IN (0,1)', field.db_check(connection))
|
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))
|
duration_field = models.DurationField(null=True, default=datetime.timedelta(1))
|
||||||
float_field = models.FloatField(null=True, default=3.2)
|
float_field = models.FloatField(null=True, default=3.2)
|
||||||
integer_field = models.IntegerField(null=True, default=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_integer_field = models.PositiveIntegerField(null=True, default=3)
|
||||||
positive_small_integer_field = models.PositiveSmallIntegerField(null=True, default=4)
|
positive_small_integer_field = models.PositiveSmallIntegerField(null=True, default=4)
|
||||||
small_integer_field = models.SmallIntegerField(null=True, default=5)
|
small_integer_field = models.SmallIntegerField(null=True, default=5)
|
||||||
|
|
|
@ -9,7 +9,8 @@ from django.db import models
|
||||||
class Donut(models.Model):
|
class Donut(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
is_frosted = models.BooleanField(default=False)
|
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_date = models.DateField(null=True)
|
||||||
baked_time = models.TimeField(null=True)
|
baked_time = models.TimeField(null=True)
|
||||||
consumed_at = models.DateTimeField(null=True)
|
consumed_at = models.DateTimeField(null=True)
|
||||||
|
|
|
@ -12,14 +12,18 @@ class DataTypesTestCase(TestCase):
|
||||||
d = Donut(name='Apple Fritter')
|
d = Donut(name='Apple Fritter')
|
||||||
self.assertFalse(d.is_frosted)
|
self.assertFalse(d.is_frosted)
|
||||||
self.assertIsNone(d.has_sprinkles)
|
self.assertIsNone(d.has_sprinkles)
|
||||||
|
self.assertIsNone(d.has_sprinkles_old)
|
||||||
d.has_sprinkles = True
|
d.has_sprinkles = True
|
||||||
|
d.has_sprinkles_old = True
|
||||||
self.assertTrue(d.has_sprinkles)
|
self.assertTrue(d.has_sprinkles)
|
||||||
|
self.assertTrue(d.has_sprinkles_old)
|
||||||
|
|
||||||
d.save()
|
d.save()
|
||||||
|
|
||||||
d2 = Donut.objects.get(name='Apple Fritter')
|
d2 = Donut.objects.get(name='Apple Fritter')
|
||||||
self.assertFalse(d2.is_frosted)
|
self.assertFalse(d2.is_frosted)
|
||||||
self.assertTrue(d2.has_sprinkles)
|
self.assertTrue(d2.has_sprinkles)
|
||||||
|
self.assertTrue(d2.has_sprinkles_old)
|
||||||
|
|
||||||
def test_date_type(self):
|
def test_date_type(self):
|
||||||
d = Donut(name='Apple Fritter')
|
d = Donut(name='Apple Fritter')
|
||||||
|
|
|
@ -25,7 +25,8 @@ class CaseTestModel(models.Model):
|
||||||
if Image:
|
if Image:
|
||||||
image = models.ImageField(null=True)
|
image = models.ImageField(null=True)
|
||||||
generic_ip_address = models.GenericIPAddressField(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_integer = models.PositiveIntegerField(null=True)
|
||||||
positive_small_integer = models.PositiveSmallIntegerField(null=True)
|
positive_small_integer = models.PositiveSmallIntegerField(null=True)
|
||||||
slug = models.SlugField(default='')
|
slug = models.SlugField(default='')
|
||||||
|
|
|
@ -820,6 +820,19 @@ class CaseExpressionTests(TestCase):
|
||||||
transform=attrgetter('integer', 'null_boolean')
|
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):
|
def test_update_positive_integer(self):
|
||||||
CaseTestModel.objects.update(
|
CaseTestModel.objects.update(
|
||||||
positive_integer=Case(
|
positive_integer=Case(
|
||||||
|
|
|
@ -169,7 +169,7 @@ class HasLinkThing(HasLinks):
|
||||||
|
|
||||||
|
|
||||||
class A(models.Model):
|
class A(models.Model):
|
||||||
flag = models.NullBooleanField()
|
flag = models.BooleanField(null=True)
|
||||||
content_type = models.ForeignKey(ContentType, models.CASCADE)
|
content_type = models.ForeignKey(ContentType, models.CASCADE)
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
content_object = GenericForeignKey('content_type', 'object_id')
|
content_object = GenericForeignKey('content_type', 'object_id')
|
||||||
|
|
|
@ -44,7 +44,7 @@ class ColumnTypes(models.Model):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
big_int_field = models.BigIntegerField()
|
big_int_field = models.BigIntegerField()
|
||||||
bool_field = models.BooleanField(default=False)
|
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)
|
char_field = models.CharField(max_length=10)
|
||||||
null_char_field = models.CharField(max_length=10, blank=True, null=True)
|
null_char_field = models.CharField(max_length=10, blank=True, null=True)
|
||||||
date_field = models.DateField()
|
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')
|
@isolate_apps('invalid_models_tests')
|
||||||
class CharFieldTests(TestCase):
|
class CharFieldTests(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ class Child7(Parent):
|
||||||
# RelatedManagers
|
# RelatedManagers
|
||||||
class RelatedModel(models.Model):
|
class RelatedModel(models.Model):
|
||||||
test_gfk = GenericRelation('RelationModel', content_type_field='gfk_ctype', object_id_field='gfk_id')
|
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):
|
def __str__(self):
|
||||||
return str(self.pk)
|
return str(self.pk)
|
||||||
|
|
|
@ -101,7 +101,8 @@ class Post(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class NullBooleanModel(models.Model):
|
class NullBooleanModel(models.Model):
|
||||||
nbfield = models.NullBooleanField()
|
nbfield = models.BooleanField(null=True, blank=True)
|
||||||
|
nbfield_old = models.NullBooleanField()
|
||||||
|
|
||||||
|
|
||||||
class BooleanModel(models.Model):
|
class BooleanModel(models.Model):
|
||||||
|
|
|
@ -24,12 +24,18 @@ class BooleanFieldTests(TestCase):
|
||||||
self._test_get_prep_value(models.BooleanField())
|
self._test_get_prep_value(models.BooleanField())
|
||||||
|
|
||||||
def test_nullbooleanfield_get_prep_value(self):
|
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())
|
self._test_get_prep_value(models.NullBooleanField())
|
||||||
|
|
||||||
def test_booleanfield_to_python(self):
|
def test_booleanfield_to_python(self):
|
||||||
self._test_to_python(models.BooleanField())
|
self._test_to_python(models.BooleanField())
|
||||||
|
|
||||||
def test_nullbooleanfield_to_python(self):
|
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())
|
self._test_to_python(models.NullBooleanField())
|
||||||
|
|
||||||
def test_booleanfield_choices_blank(self):
|
def test_booleanfield_choices_blank(self):
|
||||||
|
@ -42,6 +48,8 @@ class BooleanFieldTests(TestCase):
|
||||||
self.assertEqual(f.formfield().choices, choices)
|
self.assertEqual(f.formfield().choices, choices)
|
||||||
|
|
||||||
def test_nullbooleanfield_formfield(self):
|
def test_nullbooleanfield_formfield(self):
|
||||||
|
f = models.BooleanField(null=True)
|
||||||
|
self.assertIsInstance(f.formfield(), forms.NullBooleanField)
|
||||||
f = models.NullBooleanField()
|
f = models.NullBooleanField()
|
||||||
self.assertIsInstance(f.formfield(), forms.NullBooleanField)
|
self.assertIsInstance(f.formfield(), forms.NullBooleanField)
|
||||||
|
|
||||||
|
@ -54,13 +62,15 @@ class BooleanFieldTests(TestCase):
|
||||||
b2.refresh_from_db()
|
b2.refresh_from_db()
|
||||||
self.assertIs(b2.bfield, False)
|
self.assertIs(b2.bfield, False)
|
||||||
|
|
||||||
b3 = NullBooleanModel.objects.create(nbfield=True)
|
b3 = NullBooleanModel.objects.create(nbfield=True, nbfield_old=True)
|
||||||
b3.refresh_from_db()
|
b3.refresh_from_db()
|
||||||
self.assertIs(b3.nbfield, True)
|
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()
|
b4.refresh_from_db()
|
||||||
self.assertIs(b4.nbfield, False)
|
self.assertIs(b4.nbfield, False)
|
||||||
|
self.assertIs(b4.nbfield_old, False)
|
||||||
|
|
||||||
# When an extra clause exists, the boolean conversions are applied with
|
# When an extra clause exists, the boolean conversions are applied with
|
||||||
# an offset (#13293).
|
# an offset (#13293).
|
||||||
|
@ -73,8 +83,8 @@ class BooleanFieldTests(TestCase):
|
||||||
"""
|
"""
|
||||||
bmt = BooleanModel.objects.create(bfield=True)
|
bmt = BooleanModel.objects.create(bfield=True)
|
||||||
bmf = BooleanModel.objects.create(bfield=False)
|
bmf = BooleanModel.objects.create(bfield=False)
|
||||||
nbmt = NullBooleanModel.objects.create(nbfield=True)
|
nbmt = NullBooleanModel.objects.create(nbfield=True, nbfield_old=True)
|
||||||
nbmf = NullBooleanModel.objects.create(nbfield=False)
|
nbmf = NullBooleanModel.objects.create(nbfield=False, nbfield_old=False)
|
||||||
m1 = FksToBooleans.objects.create(bf=bmt, nbf=nbmt)
|
m1 = FksToBooleans.objects.create(bf=bmt, nbf=nbmt)
|
||||||
m2 = FksToBooleans.objects.create(bf=bmf, nbf=nbmf)
|
m2 = FksToBooleans.objects.create(bf=bmf, nbf=nbmf)
|
||||||
|
|
||||||
|
@ -88,8 +98,10 @@ class BooleanFieldTests(TestCase):
|
||||||
mc = FksToBooleans.objects.select_related().get(pk=m2.id)
|
mc = FksToBooleans.objects.select_related().get(pk=m2.id)
|
||||||
self.assertIs(mb.bf.bfield, True)
|
self.assertIs(mb.bf.bfield, True)
|
||||||
self.assertIs(mb.nbf.nbfield, True)
|
self.assertIs(mb.nbf.nbfield, True)
|
||||||
|
self.assertIs(mb.nbf.nbfield_old, True)
|
||||||
self.assertIs(mc.bf.bfield, False)
|
self.assertIs(mc.bf.bfield, False)
|
||||||
self.assertIs(mc.nbf.nbfield, False)
|
self.assertIs(mc.nbf.nbfield, False)
|
||||||
|
self.assertIs(mc.nbf.nbfield_old, False)
|
||||||
|
|
||||||
def test_null_default(self):
|
def test_null_default(self):
|
||||||
"""
|
"""
|
||||||
|
@ -105,6 +117,7 @@ class BooleanFieldTests(TestCase):
|
||||||
|
|
||||||
nb = NullBooleanModel()
|
nb = NullBooleanModel()
|
||||||
self.assertIsNone(nb.nbfield)
|
self.assertIsNone(nb.nbfield)
|
||||||
|
self.assertIsNone(nb.nbfield_old)
|
||||||
nb.save() # no error
|
nb.save() # no error
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,5 +133,5 @@ class ValidationTest(SimpleTestCase):
|
||||||
NullBooleanField shouldn't throw a validation error when given a value
|
NullBooleanField shouldn't throw a validation error when given a value
|
||||||
of None.
|
of None.
|
||||||
"""
|
"""
|
||||||
nullboolean = NullBooleanModel(nbfield=None)
|
nullboolean = NullBooleanModel(nbfield=None, nbfield_old=None)
|
||||||
nullboolean.full_clean()
|
nullboolean.full_clean()
|
||||||
|
|
|
@ -168,7 +168,7 @@ class Migration(migrations.Migration):
|
||||||
name='AggregateTestModel',
|
name='AggregateTestModel',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
('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)),
|
('char_field', models.CharField(max_length=30, blank=True)),
|
||||||
('integer_field', models.IntegerField(null=True)),
|
('integer_field', models.IntegerField(null=True)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -156,7 +156,7 @@ class AggregateTestModel(models.Model):
|
||||||
"""
|
"""
|
||||||
char_field = models.CharField(max_length=30, blank=True)
|
char_field = models.CharField(max_length=30, blank=True)
|
||||||
integer_field = models.IntegerField(null=True)
|
integer_field = models.IntegerField(null=True)
|
||||||
boolean_field = models.NullBooleanField()
|
boolean_field = models.BooleanField(null=True)
|
||||||
|
|
||||||
|
|
||||||
class StatTestModel(models.Model):
|
class StatTestModel(models.Model):
|
||||||
|
|
|
@ -676,7 +676,7 @@ class Student(models.Model):
|
||||||
|
|
||||||
class Classroom(models.Model):
|
class Classroom(models.Model):
|
||||||
name = models.CharField(max_length=20)
|
name = models.CharField(max_length=20)
|
||||||
has_blackboard = models.NullBooleanField()
|
has_blackboard = models.BooleanField(null=True)
|
||||||
school = models.ForeignKey(School, models.CASCADE)
|
school = models.ForeignKey(School, models.CASCADE)
|
||||||
students = models.ManyToManyField(Student, related_name='classroom')
|
students = models.ManyToManyField(Student, related_name='classroom')
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class BinaryData(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class BooleanData(models.Model):
|
class BooleanData(models.Model):
|
||||||
data = models.BooleanField(default=False)
|
data = models.BooleanField(default=False, null=True)
|
||||||
|
|
||||||
|
|
||||||
class CharData(models.Model):
|
class CharData(models.Model):
|
||||||
|
@ -226,10 +226,6 @@ class IntegerPKData(models.Model):
|
||||||
class GenericIPAddressPKData(models.Model):
|
class GenericIPAddressPKData(models.Model):
|
||||||
data = models.GenericIPAddressField(primary_key=True)
|
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):
|
class PositiveIntegerPKData(models.Model):
|
||||||
data = models.PositiveIntegerField(primary_key=True)
|
data = models.PositiveIntegerField(primary_key=True)
|
||||||
|
|
|
@ -200,6 +200,7 @@ test_data = [
|
||||||
(data_obj, 2, BinaryData, None),
|
(data_obj, 2, BinaryData, None),
|
||||||
(data_obj, 5, BooleanData, True),
|
(data_obj, 5, BooleanData, True),
|
||||||
(data_obj, 6, BooleanData, False),
|
(data_obj, 6, BooleanData, False),
|
||||||
|
(data_obj, 7, BooleanData, None),
|
||||||
(data_obj, 10, CharData, "Test Char Data"),
|
(data_obj, 10, CharData, "Test Char Data"),
|
||||||
(data_obj, 11, CharData, ""),
|
(data_obj, 11, CharData, ""),
|
||||||
(data_obj, 12, CharData, "None"),
|
(data_obj, 12, CharData, "None"),
|
||||||
|
@ -334,8 +335,6 @@ The end."""),
|
||||||
(pk_obj, 682, IntegerPKData, 0),
|
(pk_obj, 682, IntegerPKData, 0),
|
||||||
# (XX, ImagePKData
|
# (XX, ImagePKData
|
||||||
(pk_obj, 695, GenericIPAddressPKData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"),
|
(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, 720, PositiveIntegerPKData, 123456789),
|
||||||
(pk_obj, 730, PositiveSmallIntegerPKData, 12),
|
(pk_obj, 730, PositiveSmallIntegerPKData, 12),
|
||||||
(pk_obj, 740, SlugPKData, "this-is-a-slug"),
|
(pk_obj, 740, SlugPKData, "this-is-a-slug"),
|
||||||
|
|
|
@ -23,6 +23,10 @@ class ValidationMessagesTest(TestCase):
|
||||||
f = models.BooleanField()
|
f = models.BooleanField()
|
||||||
self._test_validation_messages(f, 'fõo', ["'fõo' value must be either True or False."])
|
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):
|
def test_float_field_raises_error_message(self):
|
||||||
f = models.FloatField()
|
f = models.FloatField()
|
||||||
self._test_validation_messages(f, 'fõo', ["'fõo' value must be a float."])
|
self._test_validation_messages(f, 'fõo', ["'fõo' value must be a float."])
|
||||||
|
|
Loading…
Reference in New Issue