Skip to content

Feat/seed registration questions - #237

Merged
sabinem merged 5 commits into
mainfrom
feat/seed-registration-questions
Aug 25, 2026
Merged

Feat/seed registration questions#237
sabinem merged 5 commits into
mainfrom
feat/seed-registration-questions

Conversation

@sabinem

@sabinem sabinem commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Custom registration forms

As an organizer, I can decide what my event asks people at sign-up —
free text, yes/no, or a fixed list of options — and see what everyone answered.

As a participant, I answer those questions when I join, and can come back
and correct them.

What's here

  • Registration Form page under Manage: add, edit, reorder and delete
    questions. A question people have already answered locks its type, options and
    required flag, because the backend refuses to change those.
  • /register/[id]: the participant's form. Deliberately outside
    /my/hackathon/[id]/ — that layout denies non-members and waitlisted people,
    who are exactly who needs this page.
  • The dashboard's Join now sends you to the form when the event asks
    something, and stays one click when it doesn't.
  • Manage Participants shows "3 of 4 answered" and each person's answers.
  • Seed data: three of the four hackathons now ask questions, one asks nothing,
    and not everyone has answered — so the gaps are visible.

Known gaps (backend, ticketed, not worked around)

  • Answers cannot be saved yet. Both write paths reject every answer at parse
    time. The form reports this instead of pretending.
  • A waitlisted participant can read their answers but not change them.
  • ListQuestions needs hackathon:read again after the revert in
    modify llm changes, so a non-member can't reach the form.

Both organizer surfaces work end to end. Neither participant surface can
complete until the first gap lands.

Gives the frontend a form to render before any of it is built.

- H1 asks four questions (text, enum, text, bool), three of them mandatory.
  It is the only hackathon with `register` on, so it is where the
  join-and-answer flow is exercised: a newcomer reads the questions before
  joining and sends the answers along with Join.
- H2 asks two, both mandatory, and everyone has answered — a closed form, the
  counterpart to H1's partly-filled one.
- H3 asks nothing, which the frontend has to handle as its own case rather
  than as an empty form.
- H4 asks three at cohort scale: 87 of its 102 participants answered, 234
  answers in all.

The gaps are the point. charles is waitlisted in H1 and has answered nothing,
bob answered but skipped the optional `dietary`, and one in seven of H4 never
answered at all — "has not filled it in" and "filled it in and left the
optional parts blank" are different facts to an organizer, and only people who
answered have rows.

H4's answers draw from dataForGoodAnswerSeed, a random stream of its own, so
the preference draw that follows them is byte-identical.

The README documents two things that each cost an hour to find:

- The backend must be restarted after seeding. The seed writes casbin rows
  into the policy table, but the running server loaded its policy at startup
  and never reloads, so every per-hackathon role the seed granted is invisible
  to it. The symptom misleads: H1's owner is refused her own hackathon, and
  ListParticipantAnswers silently returns only her own answers instead of the
  cohort's. A global admin is unaffected, which makes it look even more like a
  handler bug.
- `Question.options` is a required column, so it is always set — an empty
  slice for the types that have no choices. Leaving it unset fails with
  "missing required field", which is not obvious from the call site.
A question arrives from the backend with a numeric type enum that lives under
$lib/server and so cannot be imported by a component, which is why this comes in
two halves: the kinds and their labels in $lib/utils/question, where a page can
reach them, and the enum mapping plus form parsing in $lib/server, where they
belong. `parseQuestionForm` checks a submission against the same rules the
backend enforces — key shape, lengths, a fixed list needing at least two
distinct options — so an organizer gets a sentence they can act on instead of a
raw InvalidArgument, while the RPC stays the authority. 33 tests cover it.
A new page under Manage where an organizer adds, edits and deletes the questions
their event asks at sign-up, plus the sidebar entry that leads to it. One
question per row and one RPC per row, because the backend edits a single question
at a time and has no whole-form save — so a failure spoils one row rather than
the lot. The part worth knowing: once a question has been answered the backend
refuses a type change, any options list, and even re-sending mandatory when it is
already true, so a plain label fix would have failed had the form sent everything
back. The action therefore reads the answer counts itself and sends only the
fields still allowed, and the page locks those controls and says why; relaxing a
required question to optional keeps working, since that is the one change the
backend still accepts.
Adds /register/[id], where someone answers a hackathon's questions — through
Join if they are signing up, SubmitAnswers if they are correcting an answer
later. It sits outside /my/hackathon/[id]/ because that layout's hackathon.get
refuses non-members and waitlisted callers, who are exactly the people who need
it. The dashboard's Join now checks ListQuestions first and sends them here when
the event asks anything. A blank answer is omitted rather than sent empty, so an
unticked required tick-box reads as unanswered instead of as a quiet "no".

Saving does not work yet: both backend write paths reject every answer at parse
time (see TODO(backend: answer-upsert-sql)), and a waitlisted participant cannot
save at all (TODO(backend: waitlisted-answers)). Both are reported to the user
rather than worked around.
Manage Participants gains the read side of the registration form: a "3 of 4
answered" line in the header, and per person either a disclosure listing their
answers question by question or a plain "Has not answered". Hidden entirely when
the event asks nothing, which is a state the fixture covers.

The disclosure sits below the card rather than inside it, because
ParticipantCard takes only an `actions` snippet and widening a component three
pages share for one of them is the wrong trade. The answered count is computed
through the roster, not off the answer map: answers outlive RemoveParticipant,
so counting them directly makes "12 of 10 answered" reachable. Both RPCs are
swallowed on failure since this page exists to approve and remove people — with
a note at the call site that ListParticipantAnswers silently narrows to the
caller's own answers without hackathon write, so a stale casbin policy shows a
roster where only the organizer appears to have answered.
@sabinem
sabinem force-pushed the feat/seed-registration-questions branch from 686affb to cdc2bcf Compare August 25, 2026 08:42
@sabinem
sabinem merged commit 2224769 into main Aug 25, 2026
2 checks passed
@sabinem
sabinem deleted the feat/seed-registration-questions branch August 25, 2026 09:28
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