Skip to content

Differentiate 'Not sure' from 'Skip': record a human abstention (issue #712) - #731

Merged
WilfordGrimley merged 3 commits into
masterfrom
feat-differentiate-notsure-skip
Aug 6, 2026
Merged

Differentiate 'Not sure' from 'Skip': record a human abstention (issue #712)#731
WilfordGrimley merged 3 commits into
masterfrom
feat-differentiate-notsure-skip

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Summary

"Not sure" and "Skip" on the What's That Card Level 1 confirm screen were
functionally identical: neither wrote anything to the backend. This
differentiates them per issue #712's design direction: "Not sure" means
the voter engaged and found the image genuinely ambiguous - information
about the card - while "Skip" carries no signal about the card at all.

Investigation before building (issue #712 point 1): checked whether an
existing mechanism already fit.

  • QuestionFeedServedLog has no card column (only anonymous_id/
    pool/question_type/origin_reason/served_at), so it cannot
    express "this voter found this card ambiguous."
  • CardScanLog records abstention, but only for MACHINE calculator runs
    (run_id, skip_reason, evidence_types_used, survivor_pks are all
    calculator-run bookkeeping) and backs a resume-exclusion query
    (local_identify_printing_tags._eligible_base_queryset) that must
    never be satisfied by a human tap - conflating the two would let one
    silently stand in for the other.
  • CardPrintingTag/CardIllustrationVote/CardTagVote (all
    AbstractWeightedVote subclasses) only express positive/no-match
    outcomes, never a no-opinion abstention.

None fit, so this adds one new model: CardQuestionAbstention (card,
anonymous_id, question_type, unique together, deliberately NOT an
AbstractWeightedVote subclass - an abstention is not a vote and must
never reach vote_consensus). Tapping "Not sure" now POSTs
2/submitQuestionAbstention/ (idempotent get_or_create) before the
same setStage("level2") transition it always did; the voter identity is
the same getOrCreateAnonymousId() value the feed fetch and every vote
submit already send. "Skip" is unchanged - it still writes nothing
anywhere, and whether it should record a lightweight "served, no answer"
fact is left as an open owner decision, not settled here.

For a future exclusion query (issue #713, not built here): "has this
anonymous_id already abstained on this card for this question_type"
is a single indexed equality lookup against this table's own unique
constraint - CardQuestionAbstention.objects.filter(card_id=..., anonymous_id=..., question_type=...).exists().

Documentation for the new model ships in this PR, extending
docs/features/printing-tags.md's "Unified question feed" section
(the neighbouring QuestionFeedServedLog/CardScanLog mechanisms are
already documented there).

Test plan

  • MPCAutofill/cardpicker/tests/test_question_abstention.py (new):
    the submit endpoint records exactly one abstention with the right
    voter/card/question-type; repeat taps from the same voter are
    idempotent (still one row); different question types or different
    voters on the same card record separately; an unknown card
    identifier is a 400; a card with no abstention calls has zero rows;
    the abstention is queryable by (card, anonymous_id, question_type).
  • frontend/src/features/questionFeed/QuestionFeed.test.tsx (new
    cases): tapping Level 1 "Not sure" POSTs the abstention with the
    right identifier/questionType and still advances to Level 2;
    tapping Level 1 "Skip" never calls submitQuestionAbstention.
  • Backend affected-area suite (test_question_abstention.py,
    test_question_feed.py, test_printing_tags_views.py,
    test_moderation_views.py) - 127 passed.
  • Full backend collection (pytest cardpicker/ --collect-only) -
    3692 tests collected, no import/collection errors from the
    model/admin/urls/views/schema_types changes.
  • frontend/src/features/questionFeed/QuestionFeed.test.tsx full
    file - 21 passed, 0 regressions.
  • Full backend suite (pytest cardpicker/) beyond the affected-area
    run above was not completed to green/red on every one of the
    3692 tests in this session.

…#712)

'Not sure' means a voter engaged and found the image genuinely ambiguous -
information about the card. 'Skip' carries no signal at all. Both used to
be no-ops; now 'Not sure' POSTs 2/submitQuestionAbstention/ and is
recorded in a new CardQuestionAbstention row (card, anonymous_id,
question_type, unique together, get_or_create-idempotent) before the
existing Level 1 -> Level 2 transition. 'Skip' is unchanged.

CardQuestionAbstention is the human counterpart to CardScanLog's machine
abstention and is deliberately a separate model, not a subclass of
AbstractWeightedVote - an abstention is not a vote.
APISubmitQuestionAbstention's return type annotation referenced
SubmitQuestionAbstentionResponse without importing it from schema_types -
the type is exported there, just never pulled into api.ts's import list.
Jest's transpile-only transform doesn't type-check, so this only surfaced
as a 'next build' compile failure (the build step that only runs on
shard 1 of the sharded Playwright job, alongside Jest), not a test
failure.

Also adds a mocks/handlers.ts handler for the new
/2/submitQuestionAbstention/ endpoint (previously unhandled in Playwright's
MSW fixture - requests to it silently fell through to the real network)
and wires it into the Level 1 NOT SURE Playwright spec, extending that
test's existing 'no printing vote cast' assertion with a payload check on
the new abstention POST.
…notsure-skip

# Conflicts:
#	.github/coverage-acks.txt
@WilfordGrimley
WilfordGrimley merged commit 1c04435 into master Aug 6, 2026
21 checks passed
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