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
38 changes: 37 additions & 1 deletion django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works on my test project, and it should not affect existing overrides. It also provides the option to fall back to the initial behavior:

def get_readonly_fields(self, request, obj=None):
    return self.readonly_fields

return list(self.readonly_fields) + self._get_readonly_manual_pk_fields(obj)

def get_prepopulated_fields(self, request, obj=None):
"""
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion tests/admin_custom_urls/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
'<div class="readonly">path/to/html/document.html</div>',
html=True,
)

def test_post_save_add_redirect(self):
"""
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