summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoshua Cannon <joshua.cannon@ni.com>2019-01-04 14:03:53 -0600
committerTim Graham <timograham@gmail.com>2019-01-30 13:44:10 -0500
commit16a5a2a2c8d8dbf9cc3e033dd84b986bcaadb963 (patch)
treea760f553b50f30f8d29b601cd81292997de83f5a
parentbae66e759faee8513da4b11d3fd16b044b415bdb (diff)
Fixed #30076 -- Added Model.get_FOO_display() even if field's choices are empty.
-rw-r--r--django/db/models/fields/__init__.py18
-rw-r--r--tests/model_fields/models.py16
-rw-r--r--tests/model_fields/tests.py55
3 files changed, 80 insertions, 9 deletions
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index d0a1324c79..f32ce64629 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -153,7 +153,7 @@ class Field(RegisterLookupMixin):
self.unique_for_year = unique_for_year
if isinstance(choices, collections.abc.Iterator):
choices = list(choices)
- self.choices = choices or []
+ self.choices = choices
self.help_text = help_text
self.db_index = db_index
self.db_column = db_column
@@ -443,7 +443,7 @@ class Field(RegisterLookupMixin):
"unique_for_date": None,
"unique_for_month": None,
"unique_for_year": None,
- "choices": [],
+ "choices": None,
"help_text": '',
"db_column": None,
"db_tablespace": None,
@@ -598,7 +598,7 @@ class Field(RegisterLookupMixin):
# Skip validation for non-editable fields.
return
- if self.choices and value not in self.empty_values:
+ if self.choices is not None and value not in self.empty_values:
for option_key, option_value in self.choices:
if isinstance(option_value, (list, tuple)):
# This is an optgroup, so look inside the group for
@@ -742,7 +742,7 @@ class Field(RegisterLookupMixin):
# such fields can't be deferred (we don't have a check for this).
if not getattr(cls, self.attname, None):
setattr(cls, self.attname, DeferredAttribute(self.attname))
- if self.choices:
+ if self.choices is not None:
setattr(cls, 'get_%s_display' % self.name,
partialmethod(cls._get_FIELD_display, field=self))
@@ -812,7 +812,7 @@ class Field(RegisterLookupMixin):
Return choices with a default blank choices included, for use
as <select> choices for this field.
"""
- if self.choices:
+ if self.choices is not None:
choices = list(self.choices)
if include_blank:
blank_defined = any(choice in ('', None) for choice, _ in self.flatchoices)
@@ -840,6 +840,8 @@ class Field(RegisterLookupMixin):
def _get_flatchoices(self):
"""Flattened version of choices tuple."""
+ if self.choices is None:
+ return []
flat = []
for choice, value in self.choices:
if isinstance(value, (list, tuple)):
@@ -865,7 +867,7 @@ class Field(RegisterLookupMixin):
defaults['show_hidden_initial'] = True
else:
defaults['initial'] = self.get_default()
- if self.choices:
+ if self.choices is not None:
# Fields with choices get special treatment.
include_blank = (self.blank or
not (self.has_default() or 'initial' in kwargs))
@@ -1018,7 +1020,7 @@ class BooleanField(Field):
return self.to_python(value)
def formfield(self, **kwargs):
- if self.choices:
+ if self.choices is not None:
include_blank = not (self.has_default() or 'initial' in kwargs)
defaults = {'choices': self.get_choices(include_blank=include_blank)}
else:
@@ -2080,7 +2082,7 @@ class TextField(Field):
# the value in the form field (to pass into widget for example).
return super().formfield(**{
'max_length': self.max_length,
- **({} if self.choices else {'widget': forms.Textarea}),
+ **({} if self.choices is not None else {'widget': forms.Textarea}),
**kwargs,
})
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index 13d4843632..02fea36b31 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -50,6 +50,14 @@ class Whiz(models.Model):
c = models.IntegerField(choices=CHOICES, null=True)
+class WhizDelayed(models.Model):
+ c = models.IntegerField(choices=(), null=True)
+
+
+# Contrived way of adding choices later.
+WhizDelayed._meta.get_field('c').choices = Whiz.CHOICES
+
+
class WhizIter(models.Model):
c = models.IntegerField(choices=iter(Whiz.CHOICES), null=True)
@@ -58,6 +66,14 @@ class WhizIterEmpty(models.Model):
c = models.CharField(choices=iter(()), blank=True, max_length=1)
+class Choiceful(models.Model):
+ no_choices = models.IntegerField(null=True)
+ empty_choices = models.IntegerField(choices=(), null=True)
+ with_choices = models.IntegerField(choices=[(1, 'A')], null=True)
+ empty_choices_bool = models.BooleanField(choices=())
+ empty_choices_text = models.TextField(choices=())
+
+
class BigD(models.Model):
d = models.DecimalField(max_digits=32, decimal_places=30)
diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py
index 42e784db54..f55175aa9e 100644
--- a/tests/model_fields/tests.py
+++ b/tests/model_fields/tests.py
@@ -1,12 +1,14 @@
import pickle
from django import forms
+from django.core.exceptions import ValidationError
from django.db import models
from django.test import SimpleTestCase, TestCase
from django.utils.functional import lazy
from .models import (
- Bar, Foo, RenamedField, VerboseNameField, Whiz, WhizIter, WhizIterEmpty,
+ Bar, Choiceful, Foo, RenamedField, VerboseNameField, Whiz, WhizDelayed,
+ WhizIter, WhizIterEmpty,
)
@@ -103,6 +105,51 @@ class BasicFieldTests(SimpleTestCase):
class ChoicesTests(SimpleTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.no_choices = Choiceful._meta.get_field('no_choices')
+ cls.empty_choices = Choiceful._meta.get_field('empty_choices')
+ cls.empty_choices_bool = Choiceful._meta.get_field('empty_choices_bool')
+ cls.empty_choices_text = Choiceful._meta.get_field('empty_choices_text')
+ cls.with_choices = Choiceful._meta.get_field('with_choices')
+
+ def test_choices(self):
+ self.assertIsNone(self.no_choices.choices)
+ self.assertEqual(self.empty_choices.choices, ())
+ self.assertEqual(self.with_choices.choices, [(1, 'A')])
+
+ def test_flatchoices(self):
+ self.assertEqual(self.no_choices.flatchoices, [])
+ self.assertEqual(self.empty_choices.flatchoices, [])
+ self.assertEqual(self.with_choices.flatchoices, [(1, 'A')])
+
+ def test_check(self):
+ self.assertEqual(Choiceful.check(), [])
+
+ def test_invalid_choice(self):
+ model_instance = None # Actual model instance not needed.
+ self.no_choices.validate(0, model_instance)
+ msg = "['Value 99 is not a valid choice.']"
+ with self.assertRaisesMessage(ValidationError, msg):
+ self.empty_choices.validate(99, model_instance)
+ with self.assertRaisesMessage(ValidationError, msg):
+ self.with_choices.validate(99, model_instance)
+
+ def test_formfield(self):
+ no_choices_formfield = self.no_choices.formfield()
+ self.assertIsInstance(no_choices_formfield, forms.IntegerField)
+ fields = (
+ self.empty_choices, self.with_choices, self.empty_choices_bool,
+ self.empty_choices_text,
+ )
+ for field in fields:
+ with self.subTest(field=field):
+ self.assertIsInstance(field.formfield(), forms.ChoiceField)
+
+
+class GetFieldDisplayTests(SimpleTestCase):
+
def test_choices_and_field_display(self):
"""
get_choices() interacts with get_FIELD_display() to return the expected
@@ -113,6 +160,7 @@ class ChoicesTests(SimpleTestCase):
self.assertEqual(Whiz(c=9).get_c_display(), 9) # Invalid value
self.assertIsNone(Whiz(c=None).get_c_display()) # Blank value
self.assertEqual(Whiz(c='').get_c_display(), '') # Empty value
+ self.assertEqual(WhizDelayed(c=0).get_c_display(), 'Other') # Delayed choices
def test_iterator_choices(self):
"""
@@ -135,6 +183,11 @@ class ChoicesTests(SimpleTestCase):
class GetChoicesTests(SimpleTestCase):
+ def test_empty_choices(self):
+ choices = []
+ f = models.CharField(choices=choices)
+ self.assertEqual(f.get_choices(include_blank=False), choices)
+
def test_blank_in_choices(self):
choices = [('', '<><>'), ('a', 'A')]
f = models.CharField(choices=choices)