Skip to content

Quickfix: errata admin 500s from shared ModelAdmin instance race#1735

Open
mwvolo wants to merge 1 commit into
mainfrom
fix/errata-admin-shared-instance-race
Open

Quickfix: errata admin 500s from shared ModelAdmin instance race#1735
mwvolo wants to merge 1 commit into
mainfrom
fix/errata-admin-shared-instance-race

Conversation

@mwvolo

@mwvolo mwvolo commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

  • Sentry flagged two FieldError 500s on /django-admin/errata/errata/ (add and change views) right after the last release: Unknown field(s) (accounts_link) and Unknown field(s) (accounts_user_faculty_status) specified for Errata.
  • Root cause: ErrataAdmin is registered once and reused as a singleton across every request. get_form()/changelist_view() mutated self.fields, self.readonly_fields, self.list_display, etc. based on request.user's role. Under concurrent requests from different-role users (matches the two different users in the Sentry issues), one user's field list could leak into another user's readonly_fields check between Django's internal get_fieldsets() call and its get_form() call, so fields_for_model saw a field name that wasn't excluded and wasn't a real model field, and raised.
  • Fix: replace the self.* mutations with Django's proper per-request hooks (get_fields, get_readonly_fields, get_list_display, get_list_filter, render_change_form), which return computed values instead of writing shared instance state. No behavior change per role — same fields/permissions as before, just computed safely.

Test plan

  • Added ErrataAdminSharedInstanceTest which replays the exact request interleaving Django performs internally (get_fieldsets for user A, then get_form for user B) — confirmed it reproduces the original FieldError against the pre-fix code and passes against the fix.
  • python manage.py test errata --settings=openstax.settings.test (7 tests) passes.

ErrataAdmin is registered once and reused as a singleton across every
request. get_form()/changelist_view() mutated self.fields,
self.readonly_fields, self.list_display, etc. per-request based on
request.user's role. Under concurrent requests from different-role
users, one user's field list could leak into another's readonly_fields
check between Django's get_fieldsets() and get_form() calls, causing
fields_for_model to raise "Unknown field(s) specified for Errata"
(Sentry OPENSTAX-CMS-VJ/VH, v13.0.0).

Replace the self.* mutations with Django's per-request hooks
(get_fields, get_readonly_fields, get_list_display, get_list_filter,
render_change_form), which return computed values instead of writing
shared state. No behavior change per role.

Add a regression test that replays the exact request interleaving
Django performs internally and confirms it no longer raises.
Copilot AI review requested due to automatic review settings July 10, 2026 21:29
Comment thread errata/admin.py
return ['id', 'book_title', 'created', 'modified', 'short_detail', 'number_of_errors', 'status', 'error_type', 'resource', 'location', 'additional_location_information', 'resolution', 'archived', 'junk']
return ['id', 'book_title', 'created', 'short_detail', 'status', 'error_type', 'resource', 'location', 'created', 'archived']

def get_list_filter(self, request):

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes intermittent Django admin 500s in the Errata admin caused by mutating shared ModelAdmin instance state (self.fields, self.readonly_fields, etc.) across concurrent requests. It refactors ErrataAdmin to compute role-specific admin configuration via per-request hooks and adds a regression test that reproduces the problematic request interleaving.

Changes:

  • Refactored ErrataAdmin to use get_fields, get_readonly_fields, get_list_display, and get_list_filter rather than mutating self.* per request.
  • Added a test that simulates cross-role request interleaving to ensure get_form() remains stable.
  • Adjusted admin list configuration defaults (e.g., list_display_links) to avoid per-request mutation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
errata/admin.py Replaces shared-instance mutations with per-request ModelAdmin hooks to prevent cross-request role leakage and resulting FieldError 500s.
errata/tests.py Adds a regression test that reproduces the interleaving that previously triggered “Unknown field(s) specified for Errata” errors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread errata/admin.py
Comment on lines +201 to +204
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
response = super().render_change_form(request, context, add, change, form_url, obj)
response.context_data['save_as'] = self._is_content_manager(request) or self._is_editorial_vendor(request)
return response
Comment thread errata/admin.py
Comment on lines +165 to +168
def get_list_display(self, request):
if self._is_content_manager(request):
return ['id', 'book_title', 'created', 'modified', 'short_detail', 'number_of_errors', 'status', 'error_type', 'resource', 'location', 'additional_location_information', 'resolution', 'archived', 'junk']
return ['id', 'book_title', 'created', 'short_detail', 'status', 'error_type', 'resource', 'location', 'created', 'archived']
Comment thread errata/admin.py
Comment on lines +170 to +173
def get_list_filter(self, request):
if self._is_content_manager(request):
return (('created', DateRangeFilter), ('modified', DateRangeFilter), ('book', ActiveBookListFilter), 'status', 'created', 'modified', 'is_assessment_errata', 'modified', 'error_type', 'resolution', 'archived', 'junk', 'resource')
return (('created', DateRangeFilter), ('modified', DateRangeFilter), ('book', ActiveBookListFilter), 'status', 'created', 'modified', 'is_assessment_errata', 'error_type', 'resolution', 'archived', 'resource')
Comment thread errata/tests.py
Comment on lines +159 to +163
# Mirrors Django's own _changeform_view: get_fieldsets() runs for one
# user, then get_form() runs for a *different* user before the first
# request finishes - the interleaving that produced the "Unknown
# field(s) specified for Errata" 500s in production.
errata_admin.get_form(plain_request, None)
@mwvolo

mwvolo commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Superseded by #1736, which is based off this branch and includes all of these commits plus the permission-model redesign. Closing this one to avoid keeping two open PRs for the same file.

@mwvolo mwvolo closed this Jul 11, 2026
@mwvolo mwvolo reopened this Jul 13, 2026
@mwvolo mwvolo changed the title Fix errata admin 500s from shared ModelAdmin instance race Quickfix: errata admin 500s from shared ModelAdmin instance race Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants