Preserve admin uploads on validation error + drag-and-drop (#248)#1379
Merged
jonfroehlich merged 2 commits intoJun 22, 2026
Merged
Conversation
The Django admin change form renders with novalidate, so the browser does not enforce the required attributes Django emits. A submit that is missing a required field round-trips to the server, fails validation, and the re-rendered form silently drops any file the user had selected -- the re-upload pain reported in #248. Add a progressive-enhancement guard on the Talk/Poster/Publication admin forms (website/static/website/js/admin_artifact_form.js): - Block the file-losing submit: if any required field is empty, stop the POST, show an accessible error summary, and scroll to the first offender, so the round-trip never happens and selected files stay attached. - Pre-check files on selection: extension (from the field's accept list) and a %PDF- magic-byte check for the PDF field, mirroring the server validators so a bad file is caught before it costs a round-trip. - Drag-and-drop onto each file field via the DataTransfer API, plus a selected-filename readout. The native input stays the accessible control; the drop zone is a mouse-only enhancement hidden from assistive tech. Staying in sync with the backend: required-ness is read from the DOM ([required]), and allowed extensions flow from the PDF_EXTENSIONS / RAW_FILE_EXTENSIONS constants in upload_validators.py via ArtifactAdmin. get_form, which sets each file input's accept attribute. Python stays the single source of truth and the server validators remain authoritative, so any drift degrades gracefully. Regression test pins the server-side contract the JS depends on: the guard assets load on all three artifact add forms and the accept attributes match the validator allowlists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow the conventional file-upload UX (Dropzone/Uppy/GitHub style): hide the raw native file input (kept focusable + submittable for a11y) and make a single drop zone the primary control, with idle / hover / drag-over / filled / error states. The selected file is shown with its name, size, and a Remove/Replace button, and the field surfaces its accepted types as a quiet hint (read from the input's accept attribute). Keyboard focus is mirrored onto the zone as a visible ring; error state pairs an icon-color change with text (never color alone). No behavior change to the guard or validation: the required-field submit guard, extension check, and PDF magic-byte check are unchanged; this is presentation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #248.
Problem
When an admin add/change form for a Talk, Poster, or Publication fails validation — most commonly because a required field was left blank (date, forum name, location, talk type, PDF) — the form round-trips to the server, and any file the user had selected is silently dropped. They have to re-attach the PDF/PPTX after fixing an unrelated field.
Root cause (verified): Django's admin renders its change form with the
novalidateattribute, which disables the browser's native HTML5 validation. So even though Django emitsrequiredon every required field, the browser does not block the submit — the POST goes through, validation fails server-side, and the re-rendered form can't repopulate a file input (browsers never re-send a file input's value).Fix
A progressive-enhancement guard on the artifact admin forms (
website/static/website/js/admin_artifact_form.js) — the form still works without JS:role="alert", scroll-to-field), so the round-trip never happens and selected files stay attached. This is the primary case.acceptlist) and a%PDF-magic-byte check for the PDF field — mirroring the server validators so a bad file is caught before it costs a round-trip.DataTransferAPI) plus a selected-filename readout. The native input stays the accessible control; the drop zone is a mouse-only enhancement hidden from assistive tech (aria-hidden).Staying in sync with the backend
The server validators remain authoritative — everything here is a convenience pre-check, so any drift degrades gracefully. To avoid duplicating rules in JS:
[required]), which Django derives from each model field'sblank=. No field list is hardcoded.PDF_EXTENSIONS/RAW_FILE_EXTENSIONSconstants inwebsite/utils/upload_validators.py→ArtifactAdmin.get_formsets each file input'saccept→ the JS reads it back. Python is the single source of truth.%PDF-signature is a frozen part of the PDF spec, so there's nothing to keep in sync.Accessibility
Handled by design: drop zone is
aria-hidden(native input remains the control), the error summary usesrole="alert"and receives focus, and error state is never color-only (outline + text). Pa11y's config scans public pages, not the login-gated admin, so it doesn't cover these forms.Testing
website/tests/test_artifact_admin_upload_guard.pypins the server-side contract the JS depends on: the guard assets load on all three artifact add forms, and theacceptattributes match the validator allowlists.aria-hidden, extension check + PDF magic-byte check both working.Screenshots
Follow-up (not in this PR)
Consider generalizing the guard + drag-and-drop to image fields (Person headshot, Project gallery image) —
validate_image_uploadalready provides the allowlist + magic bytes. Caveat: those use the in-repoimage_croppingCropper.js widget, whose own client-side file handling must be confirmed to coexist with the drop zone. Rich-text (prose-editor) uploads are a different mechanism and out of scope.🤖 Generated with Claude Code