diff options
| author | Tim Graham <timograham@gmail.com> | 2017-05-06 10:56:28 -0400 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2018-03-20 12:10:10 -0400 |
| commit | 5fa4f40f45fcdbb7e48489ed3039a314b5c961d0 (patch) | |
| tree | 272b8798d2c2a054f56d8613a42453bce30f92c0 /tests | |
| parent | 73f7d1755ff1da3aac687c7b046e4b5028e505db (diff) | |
Fixed #29227 -- Allowed BooleanField to be null=True.
Thanks Lynn Cyrin for contributing to the patch, and Nick Pope for review.
Diffstat (limited to 'tests')
23 files changed, 123 insertions, 45 deletions
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."]) |
