Skip to content

Commit

Permalink
Add guardrails for duplicate/excess answers in no-guess-queue mode.
Browse files Browse the repository at this point in the history
These guardrails exist for guess-queue mode but were missing from
no-guess-queue mode. This change brings parity between the two modes.

Note that:

1. This is not a firm guarantee against (unintended) duplicate answers,
since two submissions made at ~the same time won't detect the presence
of the other submission in time. Only a read-modify-write transaction
would prevent this.

2. If duplicate answers _are_ submitted, clearing one answer in
no-guess-queue mode will mark the puzzle as incorrect and clear the
answer, while leaving a correct guess in place. This is an inconsistent
state. The workaround is to clear all copies of the answer and resubmit
it.
  • Loading branch information
jpd236 committed Jan 5, 2025
1 parent a3855d7 commit 097b345
Showing 1 changed file with 39 additions and 6 deletions.
45 changes: 39 additions & 6 deletions imports/client/components/PuzzlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,8 @@ const PuzzleAnswerModal = React.forwardRef(
forwardedRef: React.Ref<PuzzleAnswerModalHandle>,
) => {
const [answer, setAnswer] = useState<string>("");
const [confirmingSubmit, setConfirmingSubmit] = useState<boolean>(false);
const [confirmationMessage, setConfirmationMessage] = useState<string>("");
const [submitState, setSubmitState] = useState<PuzzleAnswerSubmitState>(
PuzzleAnswerSubmitState.IDLE,
);
Expand All @@ -1675,14 +1677,38 @@ const PuzzleAnswerModal = React.forwardRef(
const onAnswerChange: NonNullable<FormControlProps["onChange"]> =
useCallback((e) => {
setAnswer(e.currentTarget.value);
setConfirmingSubmit(false);
}, []);

const onDismissError = useCallback(() => {
setSubmitState(PuzzleAnswerSubmitState.IDLE);
setSubmitError("");
}, []);

const solvedness = useMemo(() => {
return computeSolvedness(puzzle);
}, [puzzle]);

const onSubmit = useCallback(() => {
const strippedAnswer = answer.replaceAll(/\s/g, "");
const repeatAnswer = puzzle.answers.find((a) => {
return a.replaceAll(/\s/g, "") === strippedAnswer;
});
if ((repeatAnswer || solvedness !== "unsolved") && !confirmingSubmit) {
const repeatAnswerStr = repeatAnswer
? "This answer has already been submitted. "
: "";
const solvednessStr = {
solved: "This puzzle has already been solved. ",
noAnswers:
"This puzzle does not expect any answers to be submitted. ",
unsolved: "",
}[solvedness];
const msg = `${solvednessStr} ${repeatAnswerStr} Are you sure you want to submit this guess?`;
setConfirmationMessage(msg);
setConfirmingSubmit(true);
return;
}
setSubmitState(PuzzleAnswerSubmitState.SUBMITTING);
setSubmitError("");
addPuzzleAnswer.call(
Expand All @@ -1699,20 +1725,24 @@ const PuzzleAnswerModal = React.forwardRef(
setSubmitState(PuzzleAnswerSubmitState.IDLE);
hide();
}
setConfirmingSubmit(false);
},
);
}, [puzzle._id, answer, hide]);
}, [
puzzle._id,
puzzle.answers,
confirmingSubmit,
solvedness,
answer,
hide,
]);

return (
<ModalForm
ref={formRef}
title={`Submit answer to ${puzzle.title}`}
onSubmit={onSubmit}
submitLabel={
submitState === PuzzleAnswerSubmitState.SUBMITTING
? "Confirm Submit"
: "Submit"
}
submitLabel={confirmingSubmit ? "Confirm Submit" : "Submit"}
>
<FormGroup as={Row} className="mb-3">
<FormLabel column xs={3} htmlFor="jr-puzzle-answer">
Expand All @@ -1730,6 +1760,9 @@ const PuzzleAnswerModal = React.forwardRef(
</Col>
</FormGroup>

{confirmingSubmit ? (
<Alert variant="warning">{confirmationMessage}</Alert>
) : null}
{submitState === PuzzleAnswerSubmitState.FAILED ? (
<Alert variant="danger" dismissible onClose={onDismissError}>
{submitError ||
Expand Down

0 comments on commit 097b345

Please sign in to comment.