Skip to content

feat(forms): response management, member response edits, and safe user deletion - #100

Merged
ethnjs merged 13 commits into
mainfrom
fix/form-responses-and-user-delete
Oct 3, 2026
Merged

ethnjs merged 13 commits into
mainfrom
fix/form-responses-and-user-delete

Conversation

@ethnjs

@ethnjs ethnjs commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

  1. Managers get a Responses tab on every form: view each member's response, search by name or email, and delete a response.
  2. A per-form "Allow response edits" toggle lets members revise their responses; members can always view what they submitted.
  3. Admins can delete any user, including ones with form responses. Personal data goes; tournaments they own and things they authored stay as history.
  4. Fixes members being able to open the form builder.

What changed

Backend

Models and migrations

alembic upgrade head required — 2 revisions.

  • tournaments.owner_id, forms.created_by, join_codes.created_by and audit_log_entries.actor_id become nullable with ON DELETE SET NULL. (b4e8d2f1a7c3)
  • Destroys data: deleting a user now succeeds and permanently removes their form responses, profile, experience and memberships; before, it failed for any user with responses.
  • User.form_responses and User.chapter_membership cascade with passive_deletes. The ORM used to null their NOT NULL user_id instead, which is why that delete failed.
  • Adds forms.allow_response_edits; existing forms default to off. (c5f1a9e3d2b7)
  • Downgrading the first revision fails if a deleted user has already left any of those four columns null.

Schemas and routes

  • GET /admin/users/{id}/ returns owned_tournaments.
  • POST /tournaments/{id}/transfer-ownership/ now also allows admins, which is how an ownerless tournament gets an owner. Its audit entry now records the previous owner, not the actor.
  • Credit references (PersonRef) return user_id: null for a deleted user; TournamentRead.owner_id and FormRead.created_by are nullable.
  • GET /forms/{id}/responses/ returns FormResponseManagerRead, adding respondent (name, email, membership id). /responses/me/ is unchanged.
  • Destroys data: new DELETE /forms/{id}/responses/{response_id}/ permanently removes one response and its answers; manage access, 409 on an archived form or tournament.
  • allow_response_edits on FormRead, FormListRead, MemberFormRead, and settable through PATCH /forms/{id}/.
  • PATCH /forms/{id}/responses/me/ accepts any live question when the form allows edits; otherwise only flagged questions, as before.
  • GET /forms/{id}/?raw=true now requires manage access. Plain GET /forms/{id}/ is unchanged.

Logic

  • With edits on, required/branching validation runs over the stored response merged with the edit, so a changed branch can't leave a newly reachable required question blank.
  • A member who has already submitted an onboarding form keeps access to it (to read and revise it), instead of losing it once it's no longer their next step.
  • Deleting a response keeps its write-through data (availability, lunch, event preferences, track statuses) and recomputes onboarding.
  • New audit action form_response_deleted, storing the form and the respondent's name.

Frontend

Responses tab

  • /forms/[id]/responses, reached from Questions / Responses tabs in the form header; the tab row is sticky.
  • Two panes: searchable respondent list and the selected response, stacking on mobile with a back button.
  • Answers render read-only through the new shared ResponseAnswers, which unwraps stored option snapshots, shows options removed since as "Removed", and shows "Not answered" for blanks.
  • Delete confirms in a modal; disabled with the reason on an archived form or tournament.

Member view and edit

  • /forms/[id]/view serves every state: fill a new response, view your submitted one read-only, or edit it with ?edit=true.
  • Editing reuses FormFillFlow one question at a time with previous answers prefilled; only changed answers are sent. FormUpdateFlow is removed.
  • ?edit=true when editing is locked falls back to the read-only view and drops the param.
  • Overview Forms card: clicking a completed form opens the read-only view; Edit opens edit mode, or shows a Locked badge with the reason. The card spans two columns.

Managers

  • "Allow response edits" toggle in the builder header's ⋮ menu and in each forms-list row's ⋮ menu.
  • Forms-list rows move Edit, Preview and View responses into the ⋮ menu; status actions stay on the row. A Locked badge marks forms with edits off.
  • The builder and Responses pages show a No access state on 403.
  • Ranked-choice builder preview lists every option, not just the first ranks.

Admin

  • The user delete modal loads and lists owned tournaments, and requires typing DELETE.
  • Ownerless tournaments show a "No owner" badge on the admin tournaments list.
  • Deleted users display as "Deleted user" wherever someone is credited.

Shared UI

  • RadioList, CheckboxList, ButtonGroup, RankedList and Dropdown options take an optional badge.
  • QuestionRenderer takes removedOptions and answerNote.
  • BulkDeleteModal takes an optional confirmPhrase and notReady.

Out of scope

  • The member-facing GET /forms/{id}/ still returns manager-only details (field_key, custom option values, response_count, prerequisites). That needs a member schema of its own.
  • Track response locks don't gate form edits; forms still write locked tracks.
  • Flagged questions aren't surfaced on the overview; that flow works as before from the form page.

Test plan

  • pytest passes locally
  • Migration included for every model change
  • Clicked through the UI change in a browser

Automated

  • test_users.py: admin delete with responses, owned tournament survives ownerless, deleted actor in the audit log, owned_tournaments, admin re-owning an ownerless tournament, self-delete as an owner.
  • test_forms.py: respondent on the manager list, response delete (archived form blocked, wrong form 404, audit entry, write-through kept, onboarding recomputed), allow_response_edits (toggle, free edits, merged validation, onboarding access), raw=true gated, forms/me field.

@vercel

vercel Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nexus Ready Ready Preview Oct 3, 2026 12:00am UTC

@railway-app

railway-app Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the nexus-pr-100 environment in nexus

Service Status Web Updated
nexus ✅ Success (View Logs) Oct 2, 2026 at 11:59 pm UTC

@railway-app
railway-app Bot temporarily deployed to nexus / nexus-pr-100 October 2, 2026 23:40 Destroyed
@ethnjs
ethnjs marked this pull request as ready for review October 2, 2026 23:44
@railway-app
railway-app Bot temporarily deployed to nexus / nexus-pr-100 October 2, 2026 23:59 Destroyed
@ethnjs
ethnjs merged commit 8839cef into main Oct 3, 2026
4 of 5 checks passed

This branch was successfully deployed

1 active deployment
Preview — 6338ffed Deployed Oct 3, 2026 by vercel[bot]
nexus / nexus-pr-100 — 6338ffed Deployed Oct 2, 2026 by railway-app[bot]
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.

1 participant