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

Conversation

Jkhall81
Copy link
Contributor

@Jkhall81 Jkhall81 commented Jul 26, 2025

Trac ticket number

ticket-2259

Branch description

When editing a model instance in the Django admin, manually specified primary key fields (e.g. CharField(primary_key=True)) should not be editable. Without this fix, users can mistakenly change a model's PK, which can cause errors or lead to duplicate records.

This patch updates BaseModelAdmin.get_readonly_fields() to automatically mark such PKs as readonly if they are not auto-generated and not already in the readonly fields.

This change was verified against existing admin inline tests (test_char_pk_inline, test_integer_pk_inline), which initially failed due to the patch, and the logic was adjusted to restore their passing state.

Checklist

  • [x ] This PR targets the main branch.
  • [x ] The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • [x ] I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

@Jkhall81 Jkhall81 force-pushed the 2259-readonly-manual-pk-admin branch from 094817a to 7a78c93 Compare July 26, 2025 03:24
@@ -418,7 +418,22 @@ 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


# Only add PK to readonly if:
# - editing an existing object
# - PK is not auto-created
Copy link

Choose a reason for hiding this comment

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

Any particular reason why an auto_created pk needs to be excluded?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

auto_created pks are already not editable in the admin.

@sarahboyce sarahboyce added the selenium Apply to have Selenium tests run on a PR label Jul 29, 2025
Copy link
Contributor

@sarahboyce sarahboyce left a comment

Choose a reason for hiding this comment

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

Thank you for the PR
This doesn't seem to be working fully for inlines.
Given the models:

class State(models.Model):
    label = models.CharField(max_length=255)


class StatePKModel(models.Model):
    id = models.IntegerField(primary_key=True)
    state = models.ForeignKey(State, related_name="pk_state_model", on_delete=models.CASCADE)

and admin

class StatePKModelInline(admin.TabularInline):
    model = StatePKModel


@admin.register(State)
class StateAdmin(admin.ModelAdmin):
    inlines = [StatePKModelInline,]

I can edit the id in the inline
Screenshot from 2025-07-29 10-43-02

Comment on lines 112 to 115
label {
font-size: 1rem;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

This change looks to be unrelated to the ticket

Copy link
Contributor Author

@Jkhall81 Jkhall81 Jul 29, 2025

Choose a reason for hiding this comment

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

That was my bad. Fixed this css thing (was from a year old PR). Also, I confirmed with a test that manually defined Pks are readonly in inlines when editing existing objects.

@@ -418,7 +418,22 @@ def get_readonly_fields(self, request, obj=None):
"""
Hook for specifying custom readonly fields.
"""
return self.readonly_fields
readonly = self.readonly_fields
pk_field = self.model._meta.pk
Copy link
Member

Choose a reason for hiding this comment

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

This should account for composite primary keys now and use _meta.pk_fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Updated to use _meta.pk_fields for future composite PK support, with fallback to _meta.pk for compatibility.

Copy link
Member

Choose a reason for hiding this comment

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

Updated to use _meta.pk_fields for future composite PK support, with fallback to _meta.pk for compatibility.

No need for a fallback, see the docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fallback removed.

@Jkhall81 Jkhall81 force-pushed the 2259-readonly-manual-pk-admin branch from 893d265 to 262ca22 Compare July 29, 2025 18:36
@Jkhall81 Jkhall81 force-pushed the 2259-readonly-manual-pk-admin branch from 262ca22 to 2153973 Compare July 29, 2025 18:50
@Jkhall81 Jkhall81 requested a review from sarahboyce July 30, 2025 12:39
@sarahboyce
Copy link
Contributor

Thank you for the PR This doesn't seem to be working fully for inlines. Given the models:

class State(models.Model):
    label = models.CharField(max_length=255)


class StatePKModel(models.Model):
    id = models.IntegerField(primary_key=True)
    state = models.ForeignKey(State, related_name="pk_state_model", on_delete=models.CASCADE)

and admin

class StatePKModelInline(admin.TabularInline):
    model = StatePKModel


@admin.register(State)
class StateAdmin(admin.ModelAdmin):
    inlines = [StatePKModelInline,]

I can edit the id in the inline Screenshot from 2025-07-29 10-43-02

This is still not fixed unfortunately

@Jkhall81
Copy link
Contributor Author

Jkhall81 commented Aug 5, 2025

Thanks for the feedback. Just to clarify -- the original Trac ticket #2259 doesn't mention inlines at all. The bug, as described and discussed over the years, is focused on making manually defined primary key fields read-only in the Django admin when editing existing objects.

The root issue was that those fields were editable, changing them would silently insert a new object instead of updating the original -- without any error or indication. This has been fixed by marking such fields as read-only in ModelAdmin when editing.

inlines were only brought up in two comments (comments 29 and 54) over the 19 years of discussion on the ticket. So I'm a little confused about why inlines are being brought up.

That being said. I can investigate a follow up fix for inlines.

@Jkhall81 Jkhall81 force-pushed the 2259-readonly-manual-pk-admin branch from acbe1c3 to 2085066 Compare August 5, 2025 13:50
@Jkhall81
Copy link
Contributor Author

Jkhall81 commented Aug 5, 2025

So I tried making manually-defined primary key fields (including composite PKs) readyonly when editing inlines by overriding InlineModelAdmin.get_readonly_fields() and adding a help method to identify and return PK field names. This worked but lead to the PK fields being entirely hidden in the Django Admin. Which I'm fairly sure you all don't want.

I'm happy with the resolution as it stands and would rather not pursue additional inline-specific logic here.

@sarahboyce
Copy link
Contributor

The root issue was that those fields were editable, changing them would silently insert a new object instead of updating the original -- without any error or indication. This has been fixed by marking such fields as read-only in ModelAdmin when editing.

This issue, as described, is present for inlines. As you mentioned, it has been mentioned a couple of times in the ticket discussion. This was also a reason why some PRs previously didn't continue as they were missing a solution for this problem
The ticket is "Primary keys should be readonly by default in admin", and inlines are part of the admin
Currently, the solution doesn't resolve fix the ticket for me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
selenium Apply to have Selenium tests run on a PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants