diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py index 896d59155c..f90793daec 100644 --- a/django/contrib/admin/checks.py +++ b/django/contrib/admin/checks.py @@ -130,6 +130,8 @@ class BaseModelAdminChecks(object): id='admin.E011', ) ] + elif not isinstance(fieldset[1]['fields'], (list, tuple)): + return must_be('a list or tuple', option="fieldsets[1]['fields']", obj=cls, id='admin.E008') fields = flatten(fieldset[1]['fields']) if len(fields) != len(set(fields)): diff --git a/tests/admin_checks/tests.py b/tests/admin_checks/tests.py index d8e6c125dd..ea0151cced 100644 --- a/tests/admin_checks/tests.py +++ b/tests/admin_checks/tests.py @@ -124,6 +124,55 @@ class SystemChecksTestCase(TestCase): errors = ValidFormFieldsets.check(model=Song) self.assertEqual(errors, []) + def test_fieldsets_fields_non_tuple(self): + """ + Tests for a tuple/list within fieldsets[1]['fields']. + """ + class NotATupleAdmin(admin.ModelAdmin): + list_display = ["pk", "title"] + list_editable = ["title"] + fieldsets = [ + (None, { + "fields": "title" # not a tuple + }), + ] + + errors = NotATupleAdmin.check(model=Song) + expected = [ + checks.Error( + "The value of 'fieldsets[1]['fields']' must be a list or tuple.", + hint=None, + obj=NotATupleAdmin, + id='admin.E008', + ) + ] + self.assertEqual(errors, expected) + + def test_nonfirst_fieldset(self): + """ + Tests for a tuple/list within the second fieldsets[2]['fields']. + """ + class NotATupleAdmin(admin.ModelAdmin): + fieldsets = [ + (None, { + "fields": ("title",) + }), + ('foo', { + "fields": "author" # not a tuple + }), + ] + + errors = NotATupleAdmin.check(model=Song) + expected = [ + checks.Error( + "The value of 'fieldsets[1]['fields']' must be a list or tuple.", + hint=None, + obj=NotATupleAdmin, + id='admin.E008', + ) + ] + self.assertEqual(errors, expected) + def test_exclude_values(self): """ Tests for basic system checks of 'exclude' option values (#12689)