From e80d0133a0bfa74f47eaea1ef663b9aa06924e51 Mon Sep 17 00:00:00 2001 From: Barna Magyarkuti Date: Mon, 19 May 2025 17:08:21 +0100 Subject: [PATCH 1/2] submit inline questions in a sequence This avoids a race condition where the API could not save multiple answers that were submitted at the same time for anonymous users. This is because, for anonymous users, when saving an answer, the API creates a lock using the user's ID, see: https://github.com/isaacphysics/isaac-api/blob/ 51296605cef5b532f6f6b90d9a9575001172d4a6/src/main/java/uk/ac/cam/cl/ dtg/isaac/quiz/PgQuestionAttempts.java#L92. --- src/app/components/content/IsaacInlineRegion.tsx | 4 ++-- src/app/services/questions.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/components/content/IsaacInlineRegion.tsx b/src/app/components/content/IsaacInlineRegion.tsx index 4b61145b65..b04082d924 100644 --- a/src/app/components/content/IsaacInlineRegion.tsx +++ b/src/app/components/content/IsaacInlineRegion.tsx @@ -84,14 +84,14 @@ export const useInlineRegionPart = (pageQuestions: AppQuestionDTO[] | undefined) }; }; -export const submitInlineRegion = (inlineContext: ContextType, currentGameboard: GameboardDTO | undefined, currentUser: any, pageQuestions: AppQuestionDTO[] | undefined, dispatch: any, hidingAttempts : boolean) => { +export const submitInlineRegion = async (inlineContext: ContextType, currentGameboard: GameboardDTO | undefined, currentUser: any, pageQuestions: AppQuestionDTO[] | undefined, dispatch: any, hidingAttempts : boolean) => { if (inlineContext && inlineContext.docId && pageQuestions) { inlineContext.setSubmitting(true); const inlineQuestions = pageQuestions.filter(q => inlineContext.docId && q.id?.startsWith(inlineContext.docId) && q.id.includes("|inline-question:")); // we submit all modified answers, and those with undefined values. we must submit this latter group to get a validation response at the same time as the other answers const modifiedInlineQuestions = inlineQuestions.filter(q => (q.id && inlineContext.modifiedQuestionIds.includes(q.id)) || (q.currentAttempt?.value === undefined && (q.bestAttempt === undefined || hidingAttempts))); for (const inlineQuestion of modifiedInlineQuestions) { - submitCurrentAttempt( + await submitCurrentAttempt( {currentAttempt: inlineQuestion.currentAttempt}, inlineQuestion.id as string, inlineQuestion.type as string, currentGameboard, currentUser, dispatch, inlineContext ); diff --git a/src/app/services/questions.ts b/src/app/services/questions.ts index b9ddf5e299..b975a028d1 100644 --- a/src/app/services/questions.ts +++ b/src/app/services/questions.ts @@ -182,7 +182,7 @@ export function useCurrentQuestionAttempt(questionId: strin }; } -export const submitCurrentAttempt = (questionPart: AppQuestionDTO | undefined, docId: string, questionType: string, currentGameboard: GameboardDTO | undefined, currentUser: any, dispatch: any, inlineContext?: ContextType) => { +export const submitCurrentAttempt = (questionPart: AppQuestionDTO | undefined, docId: string, questionType: string, currentGameboard: GameboardDTO | undefined, currentUser: any, dispatch: any, inlineContext?: ContextType): Promise => { if (questionPart?.currentAttempt) { // Notify Plausible that at least one question attempt has taken place today if (persistence.load(KEY.INITIAL_DAILY_QUESTION_ATTEMPT_TIME) == null || !wasTodayUTC(persistence.load(KEY.INITIAL_DAILY_QUESTION_ATTEMPT_TIME))) { @@ -190,7 +190,7 @@ export const submitCurrentAttempt = (questionPart: AppQuestionDTO | undefined, d trackEvent("question_attempted"); } - dispatch(attemptQuestion(docId, questionPart?.currentAttempt, questionType, currentGameboard?.id, inlineContext)); + const attempt = dispatch(attemptQuestion(docId, questionPart?.currentAttempt, questionType, currentGameboard?.id, inlineContext)); if (isLoggedIn(currentUser) && isNotPartiallyLoggedIn(currentUser) && currentGameboard?.id && !currentGameboard.savedToCurrentUser) { dispatch(saveGameboard({ @@ -199,7 +199,10 @@ export const submitCurrentAttempt = (questionPart: AppQuestionDTO | undefined, d redirectOnSuccess: false })); } + + return attempt; } + return Promise.resolve(); }; export const getMostRecentCorrectAttemptDate = (questions: AppQuestionDTO[] | undefined) => { From e184ebf1ff0db82b1edaf9281159432d4e3b55aa Mon Sep 17 00:00:00 2001 From: Barna Magyarkuti Date: Mon, 19 May 2025 17:34:16 +0100 Subject: [PATCH 2/2] make sure errors are still shown Previously, the synchronous operations after firing the requests would finish before the requests were actually sent. This meant it was okay to enable error reporting after triggering the request. Now that we wait for the requests to finish, it's important to enable error reporting before firing the requests. It doesn't really matter whether we reset the feedback field before or after the requests were sent, as the box only appears after the last request finishes, but it's still safer to do before sending the requests. --- src/app/components/content/IsaacInlineRegion.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/content/IsaacInlineRegion.tsx b/src/app/components/content/IsaacInlineRegion.tsx index b04082d924..342da676a1 100644 --- a/src/app/components/content/IsaacInlineRegion.tsx +++ b/src/app/components/content/IsaacInlineRegion.tsx @@ -87,6 +87,9 @@ export const useInlineRegionPart = (pageQuestions: AppQuestionDTO[] | undefined) export const submitInlineRegion = async (inlineContext: ContextType, currentGameboard: GameboardDTO | undefined, currentUser: any, pageQuestions: AppQuestionDTO[] | undefined, dispatch: any, hidingAttempts : boolean) => { if (inlineContext && inlineContext.docId && pageQuestions) { inlineContext.setSubmitting(true); + inlineContext.canShowWarningToast = true; + if (Object.keys(inlineContext.elementToQuestionMap).length > 1) inlineContext.setFeedbackIndex(0); + const inlineQuestions = pageQuestions.filter(q => inlineContext.docId && q.id?.startsWith(inlineContext.docId) && q.id.includes("|inline-question:")); // we submit all modified answers, and those with undefined values. we must submit this latter group to get a validation response at the same time as the other answers const modifiedInlineQuestions = inlineQuestions.filter(q => (q.id && inlineContext.modifiedQuestionIds.includes(q.id)) || (q.currentAttempt?.value === undefined && (q.bestAttempt === undefined || hidingAttempts))); @@ -96,8 +99,6 @@ export const submitInlineRegion = async (inlineContext: ContextType 1) inlineContext.setFeedbackIndex(0); } };