From b53948db72048007cd32f44f3e23a39fd7ea6820 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Wed, 22 Oct 2025 13:18:18 -0600 Subject: [PATCH] can get exact answers --- src/app/providersQueryClientUtils.ts | 1 + .../canvas/services/canvasQuizService.ts | 11 +++- .../course/courseItemFileStorageService.ts | 7 +-- .../quizMarkdown/numericalQuestion.test.ts | 22 ++++++++ .../quizDeterministicChecks.test.ts | 32 +++++++++++ .../quizMarkdown/testAnswer.test.ts | 5 +- .../local/quizzes/models/localQuiz.ts | 22 +------- .../local/quizzes/models/localQuizQuestion.ts | 55 ++++++++----------- .../models/utils/quizFeedbackMarkdownUtils.ts | 35 +----------- .../utils/quizQuestionAnswerMarkdownUtils.ts | 2 + 10 files changed, 97 insertions(+), 95 deletions(-) diff --git a/src/app/providersQueryClientUtils.ts b/src/app/providersQueryClientUtils.ts index fc2ffea..9d19027 100644 --- a/src/app/providersQueryClientUtils.ts +++ b/src/app/providersQueryClientUtils.ts @@ -17,6 +17,7 @@ export function makeQueryClient() { // refetchOnMount: false, }, mutations: { + retry: 0, onError: (error) => { const message = getAxiosErrorMessage(error as AxiosError); console.error("Mutation error:", message); diff --git a/src/features/canvas/services/canvasQuizService.ts b/src/features/canvas/services/canvasQuizService.ts index 50a7eb9..9459adc 100644 --- a/src/features/canvas/services/canvasQuizService.ts +++ b/src/features/canvas/services/canvasQuizService.ts @@ -16,7 +16,7 @@ import { rateLimitAwarePost, } from "./canvasWebRequestUtils"; -export const getAnswers = ( +export const getAnswersForCanvas = ( question: LocalQuizQuestion, settings: LocalCourseSettings ) => { @@ -32,6 +32,13 @@ export const getAnswers = ( }; }); + if (question.questionType === QuestionType.NUMERICAL) { + return question.answers.map((answer) => ({ + numerical_answer_type: answer.numericalAnswerType, + exact: answer.numericAnswer, + })); + } + return question.answers.map((answer) => ({ answer_html: markdownToHTMLSafe({ markdownString: answer.text, settings }), answer_weight: answer.correct ? 100 : 0, @@ -64,7 +71,7 @@ const createQuestionOnly = async ( question_type: getQuestionTypeForCanvas(question), points_possible: question.points, position, - answers: getAnswers(question, settings), + answers: getAnswersForCanvas(question, settings), correct_comments: question.incorrectComments, incorrect_comments: question.incorrectComments, neutral_comments: question.neutralComments, diff --git a/src/features/local/course/courseItemFileStorageService.ts b/src/features/local/course/courseItemFileStorageService.ts index 0cb9d86..29a9f88 100644 --- a/src/features/local/course/courseItemFileStorageService.ts +++ b/src/features/local/course/courseItemFileStorageService.ts @@ -13,9 +13,8 @@ import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorage import { localPageMarkdownUtils, } from "@/features/local/pages/localCoursePageModels"; -import { - localQuizMarkdownUtils, -} from "@/features/local/quizzes/models/localQuiz"; +import { quizMarkdownUtils } from "../quizzes/models/utils/quizMarkdownUtils"; + const getItemFileNames = async ({ courseName, @@ -61,7 +60,7 @@ const getItem = async ({ name ) as CourseItemReturnType; } else if (type === "Quiz") { - return localQuizMarkdownUtils.parseMarkdown( + return quizMarkdownUtils.parseMarkdown( rawFile, name ) as CourseItemReturnType; diff --git a/src/features/local/parsingTests/quizMarkdown/numericalQuestion.test.ts b/src/features/local/parsingTests/quizMarkdown/numericalQuestion.test.ts index 989b923..14082f4 100644 --- a/src/features/local/parsingTests/quizMarkdown/numericalQuestion.test.ts +++ b/src/features/local/parsingTests/quizMarkdown/numericalQuestion.test.ts @@ -25,4 +25,26 @@ What is 2+3? expect(question.questionType).toBe(QuestionType.NUMERICAL); expect(question.answers[0].numericAnswer).toBe(5); }); +// it("can parse question with range answers", () => { +// const name = "Test Quiz"; +// const rawMarkdownQuiz = ` +// ShuffleAnswers: true +// OneQuestionAtATime: false +// DueAt: 08/21/2023 23:59:00 +// LockAt: 08/21/2023 23:59:00 +// AssignmentGroup: Assignments +// AllowedAttempts: -1 +// Description: quiz description +// --- +// What is 2+3? +// = 5 +// `; + +// const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name); +// const question = quiz.questions[0]; + +// expect(question.text).toBe("What is 2+3?"); +// expect(question.questionType).toBe(QuestionType.NUMERICAL); +// expect(question.answers[0].numericAnswer).toBe(5); +// }); }); diff --git a/src/features/local/parsingTests/quizMarkdown/quizDeterministicChecks.test.ts b/src/features/local/parsingTests/quizMarkdown/quizDeterministicChecks.test.ts index f3c4986..778a998 100644 --- a/src/features/local/parsingTests/quizMarkdown/quizDeterministicChecks.test.ts +++ b/src/features/local/parsingTests/quizMarkdown/quizDeterministicChecks.test.ts @@ -201,6 +201,38 @@ describe("QuizDeterministicChecks", () => { const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz); const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name); + expect(parsedQuiz).toEqual(quiz); + }); + it("SerializationIsDeterministic Numeric with exact answer", () => { + const name = "Test Quiz"; + const quiz: LocalQuiz = { + name, + description: "quiz description", + lockAt: "08/21/2023 23:59:00", + dueAt: "08/21/2023 23:59:00", + shuffleAnswers: true, + oneQuestionAtATime: true, + password: undefined, + localAssignmentGroupName: "Assignments", + questions: [ + { + text: "test numeric", + questionType: QuestionType.NUMERICAL, + points: 1, + matchDistractors: [], + answers: [ + { text: "= 42", correct: true, numericalAnswerType: "exact_answer", numericAnswer: 42 }, + ], + }, + ], + allowedAttempts: -1, + showCorrectAnswers: true, + + }; + + const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz); + const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name); + expect(parsedQuiz).toEqual(quiz); }); }); diff --git a/src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts b/src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts index 37d9c95..cf2b013 100644 --- a/src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts +++ b/src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts @@ -1,10 +1,9 @@ import { getQuestionTypeForCanvas, - getAnswers, + getAnswersForCanvas, } from "@/features/canvas/services/canvasQuizService"; import { QuestionType, - zodQuestionType, } from "@/features/local/quizzes/models/localQuizQuestion"; import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils"; import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils"; @@ -255,7 +254,7 @@ short_answer= const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name); const firstQuestion = quiz.questions[0]; - const answers = getAnswers(firstQuestion, { + const answers = getAnswersForCanvas(firstQuestion, { name: "", assignmentGroups: [], daysOfWeek: [], diff --git a/src/features/local/quizzes/models/localQuiz.ts b/src/features/local/quizzes/models/localQuiz.ts index af4e6e3..2fed333 100644 --- a/src/features/local/quizzes/models/localQuiz.ts +++ b/src/features/local/quizzes/models/localQuiz.ts @@ -1,22 +1,7 @@ import { z } from "zod"; -import { LocalQuizQuestion, zodLocalQuizQuestion } from "./localQuizQuestion"; -import { quizMarkdownUtils } from "./utils/quizMarkdownUtils"; +import { zodLocalQuizQuestion } from "./localQuizQuestion"; import { IModuleItem } from "@/features/local/modules/IModuleItem"; -export interface LocalQuiz extends IModuleItem { - name: string; - description: string; - password?: string; - lockAt?: string; // ISO 8601 date string - dueAt: string; // ISO 8601 date string - shuffleAnswers: boolean; - showCorrectAnswers: boolean; - oneQuestionAtATime: boolean; - localAssignmentGroupName?: string; - allowedAttempts: number; - questions: LocalQuizQuestion[]; -} - export const zodLocalQuiz = z.object({ name: z.string(), description: z.string(), @@ -31,7 +16,4 @@ export const zodLocalQuiz = z.object({ questions: zodLocalQuizQuestion.array(), }); -export const localQuizMarkdownUtils = { - parseMarkdown: quizMarkdownUtils.parseMarkdown, - toMarkdown: quizMarkdownUtils.toMarkdown, -}; +export interface LocalQuiz extends IModuleItem, z.infer {} diff --git a/src/features/local/quizzes/models/localQuizQuestion.ts b/src/features/local/quizzes/models/localQuizQuestion.ts index fa6a50b..f0336cb 100644 --- a/src/features/local/quizzes/models/localQuizQuestion.ts +++ b/src/features/local/quizzes/models/localQuizQuestion.ts @@ -1,40 +1,30 @@ import { z } from "zod"; -import { - LocalQuizQuestionAnswer, - zodLocalQuizQuestionAnswer, -} from "./localQuizQuestionAnswer"; - -export enum QuestionType { - MULTIPLE_ANSWERS = "multiple_answers", - MULTIPLE_CHOICE = "multiple_choice", - ESSAY = "essay", - SHORT_ANSWER = "short_answer", - MATCHING = "matching", - NONE = "", - SHORT_ANSWER_WITH_ANSWERS = "short_answer=", - NUMERICAL = "numerical", -} +import { zodLocalQuizQuestionAnswer } from "./localQuizQuestionAnswer"; export const zodQuestionType = z.enum([ - QuestionType.MULTIPLE_ANSWERS, - QuestionType.MULTIPLE_CHOICE, - QuestionType.ESSAY, - QuestionType.SHORT_ANSWER, - QuestionType.MATCHING, - QuestionType.NONE, - QuestionType.SHORT_ANSWER_WITH_ANSWERS, + "multiple_answers", + "multiple_choice", + "essay", + "short_answer", + "matching", + "", + "short_answer=", + "numerical", ]); -export interface LocalQuizQuestion { - text: string; - questionType: QuestionType; - points: number; - answers: LocalQuizQuestionAnswer[]; - matchDistractors: string[]; - correctComments?: string; - incorrectComments?: string; - neutralComments?: string; -} +export const QuestionType = { + MULTIPLE_ANSWERS: "multiple_answers", + MULTIPLE_CHOICE: "multiple_choice", + ESSAY: "essay", + SHORT_ANSWER: "short_answer", + MATCHING: "matching", + NONE: "", + SHORT_ANSWER_WITH_ANSWERS: "short_answer=", + NUMERICAL: "numerical", +} as const; + +export type QuestionType = z.infer; + export const zodLocalQuizQuestion = z.object({ text: z.string(), questionType: zodQuestionType, @@ -45,3 +35,4 @@ export const zodLocalQuizQuestion = z.object({ incorrectComments: z.string().optional(), neutralComments: z.string().optional(), }); +export type LocalQuizQuestion = z.infer; diff --git a/src/features/local/quizzes/models/utils/quizFeedbackMarkdownUtils.ts b/src/features/local/quizzes/models/utils/quizFeedbackMarkdownUtils.ts index a5aabfd..01b380f 100644 --- a/src/features/local/quizzes/models/utils/quizFeedbackMarkdownUtils.ts +++ b/src/features/local/quizzes/models/utils/quizFeedbackMarkdownUtils.ts @@ -1,36 +1,3 @@ -type FeedbackType = "+" | "-" | "..."; - -const extractFeedbackContent = ( - trimmedLine: string, - feedbackType: FeedbackType -): string => { - if (trimmedLine === feedbackType) return ""; - - const prefixLength = feedbackType === "..." ? 4 : 2; // "... " is 4 chars, "+ " and "- " are 2 - return trimmedLine.substring(prefixLength); -}; - -const saveFeedback = ( - feedbackType: FeedbackType | null, - feedbackLines: string[], - comments: { - correct?: string; - incorrect?: string; - neutral?: string; - } -): void => { - if (!feedbackType || feedbackLines.length === 0) return; - - const feedbackText = feedbackLines.join("\n"); - if (feedbackType === "+") { - comments.correct = feedbackText; - } else if (feedbackType === "-") { - comments.incorrect = feedbackText; - } else if (feedbackType === "...") { - comments.neutral = feedbackText; - } -}; - type feedbackTypeOptions = "correct" | "incorrect" | "neutral" | "none"; export const quizFeedbackMarkdownUtils = { @@ -78,7 +45,7 @@ export const quizFeedbackMarkdownUtils = { .trim(); currentFeedbackType = lineFeedbackType; comments[lineFeedbackType].push(lineWithoutIndicator); - + } else { otherLines.push(line); } diff --git a/src/features/local/quizzes/models/utils/quizQuestionAnswerMarkdownUtils.ts b/src/features/local/quizzes/models/utils/quizQuestionAnswerMarkdownUtils.ts index 0367eb2..54c6207 100644 --- a/src/features/local/quizzes/models/utils/quizQuestionAnswerMarkdownUtils.ts +++ b/src/features/local/quizzes/models/utils/quizQuestionAnswerMarkdownUtils.ts @@ -199,6 +199,8 @@ export const quizQuestionAnswerMarkdownUtils = { return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`; } else if (question.questionType === "matching") { return `^ ${answer.text} - ${answer.matchedText}`; + } else if (question.questionType === "numerical") { + return `= ${answer.numericAnswer}`; } else { const questionLetter = String.fromCharCode(97 + index); const correctIndicator = answer.correct ? "*" : "";