summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/db/models/fields/__init__.py68
-rw-r--r--tests/invalid_models_tests/test_ordinary_fields.py38
2 files changed, 84 insertions, 22 deletions
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')