Skip to content

Fixed #2259 -- Made manually specified primary keys readonly in the admin. #19675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fixed 2259 -- Made manually defined PKs readonly in admin inlines.
  • Loading branch information
Jkhall81 committed Jul 29, 2025
commit 21539731a574d119a11fe83a502bd89a3450b92e
41 changes: 25 additions & 16 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,27 +414,36 @@ def get_ordering(self, request):
"""
return self.ordering or () # otherwise we might try to *None, which is bad ;)

def get_readonly_fields(self, request, obj=None):
def _get_readonly_manual_pk_fields(self, obj):
"""
Hook for specifying custom readonly fields.
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.
"""
readonly = self.readonly_fields
pk_field = self.model._meta.pk
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)

# Only add PK to readonly if:
# - editing an existing object
# - PK is not auto-created
# - it's not already in the readonly list
# - and this is NOT an inline admin (which has `parent_model`)
if (
obj is not None
and not pk_field.auto_created
and pk_field.name not in readonly
and not hasattr(self, "parent_model")
):
readonly = readonly + (pk_field.name,)
return readonly

def get_readonly_fields(self, request, obj=None):
"""
Hook for specifying custom readonly fields.
"""
return list(self.readonly_fields) + self._get_readonly_manual_pk_fields(obj)

def get_prepopulated_fields(self, request, obj=None):
"""
Hook for specifying custom prepopulated fields.
Expand Down
4 changes: 0 additions & 4 deletions django/contrib/admin/static/admin/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,6 @@ body {
background: var(--body-bg);
}

label {
font-size: 1rem;
}

/* LINKS */

a:link, a:visited {
Expand Down
16 changes: 16 additions & 0 deletions tests/admin_inlines/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Child,
ChildModel1,
ChildModel2,
EditablePKBook,
ExtraTerrestrial,
Fashionista,
FootNote,
Expand Down Expand Up @@ -708,6 +709,21 @@ def test_inline_editable_pk(self):
count=1,
)

def test_inline_manual_pk_is_readonly_when_editing(self):
author = Author.objects.create(name="Jane Austen")
EditablePKBook.objects.create(
author=author, manual_pk=101, title="Pride and Prejudice"
)

response = self.client.get(
reverse("admin:admin_inlines_author_change", args=[author.pk])
)

self.assertContains(
response, 'name="editablepkbook_set-0-manual_pk"', html=False
)
self.assertContains(response, "readonly", html=False)

def test_stacked_inline_edit_form_contains_has_original_class(self):
holder = Holder.objects.create(dummy=1)
holder.inner_set.create(dummy=1)
Expand Down
Loading