Skip to content

Preserve admin uploads on validation error + drag-and-drop (#248)#1379

Merged
jonfroehlich merged 2 commits into
masterfrom
248-preserve-uploads-on-admin-validation-error
Jun 22, 2026
Merged

Preserve admin uploads on validation error + drag-and-drop (#248)#1379
jonfroehlich merged 2 commits into
masterfrom
248-preserve-uploads-on-admin-validation-error

Conversation

@jonfroehlich

@jonfroehlich jonfroehlich commented Jun 22, 2026

Copy link
Copy Markdown
Member

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 novalidate attribute, which disables the browser's native HTML5 validation. So even though Django emits required on 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:

  • Blocks the file-losing submit. If any required field is empty, it stops the POST, shows an accessible error summary (role="alert", scroll-to-field), so the round-trip never happens and selected files stay attached. This is the primary case.
  • File pre-checks 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 (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-ness is read from the DOM ([required]), which Django derives from each model field's blank=. No field list is hardcoded.
  • Allowed extensions flow from the PDF_EXTENSIONS / RAW_FILE_EXTENSIONS constants in website/utils/upload_validators.pyArtifactAdmin.get_form sets each file input's accept → the JS reads it back. Python is the single source of truth.
  • The %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 uses role="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

  • New regression test website/tests/test_artifact_admin_upload_guard.py 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.
  • Full suite green: 486 tests OK (8 skipped).
  • Behavior verified in headless Chrome: submit blocked when a required field is empty / allowed once filled, accessible summary rendered, drag-drop scaffolding present and aria-hidden, extension check + PDF magic-byte check both working.

Screenshots

image

Follow-up (not in this PR)

Consider generalizing the guard + drag-and-drop to image fields (Person headshot, Project gallery image) — validate_image_upload already provides the allowlist + magic bytes. Caveat: those use the in-repo image_cropping Cropper.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

jonfroehlich and others added 2 commits June 22, 2026 14:18
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>
@jonfroehlich jonfroehlich merged commit b1fa6ae into master Jun 22, 2026
3 checks passed
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.

When There is An Error On Admin Page and You're Uploading Files, the Files and Paths to Files are Not Saved

1 participant