mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
8552
package-lock.json
generated
Normal file
8552
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"form-data": "^4.0.1",
|
"form-data": "^4.0.1",
|
||||||
"jsdom": "^25.0.0",
|
"jsdom": "^25.0.0",
|
||||||
|
"marked-katex-extension": "^5.1.4",
|
||||||
"next": "^15.0.2",
|
"next": "^15.0.2",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export enum QuestionType {
|
|||||||
SHORT_ANSWER = "short_answer",
|
SHORT_ANSWER = "short_answer",
|
||||||
MATCHING = "matching",
|
MATCHING = "matching",
|
||||||
NONE = "",
|
NONE = "",
|
||||||
|
SHORT_ANSWER_WITH_ANSWERS = "short_answer=",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zodQuestionType = z.enum([
|
export const zodQuestionType = z.enum([
|
||||||
@@ -20,6 +21,7 @@ export const zodQuestionType = z.enum([
|
|||||||
QuestionType.SHORT_ANSWER,
|
QuestionType.SHORT_ANSWER,
|
||||||
QuestionType.MATCHING,
|
QuestionType.MATCHING,
|
||||||
QuestionType.NONE,
|
QuestionType.NONE,
|
||||||
|
QuestionType.SHORT_ANSWER_WITH_ANSWERS,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export interface LocalQuizQuestion {
|
export interface LocalQuizQuestion {
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ const getQuestionType = (
|
|||||||
"short_answer"
|
"short_answer"
|
||||||
)
|
)
|
||||||
return QuestionType.SHORT_ANSWER;
|
return QuestionType.SHORT_ANSWER;
|
||||||
|
if (
|
||||||
|
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() ===
|
||||||
|
"short_answer="
|
||||||
|
)
|
||||||
|
return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
|
||||||
|
|
||||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||||
linesWithoutPoints,
|
linesWithoutPoints,
|
||||||
@@ -97,6 +102,7 @@ const getAnswers = (
|
|||||||
questionIndex: number,
|
questionIndex: number,
|
||||||
questionType: string
|
questionType: string
|
||||||
): LocalQuizQuestionAnswer[] => {
|
): LocalQuizQuestionAnswer[] => {
|
||||||
|
if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS) linesWithoutPoints = linesWithoutPoints.slice(0, linesWithoutPoints.length - 1);
|
||||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||||
linesWithoutPoints,
|
linesWithoutPoints,
|
||||||
questionIndex
|
questionIndex
|
||||||
@@ -149,6 +155,8 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
question.questionType === "essay" ||
|
question.questionType === "essay" ||
|
||||||
question.questionType === "short_answer"
|
question.questionType === "short_answer"
|
||||||
? question.questionType
|
? question.questionType
|
||||||
|
: question.questionType === QuestionType.SHORT_ANSWER_WITH_ANSWERS
|
||||||
|
? `\n${QuestionType.SHORT_ANSWER_WITH_ANSWERS}`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return `Points: ${question.points}\n${question.text}\n${answersText}${distractorText}${questionTypeIndicator}`;
|
return `Points: ${question.points}\n${question.text}\n${answersText}${distractorText}${questionTypeIndicator}`;
|
||||||
@@ -214,6 +222,7 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
"multiple_choice",
|
"multiple_choice",
|
||||||
"multiple_answers",
|
"multiple_answers",
|
||||||
"matching",
|
"matching",
|
||||||
|
"short_answer=",
|
||||||
];
|
];
|
||||||
const answers = typesWithAnswers.includes(questionType)
|
const answers = typesWithAnswers.includes(questionType)
|
||||||
? getAnswers(linesWithoutPoints, questionIndex, questionType)
|
? getAnswers(linesWithoutPoints, questionIndex, questionType)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { LocalQuiz } from "../../quiz/localQuiz";
|
|||||||
import { quizMarkdownUtils } from "../../quiz/utils/quizMarkdownUtils";
|
import { quizMarkdownUtils } from "../../quiz/utils/quizMarkdownUtils";
|
||||||
import { QuestionType } from "@/models/local/quiz/localQuizQuestion";
|
import { QuestionType } from "@/models/local/quiz/localQuizQuestion";
|
||||||
import { quizQuestionMarkdownUtils } from "@/models/local/quiz/utils/quizQuestionMarkdownUtils";
|
import { quizQuestionMarkdownUtils } from "@/models/local/quiz/utils/quizQuestionMarkdownUtils";
|
||||||
|
import { markdownToHtmlNoImages, markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||||
|
|
||||||
// Test suite for QuizMarkdown
|
// Test suite for QuizMarkdown
|
||||||
describe("QuizMarkdownTests", () => {
|
describe("QuizMarkdownTests", () => {
|
||||||
@@ -253,4 +254,33 @@ short answer
|
|||||||
const firstQuestion = quiz.questions[0];
|
const firstQuestion = quiz.questions[0];
|
||||||
expect(firstQuestion.points).toBe(4.56);
|
expect(firstQuestion.points).toBe(4.56);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can parse quiz with latex in a question", () => {
|
||||||
|
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: this is the
|
||||||
|
multi line
|
||||||
|
description
|
||||||
|
---
|
||||||
|
Points: 2
|
||||||
|
|
||||||
|
This is latex: $x_2$
|
||||||
|
|
||||||
|
*a) true
|
||||||
|
b) false
|
||||||
|
|
||||||
|
endline`;
|
||||||
|
|
||||||
|
const quizHtml = markdownToHtmlNoImages(rawMarkdownQuiz);
|
||||||
|
expect(quizHtml).not.toContain("$");
|
||||||
|
expect(quizHtml).toContain("<mi>x</mi>");
|
||||||
|
expect(quizHtml).not.toContain("x_2");
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { QuestionType } from "../../quiz/localQuizQuestion";
|
import { getAnswers, getQuestionType } from "@/services/canvas/canvasQuizService";
|
||||||
|
import { QuestionType, zodQuestionType } from "../../quiz/localQuizQuestion";
|
||||||
import { quizMarkdownUtils } from "../../quiz/utils/quizMarkdownUtils";
|
import { quizMarkdownUtils } from "../../quiz/utils/quizMarkdownUtils";
|
||||||
import { quizQuestionMarkdownUtils } from "../../quiz/utils/quizQuestionMarkdownUtils";
|
import { quizQuestionMarkdownUtils } from "../../quiz/utils/quizQuestionMarkdownUtils";
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { LocalCourseSettings } from "../../localCourseSettings";
|
||||||
|
|
||||||
describe("TextAnswerTests", () => {
|
describe("TextAnswerTests", () => {
|
||||||
it("can parse essay", () => {
|
it("can parse essay", () => {
|
||||||
@@ -83,6 +85,39 @@ short_answer`;
|
|||||||
expect(questionMarkdown).toContain(expectedMarkdown);
|
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("short_answer= to markdown is correct", () => {
|
||||||
|
|
||||||
|
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: this is the
|
||||||
|
multi line
|
||||||
|
description
|
||||||
|
---
|
||||||
|
Which events are triggered when the user clicks on an input field?
|
||||||
|
*a) yes
|
||||||
|
*b) Yes
|
||||||
|
short_answer=
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const firstQuestion = quiz.questions[0];
|
||||||
|
|
||||||
|
const questionMarkdown =
|
||||||
|
quizQuestionMarkdownUtils.toMarkdown(firstQuestion);
|
||||||
|
const expectedMarkdown = `Points: 1
|
||||||
|
Which events are triggered when the user clicks on an input field?
|
||||||
|
*a) yes
|
||||||
|
*b) Yes
|
||||||
|
short_answer=`;
|
||||||
|
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||||
|
});
|
||||||
|
|
||||||
it("essay question to markdown is correct", () => {
|
it("essay question to markdown is correct", () => {
|
||||||
const name = "Test Quiz"
|
const name = "Test Quiz"
|
||||||
const rawMarkdownQuiz = `
|
const rawMarkdownQuiz = `
|
||||||
@@ -111,28 +146,128 @@ essay`;
|
|||||||
expect(questionMarkdown).toContain(expectedMarkdown);
|
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||||
});
|
});
|
||||||
|
|
||||||
// it("Can parse short answer with auto graded answers", () => {
|
it("Can parse short answer with auto graded answers", () => {
|
||||||
// const rawMarkdownQuiz = `
|
const name = "Test Quiz"
|
||||||
// Name: Test Quiz
|
const rawMarkdownQuiz = `
|
||||||
// ShuffleAnswers: true
|
ShuffleAnswers: true
|
||||||
// OneQuestionAtATime: false
|
OneQuestionAtATime: false
|
||||||
// DueAt: 08/21/2023 23:59:00
|
DueAt: 08/21/2023 23:59:00
|
||||||
// LockAt: 08/21/2023 23:59:00
|
LockAt: 08/21/2023 23:59:00
|
||||||
// AssignmentGroup: Assignments
|
AssignmentGroup: Assignments
|
||||||
// AllowedAttempts: -1
|
AllowedAttempts: -1
|
||||||
// Description: this is the
|
Description: this is the
|
||||||
// multi line
|
multi line
|
||||||
// description
|
description
|
||||||
// ---
|
---
|
||||||
// Which events are triggered when the user clicks on an input field?
|
Which events are triggered when the user clicks on an input field?
|
||||||
// *a) test
|
*a) test
|
||||||
// short_answer=
|
*b) other
|
||||||
// `;
|
short_answer=
|
||||||
|
`;
|
||||||
|
|
||||||
// const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
// const firstQuestion = quiz.questions[0];
|
const firstQuestion = quiz.questions[0];
|
||||||
|
expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
||||||
|
expect(firstQuestion.answers.length).toBe(2);
|
||||||
|
expect(firstQuestion.answers[0].text).toBe("test");
|
||||||
|
expect(firstQuestion.answers[1].text).toBe("other");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Can parse short answer with auto graded 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: this is the
|
||||||
|
multi line
|
||||||
|
description
|
||||||
|
---
|
||||||
|
Which events are triggered when the user clicks on an input field?
|
||||||
|
*a) test
|
||||||
|
*b) other
|
||||||
|
short_answer=
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const firstQuestion = quiz.questions[0];
|
||||||
|
expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
||||||
|
expect(firstQuestion.answers.length).toBe(2);
|
||||||
|
expect(firstQuestion.answers[0].text).toBe("test");
|
||||||
|
expect(firstQuestion.answers[1].text).toBe("other");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Has short_answer= type at the same position in types and zod types", () => {
|
||||||
|
expect(Object.values(zodQuestionType.Enum)).toEqual(Object.values(QuestionType));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Associates short_answer= questions with short_answer_question canvas question type", () => {
|
||||||
|
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: this is the
|
||||||
|
multi line
|
||||||
|
description
|
||||||
|
---
|
||||||
|
Which events are triggered when the user clicks on an input field?
|
||||||
|
*a) test
|
||||||
|
*b) other
|
||||||
|
short_answer=
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const firstQuestion = quiz.questions[0];
|
||||||
|
expect(getQuestionType(firstQuestion)).toBe("short_answer_question");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Includes answer_text in answers sent to canvas", () => {
|
||||||
|
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: this is the
|
||||||
|
multi line
|
||||||
|
description
|
||||||
|
---
|
||||||
|
Which events are triggered when the user clicks on an input field?
|
||||||
|
*a) test
|
||||||
|
*b) other
|
||||||
|
short_answer=
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const firstQuestion = quiz.questions[0];
|
||||||
|
const answers = getAnswers(firstQuestion, {
|
||||||
|
name: "",
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: [],
|
||||||
|
canvasId: 0,
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
defaultDueTime: {
|
||||||
|
hour: 0,
|
||||||
|
minute: 0
|
||||||
|
},
|
||||||
|
defaultAssignmentSubmissionTypes: [],
|
||||||
|
defaultFileUploadTypes: [],
|
||||||
|
holidays: [],
|
||||||
|
assets: []
|
||||||
|
})
|
||||||
|
expect(answers).toHaveLength(2);
|
||||||
|
const firstAnswer = answers[0];
|
||||||
|
expect(firstAnswer).toHaveProperty("answer_text");
|
||||||
|
});
|
||||||
|
|
||||||
// expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel";
|
import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel";
|
||||||
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||||
|
|
||||||
const getAnswers = (
|
export const getAnswers = (
|
||||||
question: LocalQuizQuestion,
|
question: LocalQuizQuestion,
|
||||||
settings: LocalCourseSettings
|
settings: LocalCourseSettings
|
||||||
) => {
|
) => {
|
||||||
@@ -25,9 +25,16 @@ const getAnswers = (
|
|||||||
return question.answers.map((answer) => ({
|
return question.answers.map((answer) => ({
|
||||||
answer_html: markdownToHTMLSafe(answer.text, settings),
|
answer_html: markdownToHTMLSafe(answer.text, settings),
|
||||||
answer_weight: answer.correct ? 100 : 0,
|
answer_weight: answer.correct ? 100 : 0,
|
||||||
|
answer_text: answer.text,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getQuestionType = (
|
||||||
|
question: LocalQuizQuestion
|
||||||
|
) => {
|
||||||
|
return `${question.questionType.replace("=", "")}_question`;
|
||||||
|
}
|
||||||
|
|
||||||
const createQuestionOnly = async (
|
const createQuestionOnly = async (
|
||||||
canvasCourseId: number,
|
canvasCourseId: number,
|
||||||
canvasQuizId: number,
|
canvasQuizId: number,
|
||||||
@@ -41,7 +48,7 @@ const createQuestionOnly = async (
|
|||||||
const body = {
|
const body = {
|
||||||
question: {
|
question: {
|
||||||
question_text: markdownToHTMLSafe(question.text, settings),
|
question_text: markdownToHTMLSafe(question.text, settings),
|
||||||
question_type: `${question.questionType}_question`,
|
question_type: getQuestionType(question),
|
||||||
points_possible: question.points,
|
points_possible: question.points,
|
||||||
position,
|
position,
|
||||||
answers: getAnswers(question, settings),
|
answers: getAnswers(question, settings),
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||||
|
import markedKatex from "marked-katex-extension";
|
||||||
|
|
||||||
|
marked.use(markedKatex({
|
||||||
|
throwOnError: false,
|
||||||
|
output: "mathml"
|
||||||
|
}));
|
||||||
|
|
||||||
export function extractImageSources(htmlString: string) {
|
export function extractImageSources(htmlString: string) {
|
||||||
const srcUrls = [];
|
const srcUrls = [];
|
||||||
@@ -40,16 +46,12 @@ export function markdownToHTMLSafe(
|
|||||||
markdownString: string,
|
markdownString: string,
|
||||||
settings: LocalCourseSettings
|
settings: LocalCourseSettings
|
||||||
) {
|
) {
|
||||||
const clean = DOMPurify.sanitize(
|
return markdownToHtmlNoImages(markdownString);
|
||||||
marked.parse(markdownString, { async: false, pedantic: false, gfm: true })
|
|
||||||
);
|
|
||||||
// return convertImagesToCanvasImages(clean, settings);
|
|
||||||
return clean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markdownToHtmlNoImages(markdownString: string) {
|
export function markdownToHtmlNoImages(markdownString: string) {
|
||||||
const clean = DOMPurify.sanitize(
|
const clean = DOMPurify.sanitize(
|
||||||
marked.parse(markdownString, { async: false, pedantic: false, gfm: true })
|
marked.parse(markdownString, { async: false, pedantic: false, gfm: true })
|
||||||
);
|
).replaceAll(/>[^<>]*<\/math>/g, "></math>");
|
||||||
return clean;
|
return clean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user