mirror of https://github.com/django/django.git
Fixed #30076 -- Added Model.get_FOO_display() even if field's choices are empty.
This commit is contained in:
parent
bae66e759f
commit
16a5a2a2c8
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue