diff options
| author | Joshua Cannon <joshua.cannon@ni.com> | 2019-01-04 14:03:53 -0600 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2019-01-30 13:44:10 -0500 |
| commit | 16a5a2a2c8d8dbf9cc3e033dd84b986bcaadb963 (patch) | |
| tree | a760f553b50f30f8d29b601cd81292997de83f5a | |
| parent | bae66e759faee8513da4b11d3fd16b044b415bdb (diff) | |
Fixed #30076 -- Added Model.get_FOO_display() even if field's choices are empty.
| -rw-r--r-- | django/db/models/fields/__init__.py | 18 | ||||
| -rw-r--r-- | tests/model_fields/models.py | 16 | ||||
| -rw-r--r-- | tests/model_fields/tests.py | 55 |
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) |
