can get exact answers

This commit is contained in:
2025-10-22 13:18:18 -06:00
parent d9f7e7b3e9
commit b53948db72
10 changed files with 97 additions and 95 deletions

View File

@@ -17,6 +17,7 @@ export function makeQueryClient() {
// refetchOnMount: false, // refetchOnMount: false,
}, },
mutations: { mutations: {
retry: 0,
onError: (error) => { onError: (error) => {
const message = getAxiosErrorMessage(error as AxiosError); const message = getAxiosErrorMessage(error as AxiosError);
console.error("Mutation error:", message); console.error("Mutation error:", message);

View File

@@ -16,7 +16,7 @@ import {
rateLimitAwarePost, rateLimitAwarePost,
} from "./canvasWebRequestUtils"; } from "./canvasWebRequestUtils";
export const getAnswers = ( export const getAnswersForCanvas = (
question: LocalQuizQuestion, question: LocalQuizQuestion,
settings: LocalCourseSettings 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) => ({ return question.answers.map((answer) => ({
answer_html: markdownToHTMLSafe({ markdownString: answer.text, settings }), answer_html: markdownToHTMLSafe({ markdownString: answer.text, settings }),
answer_weight: answer.correct ? 100 : 0, answer_weight: answer.correct ? 100 : 0,
@@ -64,7 +71,7 @@ const createQuestionOnly = async (
question_type: getQuestionTypeForCanvas(question), question_type: getQuestionTypeForCanvas(question),
points_possible: question.points, points_possible: question.points,
position, position,
answers: getAnswers(question, settings), answers: getAnswersForCanvas(question, settings),
correct_comments: question.incorrectComments, correct_comments: question.incorrectComments,
incorrect_comments: question.incorrectComments, incorrect_comments: question.incorrectComments,
neutral_comments: question.neutralComments, neutral_comments: question.neutralComments,

View File

@@ -13,9 +13,8 @@ import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorage
import { import {
localPageMarkdownUtils, localPageMarkdownUtils,
} from "@/features/local/pages/localCoursePageModels"; } from "@/features/local/pages/localCoursePageModels";
import { import { quizMarkdownUtils } from "../quizzes/models/utils/quizMarkdownUtils";
localQuizMarkdownUtils,
} from "@/features/local/quizzes/models/localQuiz";
const getItemFileNames = async ({ const getItemFileNames = async ({
courseName, courseName,
@@ -61,7 +60,7 @@ const getItem = async <T extends CourseItemType>({
name name
) as CourseItemReturnType<T>; ) as CourseItemReturnType<T>;
} else if (type === "Quiz") { } else if (type === "Quiz") {
return localQuizMarkdownUtils.parseMarkdown( return quizMarkdownUtils.parseMarkdown(
rawFile, rawFile,
name name
) as CourseItemReturnType<T>; ) as CourseItemReturnType<T>;

View File

@@ -25,4 +25,26 @@ What is 2+3?
expect(question.questionType).toBe(QuestionType.NUMERICAL); expect(question.questionType).toBe(QuestionType.NUMERICAL);
expect(question.answers[0].numericAnswer).toBe(5); 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);
// });
}); });

View File

@@ -201,6 +201,38 @@ describe("QuizDeterministicChecks", () => {
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz); const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name); 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); expect(parsedQuiz).toEqual(quiz);
}); });
}); });

View File

@@ -1,10 +1,9 @@
import { import {
getQuestionTypeForCanvas, getQuestionTypeForCanvas,
getAnswers, getAnswersForCanvas,
} from "@/features/canvas/services/canvasQuizService"; } from "@/features/canvas/services/canvasQuizService";
import { import {
QuestionType, QuestionType,
zodQuestionType,
} from "@/features/local/quizzes/models/localQuizQuestion"; } from "@/features/local/quizzes/models/localQuizQuestion";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils"; import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils"; import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
@@ -255,7 +254,7 @@ short_answer=
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name); const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0]; const firstQuestion = quiz.questions[0];
const answers = getAnswers(firstQuestion, { const answers = getAnswersForCanvas(firstQuestion, {
name: "", name: "",
assignmentGroups: [], assignmentGroups: [],
daysOfWeek: [], daysOfWeek: [],

View File

@@ -1,22 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { LocalQuizQuestion, zodLocalQuizQuestion } from "./localQuizQuestion"; import { zodLocalQuizQuestion } from "./localQuizQuestion";
import { quizMarkdownUtils } from "./utils/quizMarkdownUtils";
import { IModuleItem } from "@/features/local/modules/IModuleItem"; 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({ export const zodLocalQuiz = z.object({
name: z.string(), name: z.string(),
description: z.string(), description: z.string(),
@@ -31,7 +16,4 @@ export const zodLocalQuiz = z.object({
questions: zodLocalQuizQuestion.array(), questions: zodLocalQuizQuestion.array(),
}); });
export const localQuizMarkdownUtils = { export interface LocalQuiz extends IModuleItem, z.infer<typeof zodLocalQuiz> {}
parseMarkdown: quizMarkdownUtils.parseMarkdown,
toMarkdown: quizMarkdownUtils.toMarkdown,
};

View File

@@ -1,40 +1,30 @@
import { z } from "zod"; import { z } from "zod";
import { import { zodLocalQuizQuestionAnswer } from "./localQuizQuestionAnswer";
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",
}
export const zodQuestionType = z.enum([ export const zodQuestionType = z.enum([
QuestionType.MULTIPLE_ANSWERS, "multiple_answers",
QuestionType.MULTIPLE_CHOICE, "multiple_choice",
QuestionType.ESSAY, "essay",
QuestionType.SHORT_ANSWER, "short_answer",
QuestionType.MATCHING, "matching",
QuestionType.NONE, "",
QuestionType.SHORT_ANSWER_WITH_ANSWERS, "short_answer=",
"numerical",
]); ]);
export interface LocalQuizQuestion { export const QuestionType = {
text: string; MULTIPLE_ANSWERS: "multiple_answers",
questionType: QuestionType; MULTIPLE_CHOICE: "multiple_choice",
points: number; ESSAY: "essay",
answers: LocalQuizQuestionAnswer[]; SHORT_ANSWER: "short_answer",
matchDistractors: string[]; MATCHING: "matching",
correctComments?: string; NONE: "",
incorrectComments?: string; SHORT_ANSWER_WITH_ANSWERS: "short_answer=",
neutralComments?: string; NUMERICAL: "numerical",
} } as const;
export type QuestionType = z.infer<typeof zodQuestionType>;
export const zodLocalQuizQuestion = z.object({ export const zodLocalQuizQuestion = z.object({
text: z.string(), text: z.string(),
questionType: zodQuestionType, questionType: zodQuestionType,
@@ -45,3 +35,4 @@ export const zodLocalQuizQuestion = z.object({
incorrectComments: z.string().optional(), incorrectComments: z.string().optional(),
neutralComments: z.string().optional(), neutralComments: z.string().optional(),
}); });
export type LocalQuizQuestion = z.infer<typeof zodLocalQuizQuestion>;

View File

@@ -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"; type feedbackTypeOptions = "correct" | "incorrect" | "neutral" | "none";
export const quizFeedbackMarkdownUtils = { export const quizFeedbackMarkdownUtils = {

View File

@@ -199,6 +199,8 @@ export const quizQuestionAnswerMarkdownUtils = {
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`; return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
} else if (question.questionType === "matching") { } else if (question.questionType === "matching") {
return `^ ${answer.text} - ${answer.matchedText}`; return `^ ${answer.text} - ${answer.matchedText}`;
} else if (question.questionType === "numerical") {
return `= ${answer.numericAnswer}`;
} else { } else {
const questionLetter = String.fromCharCode(97 + index); const questionLetter = String.fromCharCode(97 + index);
const correctIndicator = answer.correct ? "*" : ""; const correctIndicator = answer.correct ? "*" : "";