Fixed #20895 -- Made check management command warn if a BooleanField does not have a default value

Thanks to Collin Anderson for the suggestion and Tim Graham for
reviewing the patch.
This commit is contained in:
Alasdair Nicol 2013-08-11 21:19:09 +01:00 committed by Tim Graham
parent 55339a7669
commit 22c6497f99
23 changed files with 111 additions and 42 deletions

View File

@ -442,6 +442,7 @@ answer newbie questions, and generally made Django that much better:
Gopal Narayanan <gopastro@gmail.com> Gopal Narayanan <gopastro@gmail.com>
Fraser Nevett <mail@nevett.org> Fraser Nevett <mail@nevett.org>
Sam Newman <http://www.magpiebrain.com/> Sam Newman <http://www.magpiebrain.com/>
Alasdair Nicol <http://al.sdair.co.uk/>
Ryan Niemeyer <https://profiles.google.com/ryan.niemeyer/about> Ryan Niemeyer <https://profiles.google.com/ryan.niemeyer/about>
Filip Noetzel <http://filip.noetzel.co.uk/> Filip Noetzel <http://filip.noetzel.co.uk/>
Afonso Fernández Nogueira <fonzzo.django@gmail.com> Afonso Fernández Nogueira <fonzzo.django@gmail.com>

View File

@ -40,7 +40,7 @@ class Track(models.Model):
def __str__(self): return self.name def __str__(self): return self.name
class Truth(models.Model): class Truth(models.Model):
val = models.BooleanField() val = models.BooleanField(default=False)
objects = models.GeoManager() objects = models.GeoManager()
if not spatialite: if not spatialite:

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models
def check_test_runner(): def check_test_runner():
""" """
@ -24,6 +25,31 @@ def check_test_runner():
] ]
return ' '.join(message) return ' '.join(message)
def check_boolean_field_default_value():
"""
Checks if there are any BooleanFields without a default value, &
warns the user that the default has changed from False to Null.
"""
fields = []
for cls in models.get_models():
opts = cls._meta
for f in opts.local_fields:
if isinstance(f, models.BooleanField) and not f.has_default():
fields.append(
'%s.%s: "%s"' % (opts.app_label, opts.object_name, f.name)
)
if fields:
fieldnames = ", ".join(fields)
message = [
"You have not set a default value for one or more BooleanFields:",
"%s." % fieldnames,
"In Django 1.6 the default value of BooleanField was changed from",
"False to Null when Field.default isn't defined. See",
"https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield"
"for more information."
]
return ' '.join(message)
def run_checks(): def run_checks():
""" """
@ -31,7 +57,8 @@ def run_checks():
messages from all the relevant check functions for this version of Django. messages from all the relevant check functions for this version of Django.
""" """
checks = [ checks = [
check_test_runner() check_test_runner(),
check_boolean_field_default_value(),
] ]
# Filter out the ``None`` or empty strings. # Filter out the ``None`` or empty strings.
return [output for output in checks if output] return [output for output in checks if output]

View File

@ -115,7 +115,7 @@ class ModelWithStringPrimaryKey(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Color(models.Model): class Color(models.Model):
value = models.CharField(max_length=10) value = models.CharField(max_length=10)
warm = models.BooleanField() warm = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.value return self.value
@ -144,7 +144,7 @@ class Actor(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Inquisition(models.Model): class Inquisition(models.Model):
expected = models.BooleanField() expected = models.BooleanField(default=False)
leader = models.ForeignKey(Actor) leader = models.ForeignKey(Actor)
country = models.CharField(max_length=20) country = models.CharField(max_length=20)
@ -376,7 +376,7 @@ class Link(models.Model):
class PrePopulatedPost(models.Model): class PrePopulatedPost(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
published = models.BooleanField() published = models.BooleanField(default=False)
slug = models.SlugField() slug = models.SlugField()
@ -607,7 +607,7 @@ class PrePopulatedPostLargeSlug(models.Model):
the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000) the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000)
""" """
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
published = models.BooleanField() published = models.BooleanField(default=False)
slug = models.SlugField(max_length=1000) slug = models.SlugField(max_length=1000)
class AdminOrderedField(models.Model): class AdminOrderedField(models.Model):

View File

@ -64,7 +64,7 @@ class Store(models.Model):
class Entries(models.Model): class Entries(models.Model):
EntryID = models.AutoField(primary_key=True, db_column='Entry ID') EntryID = models.AutoField(primary_key=True, db_column='Entry ID')
Entry = models.CharField(unique=True, max_length=50) Entry = models.CharField(unique=True, max_length=50)
Exclude = models.BooleanField() Exclude = models.BooleanField(default=False)
class Clues(models.Model): class Clues(models.Model):

View File

@ -1 +1,9 @@
# Stubby. from django.db import models
class Book(models.Model):
title = models.CharField(max_length=250)
is_published = models.BooleanField(default=False)
class BlogPost(models.Model):
title = models.CharField(max_length=250)
is_published = models.BooleanField(default=False)

View File

@ -2,8 +2,10 @@ from django.core.checks.compatibility import base
from django.core.checks.compatibility import django_1_6_0 from django.core.checks.compatibility import django_1_6_0
from django.core.management.commands import check from django.core.management.commands import check
from django.core.management import call_command from django.core.management import call_command
from django.db.models.fields import NOT_PROVIDED
from django.test import TestCase from django.test import TestCase
from .models import Book
class StubCheckModule(object): class StubCheckModule(object):
# Has no ``run_checks`` attribute & will trigger a warning. # Has no ``run_checks`` attribute & will trigger a warning.
@ -53,6 +55,24 @@ class CompatChecksTestCase(TestCase):
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
self.assertEqual(len(django_1_6_0.run_checks()), 0) self.assertEqual(len(django_1_6_0.run_checks()), 0)
def test_boolean_field_default_value(self):
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
# We patch the field's default value to trigger the warning
boolean_field = Book._meta.get_field('is_published')
old_default = boolean_field.default
try:
boolean_field.default = NOT_PROVIDED
result = django_1_6_0.run_checks()
self.assertEqual(len(result), 1)
self.assertTrue("You have not set a default value for one or more BooleanFields" in result[0])
self.assertTrue('check.Book: "is_published"' in result[0])
# We did not patch the BlogPost.is_published field so
# there should not be a warning about it
self.assertFalse('check.BlogPost' in result[0])
finally:
# Restore the ``default``
boolean_field.default = old_default
def test_check_compatibility(self): def test_check_compatibility(self):
with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
result = base.check_compatibility() result = base.check_compatibility()

View File

@ -30,7 +30,7 @@ class Entry(models.Model):
title = models.CharField(max_length=250) title = models.CharField(max_length=250)
body = models.TextField() body = models.TextField()
pub_date = models.DateField() pub_date = models.DateField()
enable_comments = models.BooleanField() enable_comments = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.title return self.title

View File

@ -67,7 +67,7 @@ CustomManager = BaseCustomManager.from_queryset(CustomQuerySet)
class Person(models.Model): class Person(models.Model):
first_name = models.CharField(max_length=30) first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30)
fun = models.BooleanField() fun = models.BooleanField(default=False)
objects = PersonManager() objects = PersonManager()
custom_queryset_default_manager = CustomQuerySet.as_manager() custom_queryset_default_manager = CustomQuerySet.as_manager()
@ -80,7 +80,7 @@ class Person(models.Model):
class Book(models.Model): class Book(models.Model):
title = models.CharField(max_length=50) title = models.CharField(max_length=50)
author = models.CharField(max_length=30) author = models.CharField(max_length=30)
is_published = models.BooleanField() is_published = models.BooleanField(default=False)
published_objects = PublishedBookManager() published_objects = PublishedBookManager()
authors = models.ManyToManyField(Person, related_name='books') authors = models.ManyToManyField(Person, related_name='books')

View File

@ -92,7 +92,7 @@ class GeckoManager(models.Manager):
return super(GeckoManager, self).get_queryset().filter(has_tail=True) return super(GeckoManager, self).get_queryset().filter(has_tail=True)
class Gecko(models.Model): class Gecko(models.Model):
has_tail = models.BooleanField() has_tail = models.BooleanField(default=False)
objects = GeckoManager() objects = GeckoManager()
# To test fix for #11263 # To test fix for #11263

View File

@ -37,7 +37,7 @@ class SpecialColumnName(models.Model):
class ColumnTypes(models.Model): 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() bool_field = models.BooleanField(default=False)
null_bool_field = models.NullBooleanField() null_bool_field = models.NullBooleanField()
char_field = models.CharField(max_length=10) char_field = models.CharField(max_length=10)
comma_separated_int_field = models.CommaSeparatedIntegerField(max_length=99) comma_separated_int_field = models.CommaSeparatedIntegerField(max_length=99)

View File

@ -58,7 +58,7 @@ class NullBooleanModel(models.Model):
nbfield = models.NullBooleanField() nbfield = models.NullBooleanField()
class BooleanModel(models.Model): class BooleanModel(models.Model):
bfield = models.BooleanField() bfield = models.BooleanField(default=None)
string = models.CharField(max_length=10, default='abc') string = models.CharField(max_length=10, default='abc')
class FksToBooleans(models.Model): class FksToBooleans(models.Model):
@ -72,7 +72,7 @@ class RenamedField(models.Model):
class VerboseNameField(models.Model): class VerboseNameField(models.Model):
id = models.AutoField("verbose pk", primary_key=True) id = models.AutoField("verbose pk", primary_key=True)
field1 = models.BigIntegerField("verbose field1") field1 = models.BigIntegerField("verbose field1")
field2 = models.BooleanField("verbose field2") field2 = models.BooleanField("verbose field2", default=False)
field3 = models.CharField("verbose field3", max_length=10) field3 = models.CharField("verbose field3", max_length=10)
field4 = models.CommaSeparatedIntegerField("verbose field4", max_length=99) field4 = models.CommaSeparatedIntegerField("verbose field4", max_length=99)
field5 = models.DateField("verbose field5") field5 = models.DateField("verbose field5")

View File

@ -12,7 +12,7 @@ from django.db.models.fields import (
AutoField, BigIntegerField, BinaryField, BooleanField, CharField, AutoField, BigIntegerField, BinaryField, BooleanField, CharField,
CommaSeparatedIntegerField, DateField, DateTimeField, DecimalField, CommaSeparatedIntegerField, DateField, DateTimeField, DecimalField,
EmailField, FilePathField, FloatField, IntegerField, IPAddressField, EmailField, FilePathField, FloatField, IntegerField, IPAddressField,
GenericIPAddressField, NullBooleanField, PositiveIntegerField, GenericIPAddressField, NOT_PROVIDED, NullBooleanField, PositiveIntegerField,
PositiveSmallIntegerField, SlugField, SmallIntegerField, TextField, PositiveSmallIntegerField, SlugField, SmallIntegerField, TextField,
TimeField, URLField) TimeField, URLField)
from django.db.models.fields.files import FileField, ImageField from django.db.models.fields.files import FileField, ImageField
@ -275,10 +275,23 @@ class BooleanFieldTests(unittest.TestCase):
Check that a BooleanField defaults to None -- which isn't Check that a BooleanField defaults to None -- which isn't
a valid value (#15124). a valid value (#15124).
""" """
b = BooleanModel() # Patch the boolean field's default value. We give it a default
self.assertIsNone(b.bfield) # value when defining the model to satisfy the check tests
with self.assertRaises(IntegrityError): # #20895.
b.save() boolean_field = BooleanModel._meta.get_field('bfield')
self.assertTrue(boolean_field.has_default())
old_default = boolean_field.default
try:
boolean_field.default = NOT_PROVIDED
# check patch was succcessful
self.assertFalse(boolean_field.has_default())
b = BooleanModel()
self.assertIsNone(b.bfield)
with self.assertRaises(IntegrityError):
b.save()
finally:
boolean_field.default = old_default
nb = NullBooleanModel() nb = NullBooleanModel()
self.assertIsNone(nb.nbfield) self.assertIsNone(nb.nbfield)
nb.save() # no error nb.save() # no error

View File

@ -117,7 +117,7 @@ class OwnerProfile(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Restaurant(Place): class Restaurant(Place):
serves_pizza = models.BooleanField() serves_pizza = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.name return self.name
@ -141,11 +141,11 @@ class Price(models.Model):
unique_together = (('price', 'quantity'),) unique_together = (('price', 'quantity'),)
class MexicanRestaurant(Restaurant): class MexicanRestaurant(Restaurant):
serves_tacos = models.BooleanField() serves_tacos = models.BooleanField(default=False)
class ClassyMexicanRestaurant(MexicanRestaurant): class ClassyMexicanRestaurant(MexicanRestaurant):
restaurant = models.OneToOneField(MexicanRestaurant, parent_link=True, primary_key=True) restaurant = models.OneToOneField(MexicanRestaurant, parent_link=True, primary_key=True)
tacos_are_yummy = models.BooleanField() tacos_are_yummy = models.BooleanField(default=False)
# models for testing unique_together validation when a fk is involved and # models for testing unique_together validation when a fk is involved and
# using inlineformset_factory. # using inlineformset_factory.

View File

@ -63,7 +63,7 @@ class Attachment(models.Model):
return self.content return self.content
class Comment(Attachment): class Comment(Attachment):
is_spam = models.BooleanField() is_spam = models.BooleanField(default=False)
class Link(Attachment): class Link(Attachment):
url = models.URLField() url = models.URLField()
@ -96,8 +96,8 @@ class Rating(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Restaurant(Place, Rating): class Restaurant(Place, Rating):
serves_hot_dogs = models.BooleanField() serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField() serves_pizza = models.BooleanField(default=False)
chef = models.ForeignKey(Chef, null=True, blank=True) chef = models.ForeignKey(Chef, null=True, blank=True)
class Meta(Rating.Meta): class Meta(Rating.Meta):
@ -108,7 +108,7 @@ class Restaurant(Place, Rating):
@python_2_unicode_compatible @python_2_unicode_compatible
class ItalianRestaurant(Restaurant): class ItalianRestaurant(Restaurant):
serves_gnocchi = models.BooleanField() serves_gnocchi = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s the italian restaurant" % self.name return "%s the italian restaurant" % self.name

View File

@ -18,15 +18,15 @@ class Place(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Restaurant(Place): class Restaurant(Place):
serves_hot_dogs = models.BooleanField() serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField() serves_pizza = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s the restaurant" % self.name return "%s the restaurant" % self.name
@python_2_unicode_compatible @python_2_unicode_compatible
class ItalianRestaurant(Restaurant): class ItalianRestaurant(Restaurant):
serves_gnocchi = models.BooleanField() serves_gnocchi = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s the italian restaurant" % self.name return "%s the italian restaurant" % self.name
@ -184,7 +184,7 @@ class Station(SearchableLocation):
class BusStation(Station): class BusStation(Station):
bus_routes = models.CommaSeparatedIntegerField(max_length=128) bus_routes = models.CommaSeparatedIntegerField(max_length=128)
inbound = models.BooleanField() inbound = models.BooleanField(default=False)
class TrainStation(Station): class TrainStation(Station):
zone = models.IntegerField() zone = models.IntegerField()

View File

@ -20,8 +20,8 @@ class Place(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Restaurant(Place): class Restaurant(Place):
serves_sushi = models.BooleanField() serves_sushi = models.BooleanField(default=False)
serves_steak = models.BooleanField() serves_steak = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s the restaurant" % self.name return "%s the restaurant" % self.name

View File

@ -32,7 +32,7 @@ class ValidationTestModel(models.Model):
slug = models.SlugField() slug = models.SlugField()
users = models.ManyToManyField(User) users = models.ManyToManyField(User)
state = models.CharField(max_length=2, choices=(("CO", "Colorado"), ("WA", "Washington"))) state = models.CharField(max_length=2, choices=(("CO", "Colorado"), ("WA", "Washington")))
is_active = models.BooleanField() is_active = models.BooleanField(default=False)
pub_date = models.DateTimeField() pub_date = models.DateTimeField()
band = models.ForeignKey(Band) band = models.ForeignKey(Band)
no = models.IntegerField(verbose_name="Number", blank=True, null=True) # This field is intentionally 2 characters long. See #16080. no = models.IntegerField(verbose_name="Number", blank=True, null=True) # This field is intentionally 2 characters long. See #16080.

View File

@ -22,8 +22,8 @@ class Place(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Restaurant(models.Model): class Restaurant(models.Model):
place = models.OneToOneField(Place, primary_key=True) place = models.OneToOneField(Place, primary_key=True)
serves_hot_dogs = models.BooleanField() serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField() serves_pizza = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s the restaurant" % self.place.name return "%s the restaurant" % self.place.name

View File

@ -15,8 +15,8 @@ class Place(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class Restaurant(models.Model): class Restaurant(models.Model):
place = models.OneToOneField(Place) place = models.OneToOneField(Place)
serves_hot_dogs = models.BooleanField() serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField() serves_pizza = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "%s the restaurant" % self.place.name return "%s the restaurant" % self.place.name

View File

@ -18,7 +18,7 @@ class Author(models.Model):
class Book(models.Model): class Book(models.Model):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
author = models.ForeignKey(Author) author = models.ForeignKey(Author)
paperback = models.BooleanField() paperback = models.BooleanField(default=False)
opening_line = models.TextField() opening_line = models.TextField()
class Coffee(models.Model): class Coffee(models.Model):

View File

@ -6,7 +6,7 @@ class SourceManager(models.Manager):
return super(SourceManager, self).get_queryset().filter(is_public=True) return super(SourceManager, self).get_queryset().filter(is_public=True)
class Source(models.Model): class Source(models.Model):
is_public = models.BooleanField() is_public = models.BooleanField(default=False)
objects = SourceManager() objects = SourceManager()
class Item(models.Model): class Item(models.Model):

View File

@ -16,7 +16,7 @@ class BinaryData(models.Model):
data = models.BinaryField(null=True) data = models.BinaryField(null=True)
class BooleanData(models.Model): class BooleanData(models.Model):
data = models.BooleanField() data = models.BooleanField(default=False)
class CharData(models.Model): class CharData(models.Model):
data = models.CharField(max_length=30, null=True) data = models.CharField(max_length=30, null=True)
@ -166,7 +166,7 @@ class Intermediate(models.Model):
# or all database backends. # or all database backends.
class BooleanPKData(models.Model): class BooleanPKData(models.Model):
data = models.BooleanField(primary_key=True) data = models.BooleanField(primary_key=True, default=False)
class CharPKData(models.Model): class CharPKData(models.Model):
data = models.CharField(max_length=30, primary_key=True) data = models.CharField(max_length=30, primary_key=True)