diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 910e6b68b9..9df4b7aadb 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -138,7 +138,11 @@ def cell_count(inline_admin_form): # Count all visible fields. for line in fieldset: for field in line: - if not field.field.is_hidden: + try: + is_hidden = field.field.is_hidden + except AttributeError: + is_hidden = field.field["is_hidden"] + if not is_hidden: count += 1 if inline_admin_form.formset.can_delete: # Delete checkbox diff --git a/docs/releases/4.0.3.txt b/docs/releases/4.0.3.txt index 17e9f65074..415e00d80f 100644 --- a/docs/releases/4.0.3.txt +++ b/docs/releases/4.0.3.txt @@ -15,3 +15,7 @@ Bugfixes * Prevented, following a regression in Django 4.0.1, :djadmin:`makemigrations` from generating infinite migrations for a model with ``ManyToManyField`` to a lowercased swappable model such as ``'auth.user'`` (:ticket:`33515`). + +* Fixed a regression in Django 4.0 that caused a crash when rendering invalid + inlines with :attr:`~django.contrib.admin.ModelAdmin.readonly_fields` in the + admin (:ticket:`33547`). diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py index 47c5b91828..a8d2ee02e1 100644 --- a/tests/admin_inlines/models.py +++ b/tests/admin_inlines/models.py @@ -5,6 +5,7 @@ import random from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError from django.db import models @@ -204,6 +205,9 @@ class Question(models.Model): text = models.CharField(max_length=40) poll = models.ForeignKey(Poll, models.CASCADE) + def clean(self): + raise ValidationError("Always invalid model.") + class Novel(models.Model): name = models.CharField(max_length=40) diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 3bd3c881d4..f61ed1aeb2 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -241,6 +241,22 @@ class TestInline(TestDataMixin, TestCase): # column cells self.assertContains(response, "

Callable in QuestionInline

") + def test_model_error_inline_with_readonly_field(self): + poll = Poll.objects.create(name="Test poll") + data = { + "question_set-TOTAL_FORMS": 1, + "question_set-INITIAL_FORMS": 0, + "question_set-MAX_NUM_FORMS": 0, + "_save": "Save", + "question_set-0-text": "Question", + "question_set-0-poll": poll.pk, + } + response = self.client.post( + reverse("admin:admin_inlines_poll_change", args=(poll.pk,)), + data, + ) + self.assertContains(response, "Always invalid model.") + def test_help_text(self): """ The inlines' model field help texts are displayed when using both the