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>
Fraser Nevett <mail@nevett.org>
Sam Newman <http://www.magpiebrain.com/>
Alasdair Nicol <http://al.sdair.co.uk/>
Ryan Niemeyer <https://profiles.google.com/ryan.niemeyer/about>
Filip Noetzel <http://filip.noetzel.co.uk/>
Afonso Fernández Nogueira <fonzzo.django@gmail.com>

View File

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

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
from django.db import models
def check_test_runner():
"""
@ -24,6 +25,31 @@ def check_test_runner():
]
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():
"""
@ -31,7 +57,8 @@ def run_checks():
messages from all the relevant check functions for this version of Django.
"""
checks = [
check_test_runner()
check_test_runner(),
check_boolean_field_default_value(),
]
# Filter out the ``None`` or empty strings.
return [output for output in checks if output]

View File

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

View File

@ -64,7 +64,7 @@ class Store(models.Model):
class Entries(models.Model):
EntryID = models.AutoField(primary_key=True, db_column='Entry ID')
Entry = models.CharField(unique=True, max_length=50)
Exclude = models.BooleanField()
Exclude = models.BooleanField(default=False)
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.management.commands import check
from django.core.management import call_command
from django.db.models.fields import NOT_PROVIDED
from django.test import TestCase
from .models import Book
class StubCheckModule(object):
# Has no ``run_checks`` attribute & will trigger a warning.
@ -53,6 +55,24 @@ class CompatChecksTestCase(TestCase):
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
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):
with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
result = base.check_compatibility()

View File

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

View File

@ -67,7 +67,7 @@ CustomManager = BaseCustomManager.from_queryset(CustomQuerySet)
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
fun = models.BooleanField()
fun = models.BooleanField(default=False)
objects = PersonManager()
custom_queryset_default_manager = CustomQuerySet.as_manager()
@ -80,7 +80,7 @@ class Person(models.Model):
class Book(models.Model):
title = models.CharField(max_length=50)
author = models.CharField(max_length=30)
is_published = models.BooleanField()
is_published = models.BooleanField(default=False)
published_objects = PublishedBookManager()
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)
class Gecko(models.Model):
has_tail = models.BooleanField()
has_tail = models.BooleanField(default=False)
objects = GeckoManager()
# To test fix for #11263

View File

@ -37,7 +37,7 @@ class SpecialColumnName(models.Model):
class ColumnTypes(models.Model):
id = models.AutoField(primary_key=True)
big_int_field = models.BigIntegerField()
bool_field = models.BooleanField()
bool_field = models.BooleanField(default=False)
null_bool_field = models.NullBooleanField()
char_field = models.CharField(max_length=10)
comma_separated_int_field = models.CommaSeparatedIntegerField(max_length=99)

View File

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

View File

@ -12,7 +12,7 @@ from django.db.models.fields import (
AutoField, BigIntegerField, BinaryField, BooleanField, CharField,
CommaSeparatedIntegerField, DateField, DateTimeField, DecimalField,
EmailField, FilePathField, FloatField, IntegerField, IPAddressField,
GenericIPAddressField, NullBooleanField, PositiveIntegerField,
GenericIPAddressField, NOT_PROVIDED, NullBooleanField, PositiveIntegerField,
PositiveSmallIntegerField, SlugField, SmallIntegerField, TextField,
TimeField, URLField)
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
a valid value (#15124).
"""
b = BooleanModel()
self.assertIsNone(b.bfield)
with self.assertRaises(IntegrityError):
b.save()
# Patch the boolean field's default value. We give it a default
# value when defining the model to satisfy the check tests
# #20895.
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()
self.assertIsNone(nb.nbfield)
nb.save() # no error

View File

@ -117,7 +117,7 @@ class OwnerProfile(models.Model):
@python_2_unicode_compatible
class Restaurant(Place):
serves_pizza = models.BooleanField()
serves_pizza = models.BooleanField(default=False)
def __str__(self):
return self.name
@ -141,11 +141,11 @@ class Price(models.Model):
unique_together = (('price', 'quantity'),)
class MexicanRestaurant(Restaurant):
serves_tacos = models.BooleanField()
serves_tacos = models.BooleanField(default=False)
class ClassyMexicanRestaurant(MexicanRestaurant):
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
# using inlineformset_factory.

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ class ValidationTestModel(models.Model):
slug = models.SlugField()
users = models.ManyToManyField(User)
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()
band = models.ForeignKey(Band)
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
class Restaurant(models.Model):
place = models.OneToOneField(Place, primary_key=True)
serves_hot_dogs = models.BooleanField()
serves_pizza = models.BooleanField()
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
def __str__(self):
return "%s the restaurant" % self.place.name

View File

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

View File

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

View File

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

View File

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