diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index da3fca1213..b511b01d17 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -241,31 +241,55 @@ class Field(RegisterLookupMixin): return [] def _check_choices(self): - if self.choices: - if isinstance(self.choices, str) or not is_iterable(self.choices): - return [ - checks.Error( - "'choices' must be an iterable (e.g., a list or tuple).", - obj=self, - id='fields.E004', - ) - ] - elif any(isinstance(choice, str) or - not is_iterable(choice) or len(choice) != 2 - for choice in self.choices): - return [ - checks.Error( - "'choices' must be an iterable containing " - "(actual value, human readable name) tuples.", - obj=self, - id='fields.E005', - ) - ] - else: - return [] + if not self.choices: + return [] + + def is_value(value): + return isinstance(value, str) or not is_iterable(value) + + if is_value(self.choices): + return [ + checks.Error( + "'choices' must be an iterable (e.g., a list or tuple).", + obj=self, + id='fields.E004', + ) + ] + + # Expect [group_name, [value, display]] + for choices_group in self.choices: + try: + group_name, group_choices = choices_group + except ValueError: + # Containing non-pairs + break + try: + if not all( + is_value(value) and is_value(human_name) + for value, human_name in group_choices + ): + break + except (TypeError, ValueError): + # No groups, choices in the form [value, display] + value, human_name = group_name, group_choices + if not is_value(value) or not is_value(human_name): + break + + # Special case: choices=['ab'] + if isinstance(choices_group, str): + break else: return [] + return [ + checks.Error( + "'choices' must be an iterable containing " + "(actual value, human readable name) tuples.", + obj=self, + id='fields.E005', + ) + ] + def _check_db_index(self): if self.db_index not in (None, True, False): return [ diff --git a/tests/invalid_models_tests/test_ordinary_fields.py b/tests/invalid_models_tests/test_ordinary_fields.py index 2c78d60924..75e6148f98 100644 --- a/tests/invalid_models_tests/test_ordinary_fields.py +++ b/tests/invalid_models_tests/test_ordinary_fields.py @@ -210,6 +210,44 @@ class CharFieldTests(TestCase): self.assertEqual(Model._meta.get_field('field').check(), []) + def test_choices_named_group_non_pairs(self): + class Model(models.Model): + field = models.CharField( + max_length=10, + choices=[['knights', [['L', 'Lancelot', 'Du Lac']]]], + ) + + field = Model._meta.get_field('field') + self.assertEqual(field.check(), [ + Error( + "'choices' must be an iterable containing (actual value, " + "human readable name) tuples.", + obj=field, + id='fields.E005', + ), + ]) + + def test_choices_named_group_bad_structure(self): + class Model(models.Model): + field = models.CharField( + max_length=10, choices=[ + ['knights', [ + ['Noble', [['G', 'Galahad']]], + ['Combative', [['L', 'Lancelot']]], + ]], + ], + ) + + field = Model._meta.get_field('field') + self.assertEqual(field.check(), [ + Error( + "'choices' must be an iterable containing (actual value, " + "human readable name) tuples.", + obj=field, + id='fields.E005', + ), + ]) + def test_bad_db_index_value(self): class Model(models.Model): field = models.CharField(max_length=10, db_index='bad')