Fixed #30076 -- Added Model.get_FOO_display() even if field's choices are empty.

This commit is contained in:
Joshua Cannon 2019-01-04 14:03:53 -06:00 committed by Tim Graham
parent bae66e759f
commit 16a5a2a2c8
3 changed files with 80 additions and 9 deletions

View File

@ -153,7 +153,7 @@ class Field(RegisterLookupMixin):
self.unique_for_year = unique_for_year self.unique_for_year = unique_for_year
if isinstance(choices, collections.abc.Iterator): if isinstance(choices, collections.abc.Iterator):
choices = list(choices) choices = list(choices)
self.choices = choices or [] self.choices = choices
self.help_text = help_text self.help_text = help_text
self.db_index = db_index self.db_index = db_index
self.db_column = db_column self.db_column = db_column
@ -443,7 +443,7 @@ class Field(RegisterLookupMixin):
"unique_for_date": None, "unique_for_date": None,
"unique_for_month": None, "unique_for_month": None,
"unique_for_year": None, "unique_for_year": None,
"choices": [], "choices": None,
"help_text": '', "help_text": '',
"db_column": None, "db_column": None,
"db_tablespace": None, "db_tablespace": None,
@ -598,7 +598,7 @@ class Field(RegisterLookupMixin):
# Skip validation for non-editable fields. # Skip validation for non-editable fields.
return 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: for option_key, option_value in self.choices:
if isinstance(option_value, (list, tuple)): if isinstance(option_value, (list, tuple)):
# This is an optgroup, so look inside the group for # 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). # such fields can't be deferred (we don't have a check for this).
if not getattr(cls, self.attname, None): if not getattr(cls, self.attname, None):
setattr(cls, self.attname, DeferredAttribute(self.attname)) setattr(cls, self.attname, DeferredAttribute(self.attname))
if self.choices: if self.choices is not None:
setattr(cls, 'get_%s_display' % self.name, setattr(cls, 'get_%s_display' % self.name,
partialmethod(cls._get_FIELD_display, field=self)) partialmethod(cls._get_FIELD_display, field=self))
@ -812,7 +812,7 @@ class Field(RegisterLookupMixin):
Return choices with a default blank choices included, for use Return choices with a default blank choices included, for use
as <select> choices for this field. as <select> choices for this field.
""" """
if self.choices: if self.choices is not None:
choices = list(self.choices) choices = list(self.choices)
if include_blank: if include_blank:
blank_defined = any(choice in ('', None) for choice, _ in self.flatchoices) blank_defined = any(choice in ('', None) for choice, _ in self.flatchoices)
@ -840,6 +840,8 @@ class Field(RegisterLookupMixin):
def _get_flatchoices(self): def _get_flatchoices(self):
"""Flattened version of choices tuple.""" """Flattened version of choices tuple."""
if self.choices is None:
return []
flat = [] flat = []
for choice, value in self.choices: for choice, value in self.choices:
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
@ -865,7 +867,7 @@ class Field(RegisterLookupMixin):
defaults['show_hidden_initial'] = True defaults['show_hidden_initial'] = True
else: else:
defaults['initial'] = self.get_default() defaults['initial'] = self.get_default()
if self.choices: if self.choices is not None:
# Fields with choices get special treatment. # Fields with choices get special treatment.
include_blank = (self.blank or include_blank = (self.blank or
not (self.has_default() or 'initial' in kwargs)) not (self.has_default() or 'initial' in kwargs))
@ -1018,7 +1020,7 @@ class BooleanField(Field):
return self.to_python(value) return self.to_python(value)
def formfield(self, **kwargs): def formfield(self, **kwargs):
if self.choices: if self.choices is not None:
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:
@ -2080,7 +2082,7 @@ class TextField(Field):
# the value in the form field (to pass into widget for example). # the value in the form field (to pass into widget for example).
return super().formfield(**{ return super().formfield(**{
'max_length': self.max_length, 'max_length': self.max_length,
**({} if self.choices else {'widget': forms.Textarea}), **({} if self.choices is not None else {'widget': forms.Textarea}),
**kwargs, **kwargs,
}) })

View File

@ -50,6 +50,14 @@ class Whiz(models.Model):
c = models.IntegerField(choices=CHOICES, null=True) 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): class WhizIter(models.Model):
c = models.IntegerField(choices=iter(Whiz.CHOICES), null=True) 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) 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): class BigD(models.Model):
d = models.DecimalField(max_digits=32, decimal_places=30) d = models.DecimalField(max_digits=32, decimal_places=30)

View File

@ -1,12 +1,14 @@
import pickle import pickle
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase
from django.utils.functional import lazy from django.utils.functional import lazy
from .models import ( 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): 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): def test_choices_and_field_display(self):
""" """
get_choices() interacts with get_FIELD_display() to return the expected 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.assertEqual(Whiz(c=9).get_c_display(), 9) # Invalid value
self.assertIsNone(Whiz(c=None).get_c_display()) # Blank value self.assertIsNone(Whiz(c=None).get_c_display()) # Blank value
self.assertEqual(Whiz(c='').get_c_display(), '') # Empty 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): def test_iterator_choices(self):
""" """
@ -135,6 +183,11 @@ class ChoicesTests(SimpleTestCase):
class GetChoicesTests(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): def test_blank_in_choices(self):
choices = [('', '<><>'), ('a', 'A')] choices = [('', '<><>'), ('a', 'A')]
f = models.CharField(choices=choices) f = models.CharField(choices=choices)