feat(options): letterless student/projector UI + text-on-wire submit
Both student-facing surfaces (the per-device join page and the
front-of-room projector) now render options as text only, no A/B/C/D
chips, no numeric prefixes. The letter-namespace remains internal:
canonical A..D in pool.json + submissions storage; canonical position
1..4 in the CSV export. Admin dashboard keeps letters because it is the
instructor's private console.
Why this lands now: the discussion of per-student option shuffling
flagged that A/B/C/D as discussion handles ("the answer is B") is itself
a leaky channel. Removing the labels closes that channel for the
non-shuffled case and keeps it closed if shuffling is added later.
Wire protocol: the submit message carries the option's full text
("answer": "Pipelining"). Server's submit_answer resolves text -> letter
via app.pool.resolve_option_key, which also accepts a canonical letter
so internal callers and tests stay readable. A non-matching string is
recorded as a zero-score submission with answer=NULL, locked in via the
PK + existing_submit_ack short-circuit. So an attempted UI bypass that
posts a fabricated string just produces a wrong answer; no retry.
CSV: A=1 .. D=2 .. C=3 .. D=4 in the answer column. Empty when no option
matched. Header unchanged so downstream pandas readers don't break, but
the value type is int instead of letter.
Histogram: the failsafe "submitted-but-no-match" row buckets into
"missed" alongside genuine misses — both yield zero credit and the
instructor cares about the same thing.
Tests:
- test_submit_accepts_option_text_resolves_to_canonical: production
wire format produces correct grading + canonical-letter storage.
- test_submit_failsafe_locks_in_zero_score_on_garbage_text: a non-
matching string is recorded at score=0 and a follow-up correct
submission cannot overwrite it.
71/71 green.
This commit is contained in:
@@ -383,7 +383,7 @@ function renderQuestion(s, revealed) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol class="big-options ${revealed ? "revealed" : ""} ${preVote ? "pre-vote" : ""}">
|
||||
<ol class="big-options letterless ${revealed ? "revealed" : ""} ${preVote ? "pre-vote" : ""}">
|
||||
${["A","B","C","D"].map((k) => {
|
||||
const v = hist[k] || 0;
|
||||
const pct = Math.round(100 * v / total);
|
||||
@@ -395,7 +395,6 @@ function renderQuestion(s, revealed) {
|
||||
].filter(Boolean).join(" ");
|
||||
return `
|
||||
<li class="${cls}">
|
||||
<span class="opt-key">${k}</span>
|
||||
<span class="opt-text">${escapeText(q.options?.[k] || "")}</span>
|
||||
<span class="opt-bar"><span class="opt-bar-fill" style="width:${pct}%"></span></span>
|
||||
<span class="opt-count">${v}<small>${pct}%</small></span>
|
||||
|
||||
Reference in New Issue
Block a user