Skip to content

feat: forms builder - #67

Merged
ethnjs merged 185 commits into
mainfrom
feat/form-setup
Aug 25, 2026
Merged

feat: forms builder#67
ethnjs merged 185 commits into
mainfrom
feat/form-setup

Conversation

@ethnjs

@ethnjs ethnjs commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Summary

Builds NEXUS's own form system as a replacement for the external Google Form + sheet-sync pipeline — models, routes, branching/validation, edit lifecycle, and availability/lunch write-through on the backend, plus the TD-facing form builder and a respondent-facing preview page on the frontend. This is one PR across the Forms tracking issues (#63 parent; closes #56, #57, #62, #64, #66).

Forms is deliberately "dumb" — it has no concept of tournament onboarding or membership side effects. Nothing in this PR lets a new member actually encounter and fill out a form as part of joining a tournament; that orchestration is a separate, later piece of work (see Out of scope).

What changed

Backend — core model, branching, validation, edit lifecycle

  • Form / FormField / FormResponse / FormAnswer, owned by exactly one of tournament or chapter (ck_form_owner_exclusive), status: draft / published / archived.
  • Per-question-type config validation (pydantic schemas per question_type), reserved field_keyquestion_type enforcement (availability_*, event_preference_*, lunch_{date}_{category}), option/branching validation, whole-form publish-time validation with reachability checks over the branching graph.
  • Bulk field replace: PUT /forms/{id}/fields/ replaces per-field CRUD entirely — full ordered target list, diffed server-side. Draft forms apply changes directly; published forms archive-not-delete removed/type-changed fields and options instead of losing them, and flag affected responses via FormResponsePendingUpdate rather than silently going stale.
  • option_id is server-generated and durable — used for branching targets, archiving, and write-through matching instead of array position or label text.
  • Snapshot select-type answers' value/label at submission time, so a later edit to an option's text doesn't retroactively change what a past response reads as.

Backend — availability & lunch write-through

  • TournamentMembershipAvailability and TournamentMembershipLunch tables. Reserved-key answers (availability_*, lunch_{date}_{category}) diff-sync into them on response submission — only the delta is touched, not a replace-all.
  • TournamentShift deletion is blocked while referenced by a live availability option, in addition to the pre-existing guard for shifts someone's already answered with.

Backend — chapter support

  • owner_type: "chapter" is fully implemented at the route layer: POST/GET /chapters/{id}/forms/, gated on officer/lead, covered in test_forms.py. No chapter-side frontend consumes it yet (see Out of scope).

Frontend — form builder

  • Standalone /forms/{id}/edit and /forms/{id}/preview, deliberately outside app/dashboard/ since a form's URL doesn't encode which owner (tournament vs. chapter) it belongs to.
  • Field builder: expand/collapse cards, drag-and-drop reorder, per-question-type config UI (options editor, entity-backed options for availability/event_preference pulling from tournament shifts/events, ranked-choice, display-style toggle for button-group vs. list), branching menu, field-key combobox with reserved-key presets, client-side validation wired to real Save.
  • Respondent-facing preview: renders the same QuestionRenderer in fill mode, floating submit bar with error summary and leave-warning, auto-scroll to the first error/next question.
  • Tournament forms list page: name/status/creator/response-count/updated, create modal, edit/preview row actions.
  • Component/UI work landed alongside: RadioCircle, RadioList, CheckboxList, RankedList, EditableText, reworked SplitButton (used for the draft/publish/archive status control), FloatingSaveBar extensions, Combobox/Input/Textarea/Card fixes.

Frontend — draft / publish / archive

  • StatusControl wires PATCH /forms/{id}/ (draft→published, validated) and POST /forms/{id}/archive/. This is what's currently built and working, not a settled design — see Out of scope.

Out of scope

  • Tournament onboarding. Nothing here lets a member actually see and fill out a form as part of joining a tournament — that's issue feat: tournament onboarding #65, tracked separately, and will land in its own branch/PR.
  • Viewing responses. No UI for browsing submitted responses, or the availability/event-preference/lunch data they write through to. GET /forms/{id}/responses/ exists on the backend, but how that data actually gets displayed depends on decisions the onboarding PR still needs to make about response/answer shape.
  • Chapter forms frontend. Backend routes are done and tested; there's no chapter dashboard page to create or manage a form from. Deferred — focus right now is tournament features, chapters aren't next.
  • Draft/publish/archive is provisional, not finalized. The current publish/archive behavior is enough to unblock the builder UI, but the actual lifecycle design — including that there's no way to unarchive a form right now — hasn't been settled and will likely change in the onboarding PR once it's clearer how forms plug into a tournament's actual lifecycle.

Test plan

  • backend/tests/api/test_forms.py, backend/tests/core/test_forms.py, test_form_branching.py, test_form_validation.py, test_form_write_through.py — model/route CRUD, config validation, branching reachability, edit-lifecycle archiving, availability/lunch diff-sync, shift-deletion guards, chapter routes.
  • backend/tests/api/tournament/test_shifts.py, test_memberships.py — updated for the availability write-through/deletion-guard changes.
  • Manually verified: build a form with every question type, publish, submit a response as a non-TD member, edit a published form's fields/options and confirm archived options still resolve on the old response, drag-reorder, branching between questions.

brandonalo and others added 30 commits August 17, 2026 16:25
…r and add status/description/membership-trigger fields
…ow non-members on creates_membership_on_submit forms
…ead presets.py, align routes on payload naming
… fix pre-existing config-shape test breakage
@ethnjs ethnjs linked an issue Aug 24, 2026 that may be closed by this pull request
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
nexus Ready Ready Preview Aug 25, 2026 3:33am

@railway-app

railway-app Bot commented Aug 24, 2026

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
nexus ✅ Success (View Logs) Aug 25, 2026 at 3:33 am

@railway-app
railway-app Bot temporarily deployed to nexus / nexus-pr-67 August 25, 2026 03:32 Destroyed
@ethnjs
ethnjs merged commit 6fa73b0 into main Aug 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend database Updates database schemas or models enhancement New feature or request forms frontend

Projects

None yet

2 participants