diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 6c202c8e613c..9a32d4e0d4f6 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -414,11 +414,35 @@ def get_ordering(self, request): """ return self.ordering or () # otherwise we might try to *None, which is bad ;) + def _get_readonly_manual_pk_fields(self, obj): + """ + Returns a list of primary key field names that should be readonly if: + - we're editing an existing object, + - the PK was manually defined (not auto-created), + - and it's not already in self.readonly_fields. + Supports both single and composite PKs. + """ + if not obj or hasattr(self, "parent_model"): + return [] + + readonly = [] + pk_fields = self.model._meta.pk_fields + + for pk in pk_fields: + if ( + pk.editable + and not pk.auto_created + and pk.name not in self.readonly_fields + ): + readonly.append(pk.name) + + return readonly + def get_readonly_fields(self, request, obj=None): """ Hook for specifying custom readonly fields. """ - return self.readonly_fields + return list(self.readonly_fields) + self._get_readonly_manual_pk_fields(obj) def get_prepopulated_fields(self, request, obj=None): """ @@ -2549,6 +2573,18 @@ def has_view_permission(self, request, obj=None): return self._has_any_perms_for_target_model(request, ["view", "change"]) return super().has_view_permission(request) + def get_readonly_fields(self, request, obj=None): + """ + Make manually specified (non-auto-created) primary key fields readonly + when editing an existing inline object. + """ + readonly = list(self.readonly_fields) + if obj: + for pk in self.model._meta.pk_fields: + if pk.editable and not pk.auto_created and pk.name not in readonly: + readonly.append(pk.name) + return readonly + class StackedInline(InlineModelAdmin): template = "admin/edit_inline/stacked.html" diff --git a/tests/admin_custom_urls/tests.py b/tests/admin_custom_urls/tests.py index d401976ebb40..6eda92b33b71 100644 --- a/tests/admin_custom_urls/tests.py +++ b/tests/admin_custom_urls/tests.py @@ -95,7 +95,11 @@ def test_admin_URLs_no_clash(self): ) response = self.client.get(url) self.assertContains(response, "Change action") - self.assertContains(response, 'value="path/to/html/document.html"') + self.assertContains( + response, + '