Selaa lähdekoodia

Fixed #28748 -- Made model field choices check more strict for named groups.

François Freitag 7 vuotta sitten
vanhempi
commit
f9844f4841

+ 46 - 22
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 [

+ 38 - 0
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')