feat(booth): multi-question asks — a questions list renders one form with a radio group per question and lands as one answer sidecar keyed by question

- asks.py: single {prompt, options} and multi {title, questions:[{key, prompt, options, notes?}]} both normalise to questions[]; per-question notes; every question required on submit
- /answer reads choice.<key> / notes.<key> / notes for multi; single shape unchanged
- booth asks prints per-question picks; README + CLI header; install step symlinks the CLI to ~/.local/bin; v0.1.10; 135 tests
This commit is contained in:
vh
2026-09-09 09:20:35 -07:00
parent 97589dd062
commit 125a1b7fc5
8 changed files with 355 additions and 69 deletions
+21 -8
View File
@@ -82,6 +82,7 @@ from booth.asks import ( # noqa: E402
is_answer_file,
is_ask_file,
list_asks,
load_ask,
valid_stem,
write_answer,
)
@@ -654,18 +655,30 @@ def create_app(
)
@app.post("/b/{name}/answer")
def booth_answer(request: Request, name: str, ask: str = Form(...), choice: str = Form(...),
notes: str = Form("")):
"""Record the operator's answer to one ask: validates the choice
against the ask's options and writes `<stem>.answer.json` atomically.
Re-submitting overwrites — the sidecar is the current answer. 404 for
an unknown/invalid stem, 400 for a choice the ask does not offer."""
async def booth_answer(request: Request, name: str):
"""Record the operator's answer to one ask: validates every choice
against the ask and writes `<stem>.answer.json` atomically.
Re-submitting overwrites — the sidecar is the current answer.
Form fields: `ask` (stem); single-question → `choice` + `notes`;
multi-question → `choice.<key>` per question, optional `notes.<key>`,
plus the form-level `notes`. 404 for an unknown/invalid stem, 400 for
a missing choice or one the ask does not offer.
"""
booth = resolve_booth(name)
if not valid_stem(ask) or not (booth / f"{ask}{ASK_SUFFIX}").is_file():
form = await request.form()
ask = form.get("ask")
if not isinstance(ask, str) or not valid_stem(ask) or not (booth / f"{ask}{ASK_SUFFIX}").is_file():
raise HTTPException(status_code=404, detail="no such ask")
who = request.client.host if request.client else ""
try:
write_answer(booth, ask, choice, notes, who=who)
spec = load_ask(booth, ask)
if spec["multi"]:
choice = {q["key"]: form.get(f"choice.{q['key']}") for q in spec["questions"]}
qnotes = {q["key"]: form.get(f"notes.{q['key']}") for q in spec["questions"]}
write_answer(booth, ask, choice, form.get("notes", ""), who=who, qnotes=qnotes)
else:
write_answer(booth, ask, form.get("choice"), form.get("notes", ""), who=who)
except AskError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return RedirectResponse(url=f"/b/{quote(name, safe='')}/#ask-{quote(ask, safe='')}", status_code=303)