From 380d77cccefbe185ddb3f9368d8fdeb7b7cf7108 Mon Sep 17 00:00:00 2001 From: Sean Helvey Date: Wed, 11 Feb 2026 15:07:40 -0800 Subject: Fixed #36921 -- Fixed KeyError in inline form for model not registered with admin. Regression in b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010. --- django/contrib/admin/options.py | 34 ++++++++++++++++++---------------- tests/admin_views/tests.py | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 9c787d2329..b67b023bd3 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1419,7 +1419,7 @@ class ModelAdmin(BaseModelAdmin): # Find the optgroup for the new item, if available source_model_name = request.POST.get(SOURCE_MODEL_VAR) - + source_admin = None if source_model_name: app_label, model_name = source_model_name.split(".", 1) try: @@ -1428,21 +1428,23 @@ class ModelAdmin(BaseModelAdmin): msg = _('The app "%s" could not be found.') % source_model_name self.message_user(request, msg, messages.ERROR) else: - source_admin = self.admin_site._registry[source_model] - form = source_admin.get_form(request)() - if self.opts.verbose_name_plural in form.fields: - field = form.fields[self.opts.verbose_name_plural] - for option_value, option_label in field.choices: - # Check if this is an optgroup (label is a sequence - # of choices rather than a single string value). - if isinstance(option_label, (list, tuple)): - # It's an optgroup: - # (group_name, [(value, label), ...]) - optgroup_label = option_value - for choice_value, choice_display in option_label: - if choice_display == str(obj): - popup_response["optgroup"] = str(optgroup_label) - break + source_admin = self.admin_site._registry.get(source_model) + + if source_admin: + form = source_admin.get_form(request)() + if self.opts.verbose_name_plural in form.fields: + field = form.fields[self.opts.verbose_name_plural] + for option_value, option_label in field.choices: + # Check if this is an optgroup (label is a sequence + # of choices rather than a single string value). + if isinstance(option_label, (list, tuple)): + # It's an optgroup: + # (group_name, [(value, label), ...]) + optgroup_label = option_value + for choice_value, choice_display in option_label: + if choice_display == str(obj): + popup_response["optgroup"] = str(optgroup_label) + break popup_response_data = json.dumps(popup_response) return TemplateResponse( diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 7f35d7c2f4..fa9d9a2dc6 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -589,6 +589,31 @@ class AdminViewBasicTest(AdminViewBasicTestCase): self.assertIn("admin_views.nonexistent", str(messages[0])) self.assertIn("could not be found", str(messages[0])) + def test_popup_add_POST_with_unregistered_source_model(self): + """ + Popup add where source_model is a valid Django model but is not + registered in the admin site (e.g. a model only used as an inline) + should succeed without raising a KeyError. + """ + post_data = { + IS_POPUP_VAR: "1", + # Chapter exists as a model but is not registered in site (only + # in site6), simulating a model used only as an inline. + SOURCE_MODEL_VAR: "admin_views.chapter", + "title": "Test Article", + "content": "some content", + "date_0": "2010-09-10", + "date_1": "14:55:39", + } + response = self.client.post(reverse("admin:admin_views_article_add"), post_data) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "data-popup-response") + # No error messages - unregistered model is silently skipped. + messages = list(response.wsgi_request._messages) + self.assertEqual(len(messages), 0) + # No optgroup in the response. + self.assertNotContains(response, ""optgroup"") + def test_basic_edit_POST(self): """ A smoke test to ensure POST on edit_view works. -- cgit v1.3