mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
fixed feedback, feedback only supported in descriptions, not in questions for now
This commit is contained in:
@@ -39,7 +39,7 @@ export const getAnswers = (
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQuestionType = (question: LocalQuizQuestion) => {
|
export const getQuestionTypeForCanvas = (question: LocalQuizQuestion) => {
|
||||||
return `${question.questionType.replace("=", "")}_question`;
|
return `${question.questionType.replace("=", "")}_question`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const createQuestionOnly = async (
|
|||||||
markdownString: question.text,
|
markdownString: question.text,
|
||||||
settings,
|
settings,
|
||||||
}),
|
}),
|
||||||
question_type: getQuestionType(question),
|
question_type: getQuestionTypeForCanvas(question),
|
||||||
points_possible: question.points,
|
points_possible: question.points,
|
||||||
position,
|
position,
|
||||||
answers: getAnswers(question, settings),
|
answers: getAnswers(question, settings),
|
||||||
|
|||||||
@@ -1,8 +1,302 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
|
import { QuestionType } 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 { LocalQuiz } from "../../quizzes/models/localQuiz";
|
||||||
|
|
||||||
describe("Question Feedback options", () => {
|
describe("Question Feedback options", () => {
|
||||||
|
it("can parse question with correct feedback", () => {
|
||||||
|
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
|
||||||
|
---
|
||||||
|
Points: 3
|
||||||
|
What is the purpose of a context switch?
|
||||||
|
+ Correct! The context switch is used to change the current process by swapping the registers and other state with a new process
|
||||||
|
*a) To change the current window you are on
|
||||||
|
b) To change the current process's status
|
||||||
|
*c) To swap the current process's registers for a new process's registers
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.correctComments).toBe(
|
||||||
|
"Correct! The context switch is used to change the current process by swapping the registers and other state with a new process"
|
||||||
|
);
|
||||||
|
expect(question.incorrectComments).toBeUndefined();
|
||||||
|
expect(question.neutralComments).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can parse question with incorrect feedback", () => {
|
||||||
|
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
|
||||||
|
---
|
||||||
|
Points: 3
|
||||||
|
What state does a process need to be in to be able to be scheduled?
|
||||||
|
- Incorrect! A process in ready state can be scheduled
|
||||||
|
*a) Ready
|
||||||
|
b) Running
|
||||||
|
c) Zombie
|
||||||
|
d) Embryo
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.incorrectComments).toBe(
|
||||||
|
"Incorrect! A process in ready state can be scheduled"
|
||||||
|
);
|
||||||
|
expect(question.correctComments).toBeUndefined();
|
||||||
|
expect(question.neutralComments).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can parse question with correct and incorrect feedback", () => {
|
||||||
|
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
|
||||||
|
---
|
||||||
|
Points: 3
|
||||||
|
What is the purpose of a context switch?
|
||||||
|
+ Correct! The context switch is used to change the current process
|
||||||
|
- Incorrect! The context switch is NOT used to change windows
|
||||||
|
*a) To change the current window you are on
|
||||||
|
b) To change the current process's status
|
||||||
|
*c) To swap the current process's registers for a new process's registers
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.correctComments).toBe(
|
||||||
|
"Correct! The context switch is used to change the current process"
|
||||||
|
);
|
||||||
|
expect(question.incorrectComments).toBe(
|
||||||
|
"Incorrect! The context switch is NOT used to change windows"
|
||||||
|
);
|
||||||
|
expect(question.neutralComments).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can parse question with neutral feedback", () => {
|
||||||
|
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
|
||||||
|
---
|
||||||
|
Points: 3
|
||||||
|
What is a prime number?
|
||||||
|
... This feedback will be shown regardless of the answer
|
||||||
|
*a) A number divisible only by 1 and itself
|
||||||
|
b) Any odd number
|
||||||
|
c) Any even number
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.neutralComments).toBe(
|
||||||
|
"This feedback will be shown regardless of the answer"
|
||||||
|
);
|
||||||
|
expect(question.correctComments).toBeUndefined();
|
||||||
|
expect(question.incorrectComments).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can parse question with all three feedback types", () => {
|
||||||
|
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
|
||||||
|
---
|
||||||
|
Points: 3
|
||||||
|
What is the purpose of a context switch?
|
||||||
|
+ Great job! You understand context switching
|
||||||
|
- Try reviewing the material on process management
|
||||||
|
... Context switches are a fundamental operating system concept
|
||||||
|
*a) To change the current window you are on
|
||||||
|
b) To change the current process's status
|
||||||
|
*c) To swap the current process's registers for a new process's registers
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.correctComments).toBe(
|
||||||
|
"Great job! You understand context switching"
|
||||||
|
);
|
||||||
|
expect(question.incorrectComments).toBe(
|
||||||
|
"Try reviewing the material on process management"
|
||||||
|
);
|
||||||
|
expect(question.neutralComments).toBe(
|
||||||
|
"Context switches are a fundamental operating system concept"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can parse multiline feedback", () => {
|
||||||
|
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
|
||||||
|
---
|
||||||
|
Points: 3
|
||||||
|
What is the purpose of a context switch?
|
||||||
|
+ Correct! The context switch is used to change the current process.
|
||||||
|
This is additional information on a new line.
|
||||||
|
- Incorrect! You should review the material.
|
||||||
|
Check your notes on process management.
|
||||||
|
*a) To change the current window you are on
|
||||||
|
b) To change the current process's status
|
||||||
|
*c) To swap the current process's registers for a new process's registers
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.correctComments).toBe(
|
||||||
|
"Correct! The context switch is used to change the current process.\nThis is additional information on a new line."
|
||||||
|
);
|
||||||
|
expect(question.incorrectComments).toBe(
|
||||||
|
"Incorrect! You should review the material.\nCheck your notes on process management."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("feedback can serialize to markdown", () => {
|
||||||
|
const quiz: LocalQuiz = {
|
||||||
|
name: "Test Quiz",
|
||||||
|
description: "quiz description",
|
||||||
|
lockAt: new Date(8640000000000000).toISOString(),
|
||||||
|
dueAt: new Date(8640000000000000).toISOString(),
|
||||||
|
shuffleAnswers: true,
|
||||||
|
oneQuestionAtATime: false,
|
||||||
|
localAssignmentGroupName: "Assignments",
|
||||||
|
allowedAttempts: -1,
|
||||||
|
showCorrectAnswers: false,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
text: "What is the purpose of a context switch?",
|
||||||
|
questionType: QuestionType.MULTIPLE_CHOICE,
|
||||||
|
points: 3,
|
||||||
|
correctComments: "Correct! Good job",
|
||||||
|
incorrectComments: "Incorrect! Try again",
|
||||||
|
neutralComments: "Context switches are important",
|
||||||
|
answers: [
|
||||||
|
{ correct: false, text: "To change the current window you are on" },
|
||||||
|
{ correct: true, text: "To swap registers" },
|
||||||
|
],
|
||||||
|
matchDistractors: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||||
|
|
||||||
|
expect(markdown).toContain("+ Correct! Good job");
|
||||||
|
expect(markdown).toContain("- Incorrect! Try again");
|
||||||
|
expect(markdown).toContain("... Context switches are important");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can parse question with alternative format using ellipsis for general feedback", () => {
|
||||||
|
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: An addition question
|
||||||
|
---
|
||||||
|
Points: 2
|
||||||
|
What is 2+3?
|
||||||
|
... General question feedback.
|
||||||
|
+ Feedback for correct answer.
|
||||||
|
- Feedback for incorrect answer.
|
||||||
|
a) 6
|
||||||
|
b) 1
|
||||||
|
*c) 5
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.text).toBe("What is 2+3?");
|
||||||
|
expect(question.points).toBe(2);
|
||||||
|
expect(question.neutralComments).toBe("General question feedback.");
|
||||||
|
expect(question.correctComments).toBe("Feedback for correct answer.");
|
||||||
|
expect(question.incorrectComments).toBe("Feedback for incorrect answer.");
|
||||||
|
expect(question.answers).toHaveLength(3);
|
||||||
|
expect(question.answers[0].text).toBe("6");
|
||||||
|
expect(question.answers[0].correct).toBe(false);
|
||||||
|
expect(question.answers[1].text).toBe("1");
|
||||||
|
expect(question.answers[1].correct).toBe(false);
|
||||||
|
expect(question.answers[2].text).toBe("5");
|
||||||
|
expect(question.answers[2].correct).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can parse multiline general feedback with ellipsis", () => {
|
||||||
|
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
|
||||||
|
---
|
||||||
|
Points: 2
|
||||||
|
What is 2+3?
|
||||||
|
...
|
||||||
|
General question feedback.
|
||||||
|
This continues on multiple lines.
|
||||||
|
+ Feedback for correct answer.
|
||||||
|
- Feedback for incorrect answer.
|
||||||
|
a) 6
|
||||||
|
b) 1
|
||||||
|
*c) 5
|
||||||
|
`;
|
||||||
|
|
||||||
|
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||||
|
const question = quiz.questions[0];
|
||||||
|
|
||||||
|
expect(question.neutralComments).toBe(
|
||||||
|
"General question feedback.\nThis continues on multiple lines."
|
||||||
|
);
|
||||||
|
expect(question.correctComments).toBe("Feedback for correct answer.");
|
||||||
|
expect(question.incorrectComments).toBe("Feedback for incorrect answer.");
|
||||||
|
});
|
||||||
it("essay questions can have feedback", () => {
|
it("essay questions can have feedback", () => {
|
||||||
const name = "Test Quiz";
|
const name = "Test Quiz";
|
||||||
const rawMarkdownQuiz = `
|
const rawMarkdownQuiz = `
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ describe("QuizDeterministicChecks", () => {
|
|||||||
],
|
],
|
||||||
allowedAttempts: -1,
|
allowedAttempts: -1,
|
||||||
showCorrectAnswers: true,
|
showCorrectAnswers: true,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||||
|
|||||||
@@ -281,297 +281,4 @@ b) false
|
|||||||
expect(quizHtml).not.toContain("x_2");
|
expect(quizHtml).not.toContain("x_2");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can parse question with correct feedback", () => {
|
|
||||||
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
|
|
||||||
---
|
|
||||||
Points: 3
|
|
||||||
What is the purpose of a context switch?
|
|
||||||
+ Correct! The context switch is used to change the current process by swapping the registers and other state with a new process
|
|
||||||
*a) To change the current window you are on
|
|
||||||
b) To change the current process's status
|
|
||||||
*c) To swap the current process's registers for a new process's registers
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.correctComments).toBe(
|
|
||||||
"Correct! The context switch is used to change the current process by swapping the registers and other state with a new process"
|
|
||||||
);
|
|
||||||
expect(question.incorrectComments).toBeUndefined();
|
|
||||||
expect(question.neutralComments).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can parse question with incorrect feedback", () => {
|
|
||||||
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
|
|
||||||
---
|
|
||||||
Points: 3
|
|
||||||
What state does a process need to be in to be able to be scheduled?
|
|
||||||
- Incorrect! A process in ready state can be scheduled
|
|
||||||
*a) Ready
|
|
||||||
b) Running
|
|
||||||
c) Zombie
|
|
||||||
d) Embryo
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.incorrectComments).toBe(
|
|
||||||
"Incorrect! A process in ready state can be scheduled"
|
|
||||||
);
|
|
||||||
expect(question.correctComments).toBeUndefined();
|
|
||||||
expect(question.neutralComments).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can parse question with correct and incorrect feedback", () => {
|
|
||||||
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
|
|
||||||
---
|
|
||||||
Points: 3
|
|
||||||
What is the purpose of a context switch?
|
|
||||||
+ Correct! The context switch is used to change the current process
|
|
||||||
- Incorrect! The context switch is NOT used to change windows
|
|
||||||
*a) To change the current window you are on
|
|
||||||
b) To change the current process's status
|
|
||||||
*c) To swap the current process's registers for a new process's registers
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.correctComments).toBe(
|
|
||||||
"Correct! The context switch is used to change the current process"
|
|
||||||
);
|
|
||||||
expect(question.incorrectComments).toBe(
|
|
||||||
"Incorrect! The context switch is NOT used to change windows"
|
|
||||||
);
|
|
||||||
expect(question.neutralComments).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can parse question with neutral feedback", () => {
|
|
||||||
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
|
|
||||||
---
|
|
||||||
Points: 3
|
|
||||||
What is a prime number?
|
|
||||||
... This feedback will be shown regardless of the answer
|
|
||||||
*a) A number divisible only by 1 and itself
|
|
||||||
b) Any odd number
|
|
||||||
c) Any even number
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.neutralComments).toBe(
|
|
||||||
"This feedback will be shown regardless of the answer"
|
|
||||||
);
|
|
||||||
expect(question.correctComments).toBeUndefined();
|
|
||||||
expect(question.incorrectComments).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can parse question with all three feedback types", () => {
|
|
||||||
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
|
|
||||||
---
|
|
||||||
Points: 3
|
|
||||||
What is the purpose of a context switch?
|
|
||||||
+ Great job! You understand context switching
|
|
||||||
- Try reviewing the material on process management
|
|
||||||
... Context switches are a fundamental operating system concept
|
|
||||||
*a) To change the current window you are on
|
|
||||||
b) To change the current process's status
|
|
||||||
*c) To swap the current process's registers for a new process's registers
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.correctComments).toBe(
|
|
||||||
"Great job! You understand context switching"
|
|
||||||
);
|
|
||||||
expect(question.incorrectComments).toBe(
|
|
||||||
"Try reviewing the material on process management"
|
|
||||||
);
|
|
||||||
expect(question.neutralComments).toBe(
|
|
||||||
"Context switches are a fundamental operating system concept"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can parse multiline feedback", () => {
|
|
||||||
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
|
|
||||||
---
|
|
||||||
Points: 3
|
|
||||||
What is the purpose of a context switch?
|
|
||||||
+ Correct! The context switch is used to change the current process.
|
|
||||||
This is additional information on a new line.
|
|
||||||
- Incorrect! You should review the material.
|
|
||||||
Check your notes on process management.
|
|
||||||
*a) To change the current window you are on
|
|
||||||
b) To change the current process's status
|
|
||||||
*c) To swap the current process's registers for a new process's registers
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.correctComments).toBe(
|
|
||||||
"Correct! The context switch is used to change the current process.\nThis is additional information on a new line."
|
|
||||||
);
|
|
||||||
expect(question.incorrectComments).toBe(
|
|
||||||
"Incorrect! You should review the material.\nCheck your notes on process management."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("feedback can serialize to markdown", () => {
|
|
||||||
const quiz: LocalQuiz = {
|
|
||||||
name: "Test Quiz",
|
|
||||||
description: "quiz description",
|
|
||||||
lockAt: new Date(8640000000000000).toISOString(),
|
|
||||||
dueAt: new Date(8640000000000000).toISOString(),
|
|
||||||
shuffleAnswers: true,
|
|
||||||
oneQuestionAtATime: false,
|
|
||||||
localAssignmentGroupName: "Assignments",
|
|
||||||
allowedAttempts: -1,
|
|
||||||
showCorrectAnswers: false,
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
text: "What is the purpose of a context switch?",
|
|
||||||
questionType: QuestionType.MULTIPLE_CHOICE,
|
|
||||||
points: 3,
|
|
||||||
correctComments: "Correct! Good job",
|
|
||||||
incorrectComments: "Incorrect! Try again",
|
|
||||||
neutralComments: "Context switches are important",
|
|
||||||
answers: [
|
|
||||||
{ correct: false, text: "To change the current window you are on" },
|
|
||||||
{ correct: true, text: "To swap registers" },
|
|
||||||
],
|
|
||||||
matchDistractors: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
|
||||||
|
|
||||||
expect(markdown).toContain("+ Correct! Good job");
|
|
||||||
expect(markdown).toContain("- Incorrect! Try again");
|
|
||||||
expect(markdown).toContain("... Context switches are important");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can parse question with alternative format using ellipsis for general feedback", () => {
|
|
||||||
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: An addition question
|
|
||||||
---
|
|
||||||
Points: 2
|
|
||||||
What is 2+3?
|
|
||||||
... General question feedback.
|
|
||||||
+ Feedback for correct answer.
|
|
||||||
- Feedback for incorrect answer.
|
|
||||||
a) 6
|
|
||||||
b) 1
|
|
||||||
*c) 5
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.text).toBe("What is 2+3?");
|
|
||||||
expect(question.points).toBe(2);
|
|
||||||
expect(question.neutralComments).toBe("General question feedback.");
|
|
||||||
expect(question.correctComments).toBe("Feedback for correct answer.");
|
|
||||||
expect(question.incorrectComments).toBe("Feedback for incorrect answer.");
|
|
||||||
expect(question.answers).toHaveLength(3);
|
|
||||||
expect(question.answers[0].text).toBe("6");
|
|
||||||
expect(question.answers[0].correct).toBe(false);
|
|
||||||
expect(question.answers[1].text).toBe("1");
|
|
||||||
expect(question.answers[1].correct).toBe(false);
|
|
||||||
expect(question.answers[2].text).toBe("5");
|
|
||||||
expect(question.answers[2].correct).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can parse multiline general feedback with ellipsis", () => {
|
|
||||||
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
|
|
||||||
---
|
|
||||||
Points: 2
|
|
||||||
What is 2+3?
|
|
||||||
...
|
|
||||||
General question feedback.
|
|
||||||
This continues on multiple lines.
|
|
||||||
+ Feedback for correct answer.
|
|
||||||
- Feedback for incorrect answer.
|
|
||||||
a) 6
|
|
||||||
b) 1
|
|
||||||
*c) 5
|
|
||||||
`;
|
|
||||||
|
|
||||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
|
||||||
const question = quiz.questions[0];
|
|
||||||
|
|
||||||
expect(question.neutralComments).toBe(
|
|
||||||
"General question feedback.\nThis continues on multiple lines."
|
|
||||||
);
|
|
||||||
expect(question.correctComments).toBe("Feedback for correct answer.");
|
|
||||||
expect(question.incorrectComments).toBe("Feedback for incorrect answer.");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { getQuestionType, getAnswers } from "@/features/canvas/services/canvasQuizService";
|
import {
|
||||||
|
getQuestionTypeForCanvas,
|
||||||
|
getAnswers,
|
||||||
|
} from "@/features/canvas/services/canvasQuizService";
|
||||||
import {
|
import {
|
||||||
QuestionType,
|
QuestionType,
|
||||||
zodQuestionType,
|
zodQuestionType,
|
||||||
@@ -232,7 +235,9 @@ 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(getQuestionType(firstQuestion)).toBe("short_answer_question");
|
expect(getQuestionTypeForCanvas(firstQuestion)).toBe(
|
||||||
|
"short_answer_question"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Includes answer_text in answers sent to canvas", () => {
|
it("Includes answer_text in answers sent to canvas", () => {
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
type FeedbackType = "+" | "-" | "...";
|
type FeedbackType = "+" | "-" | "...";
|
||||||
|
|
||||||
const isFeedbackStart = (
|
|
||||||
trimmedLine: string,
|
|
||||||
feedbackType: FeedbackType
|
|
||||||
): boolean => {
|
|
||||||
const prefix = feedbackType === "..." ? "... " : `${feedbackType} `;
|
|
||||||
return trimmedLine.startsWith(prefix) || trimmedLine === feedbackType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractFeedbackContent = (
|
const extractFeedbackContent = (
|
||||||
trimmedLine: string,
|
trimmedLine: string,
|
||||||
feedbackType: FeedbackType
|
feedbackType: FeedbackType
|
||||||
@@ -39,69 +31,65 @@ const saveFeedback = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type feedbackTypeOptions = "correct" | "incorrect" | "neutral" | "none";
|
||||||
|
|
||||||
export const quizFeedbackMarkdownUtils = {
|
export const quizFeedbackMarkdownUtils = {
|
||||||
extractFeedback(
|
extractFeedback(lines: string[]): {
|
||||||
linesWithoutPoints: string[],
|
|
||||||
isAnswerLine: (trimmedLine: string) => boolean
|
|
||||||
): {
|
|
||||||
correctComments?: string;
|
correctComments?: string;
|
||||||
incorrectComments?: string;
|
incorrectComments?: string;
|
||||||
neutralComments?: string;
|
neutralComments?: string;
|
||||||
linesWithoutFeedback: string[];
|
otherLines: string[];
|
||||||
} {
|
} {
|
||||||
const comments: {
|
const comments = {
|
||||||
correct?: string;
|
correct: [] as string[],
|
||||||
incorrect?: string;
|
incorrect: [] as string[],
|
||||||
neutral?: string;
|
neutral: [] as string[],
|
||||||
} = {};
|
};
|
||||||
const linesWithoutFeedback: string[] = [];
|
|
||||||
|
|
||||||
let currentFeedbackType: FeedbackType | null = null;
|
const otherLines: string[] = [];
|
||||||
let currentFeedbackLines: string[] = [];
|
|
||||||
|
|
||||||
for (const line of linesWithoutPoints) {
|
const feedbackIndicators = {
|
||||||
const trimmed = line.trim();
|
correct: "+",
|
||||||
|
incorrect: "-",
|
||||||
|
neutral: "...",
|
||||||
|
};
|
||||||
|
|
||||||
// Check if this is a new feedback line
|
let currentFeedbackType: feedbackTypeOptions = "none";
|
||||||
let newFeedbackType: FeedbackType | null = null;
|
|
||||||
if (isFeedbackStart(trimmed, "+")) {
|
|
||||||
newFeedbackType = "+";
|
|
||||||
} else if (isFeedbackStart(trimmed, "-")) {
|
|
||||||
newFeedbackType = "-";
|
|
||||||
} else if (isFeedbackStart(trimmed, "...")) {
|
|
||||||
newFeedbackType = "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newFeedbackType) {
|
for (const line of lines.map((l) => l)) {
|
||||||
// Save previous feedback if any
|
const lineFeedbackType: feedbackTypeOptions = line.startsWith("+")
|
||||||
saveFeedback(currentFeedbackType, currentFeedbackLines, comments);
|
? "correct"
|
||||||
|
: line.startsWith("-")
|
||||||
|
? "incorrect"
|
||||||
|
: line.startsWith("...")
|
||||||
|
? "neutral"
|
||||||
|
: "none";
|
||||||
|
|
||||||
// Start new feedback
|
if (lineFeedbackType === "none" && currentFeedbackType !== "none") {
|
||||||
currentFeedbackType = newFeedbackType;
|
const lineWithoutIndicator = line
|
||||||
const content = extractFeedbackContent(trimmed, newFeedbackType);
|
.replace(feedbackIndicators[currentFeedbackType], "")
|
||||||
currentFeedbackLines = content ? [content] : [];
|
.trim();
|
||||||
} else if (currentFeedbackType && !isAnswerLine(trimmed)) {
|
comments[currentFeedbackType].push(lineWithoutIndicator);
|
||||||
// This is a continuation of the current feedback
|
} else if (lineFeedbackType !== "none") {
|
||||||
currentFeedbackLines.push(line);
|
const lineWithoutIndicator = line
|
||||||
|
.replace(feedbackIndicators[lineFeedbackType], "")
|
||||||
|
.trim();
|
||||||
|
currentFeedbackType = lineFeedbackType;
|
||||||
|
comments[lineFeedbackType].push(lineWithoutIndicator);
|
||||||
} else {
|
} else {
|
||||||
// Save any pending feedback
|
otherLines.push(line);
|
||||||
saveFeedback(currentFeedbackType, currentFeedbackLines, comments);
|
|
||||||
currentFeedbackType = null;
|
|
||||||
currentFeedbackLines = [];
|
|
||||||
|
|
||||||
// This is a regular line
|
|
||||||
linesWithoutFeedback.push(line);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save any remaining feedback
|
const correctComments = comments.correct.filter((l) => l).join("\n");
|
||||||
saveFeedback(currentFeedbackType, currentFeedbackLines, comments);
|
const incorrectComments = comments.incorrect.filter((l) => l).join("\n");
|
||||||
|
const neutralComments = comments.neutral.filter((l) => l).join("\n");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
correctComments: comments.correct,
|
correctComments: correctComments || undefined,
|
||||||
incorrectComments: comments.incorrect,
|
incorrectComments: incorrectComments || undefined,
|
||||||
neutralComments: comments.neutral,
|
neutralComments: neutralComments || undefined,
|
||||||
linesWithoutFeedback,
|
otherLines,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ const parseMatchingAnswer = (input: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const quizQuestionAnswerMarkdownUtils = {
|
export const quizQuestionAnswerMarkdownUtils = {
|
||||||
// getHtmlText(): string {
|
|
||||||
// return MarkdownService.render(this.text);
|
|
||||||
// }
|
|
||||||
|
|
||||||
parseMarkdown(input: string, questionType: string): LocalQuizQuestionAnswer {
|
parseMarkdown(input: string, questionType: string): LocalQuizQuestionAnswer {
|
||||||
const isCorrect = input.startsWith("*") || input[1] === "*";
|
const isCorrect = input.startsWith("*") || input[1] === "*";
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { QuestionType, LocalQuizQuestion } from "../localQuizQuestion";
|
||||||
|
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
||||||
|
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
|
||||||
|
|
||||||
|
const _validFirstAnswerDelimiters = [
|
||||||
|
"*a)",
|
||||||
|
"a)",
|
||||||
|
"*)",
|
||||||
|
")",
|
||||||
|
"[ ]",
|
||||||
|
"[]",
|
||||||
|
"[*]",
|
||||||
|
"^",
|
||||||
|
];
|
||||||
|
const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"];
|
||||||
|
const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"];
|
||||||
|
|
||||||
|
export const isAnswerLine = (trimmedLine: string): boolean => {
|
||||||
|
return _validFirstAnswerDelimiters.some((prefix) =>
|
||||||
|
trimmedLine.startsWith(prefix)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAnswerStringsWithMultilineSupport = (
|
||||||
|
linesWithoutPoints: string[],
|
||||||
|
questionIndex: number
|
||||||
|
) => {
|
||||||
|
const indexOfAnswerStart = linesWithoutPoints.findIndex((l) =>
|
||||||
|
_validFirstAnswerDelimiters.some((prefix) =>
|
||||||
|
l.trimStart().startsWith(prefix)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (indexOfAnswerStart === -1) {
|
||||||
|
const debugLine = linesWithoutPoints.find((l) => l.trim().length > 0);
|
||||||
|
throw Error(
|
||||||
|
`question ${
|
||||||
|
questionIndex + 1
|
||||||
|
}: no answers when detecting question type on ${debugLine}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerLinesRaw = linesWithoutPoints.slice(indexOfAnswerStart);
|
||||||
|
|
||||||
|
const answerStartPattern = /^(\*?[a-z]?\))|(?<!\S)\[\s*\]|\[\*\]|\^/;
|
||||||
|
const answerLines = answerLinesRaw.reduce((acc: string[], line: string) => {
|
||||||
|
const isNewAnswer = answerStartPattern.test(line);
|
||||||
|
if (isNewAnswer) {
|
||||||
|
acc.push(line);
|
||||||
|
} else if (acc.length !== 0) {
|
||||||
|
acc[acc.length - 1] += "\n" + line;
|
||||||
|
} else {
|
||||||
|
acc.push(line);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
return answerLines;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestionType = (
|
||||||
|
linesWithoutPoints: string[],
|
||||||
|
questionIndex: number
|
||||||
|
): QuestionType => {
|
||||||
|
if (linesWithoutPoints.length === 0) return QuestionType.NONE;
|
||||||
|
if (
|
||||||
|
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() === "essay"
|
||||||
|
)
|
||||||
|
return QuestionType.ESSAY;
|
||||||
|
if (
|
||||||
|
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||||
|
"short answer"
|
||||||
|
)
|
||||||
|
return QuestionType.SHORT_ANSWER;
|
||||||
|
if (
|
||||||
|
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
||||||
|
"short_answer"
|
||||||
|
)
|
||||||
|
return QuestionType.SHORT_ANSWER;
|
||||||
|
if (
|
||||||
|
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() ===
|
||||||
|
"short_answer="
|
||||||
|
)
|
||||||
|
return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
|
||||||
|
|
||||||
|
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||||
|
linesWithoutPoints,
|
||||||
|
questionIndex
|
||||||
|
);
|
||||||
|
const firstAnswerLine = answerLines[0];
|
||||||
|
const isMultipleChoice = _multipleChoicePrefix.some((prefix) =>
|
||||||
|
firstAnswerLine.startsWith(prefix)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
|
||||||
|
|
||||||
|
const isMultipleAnswer = _multipleAnswerPrefix.some((prefix) =>
|
||||||
|
firstAnswerLine.startsWith(prefix)
|
||||||
|
);
|
||||||
|
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
|
||||||
|
|
||||||
|
const isMatching = firstAnswerLine.startsWith("^");
|
||||||
|
if (isMatching) return QuestionType.MATCHING;
|
||||||
|
|
||||||
|
return QuestionType.NONE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAnswers = (
|
||||||
|
linesWithoutPoints: string[],
|
||||||
|
questionIndex: number,
|
||||||
|
questionType: string
|
||||||
|
): LocalQuizQuestionAnswer[] => {
|
||||||
|
if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
||||||
|
linesWithoutPoints = linesWithoutPoints.slice(
|
||||||
|
0,
|
||||||
|
linesWithoutPoints.length - 1
|
||||||
|
);
|
||||||
|
const answerLines = getAnswerStringsWithMultilineSupport(
|
||||||
|
linesWithoutPoints,
|
||||||
|
questionIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const answers = answerLines.map((a) =>
|
||||||
|
quizQuestionAnswerMarkdownUtils.parseMarkdown(a, questionType)
|
||||||
|
);
|
||||||
|
return answers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAnswerMarkdown = (
|
||||||
|
question: LocalQuizQuestion,
|
||||||
|
answer: LocalQuizQuestionAnswer,
|
||||||
|
index: number
|
||||||
|
): string => {
|
||||||
|
const multilineMarkdownCompatibleText = answer.text.startsWith("```")
|
||||||
|
? "\n" + answer.text
|
||||||
|
: answer.text;
|
||||||
|
|
||||||
|
if (question.questionType === "multiple_answers") {
|
||||||
|
const correctIndicator = answer.correct ? "*" : " ";
|
||||||
|
const questionTypeIndicator = `[${correctIndicator}] `;
|
||||||
|
|
||||||
|
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||||
|
} else if (question.questionType === "matching") {
|
||||||
|
return `^ ${answer.text} - ${answer.matchedText}`;
|
||||||
|
} else {
|
||||||
|
const questionLetter = String.fromCharCode(97 + index);
|
||||||
|
const correctIndicator = answer.correct ? "*" : "";
|
||||||
|
const questionTypeIndicator = `${correctIndicator}${questionLetter}) `;
|
||||||
|
|
||||||
|
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,153 +1,11 @@
|
|||||||
import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
|
import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
|
||||||
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
|
|
||||||
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
|
|
||||||
import { quizFeedbackMarkdownUtils } from "./quizFeedbackMarkdownUtils";
|
import { quizFeedbackMarkdownUtils } from "./quizFeedbackMarkdownUtils";
|
||||||
|
import {
|
||||||
const _validFirstAnswerDelimiters = [
|
getAnswerMarkdown,
|
||||||
"*a)",
|
getAnswers,
|
||||||
"a)",
|
getQuestionType,
|
||||||
"*)",
|
isAnswerLine,
|
||||||
")",
|
} from "./quizQuestionAnswerParsingUtils";
|
||||||
"[ ]",
|
|
||||||
"[]",
|
|
||||||
"[*]",
|
|
||||||
"^",
|
|
||||||
];
|
|
||||||
const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"];
|
|
||||||
const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"];
|
|
||||||
|
|
||||||
const isAnswerLine = (trimmedLine: string): boolean => {
|
|
||||||
return _validFirstAnswerDelimiters.some((prefix) =>
|
|
||||||
trimmedLine.startsWith(prefix)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnswerStringsWithMultilineSupport = (
|
|
||||||
linesWithoutPoints: string[],
|
|
||||||
questionIndex: number
|
|
||||||
) => {
|
|
||||||
const indexOfAnswerStart = linesWithoutPoints.findIndex((l) =>
|
|
||||||
_validFirstAnswerDelimiters.some((prefix) =>
|
|
||||||
l.trimStart().startsWith(prefix)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (indexOfAnswerStart === -1) {
|
|
||||||
const debugLine = linesWithoutPoints.find((l) => l.trim().length > 0);
|
|
||||||
throw Error(
|
|
||||||
`question ${
|
|
||||||
questionIndex + 1
|
|
||||||
}: no answers when detecting question type on ${debugLine}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const answerLinesRaw = linesWithoutPoints.slice(indexOfAnswerStart);
|
|
||||||
|
|
||||||
const answerStartPattern = /^(\*?[a-z]?\))|(?<!\S)\[\s*\]|\[\*\]|\^/;
|
|
||||||
const answerLines = answerLinesRaw.reduce((acc: string[], line: string) => {
|
|
||||||
const isNewAnswer = answerStartPattern.test(line);
|
|
||||||
if (isNewAnswer) {
|
|
||||||
acc.push(line);
|
|
||||||
} else if (acc.length !== 0) {
|
|
||||||
acc[acc.length - 1] += "\n" + line;
|
|
||||||
} else {
|
|
||||||
acc.push(line);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
return answerLines;
|
|
||||||
};
|
|
||||||
const getQuestionType = (
|
|
||||||
linesWithoutPoints: string[],
|
|
||||||
questionIndex: number
|
|
||||||
): QuestionType => {
|
|
||||||
if (linesWithoutPoints.length === 0) return QuestionType.NONE;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() === "essay"
|
|
||||||
)
|
|
||||||
return QuestionType.ESSAY;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
|
||||||
"short answer"
|
|
||||||
)
|
|
||||||
return QuestionType.SHORT_ANSWER;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase() ===
|
|
||||||
"short_answer"
|
|
||||||
)
|
|
||||||
return QuestionType.SHORT_ANSWER;
|
|
||||||
if (
|
|
||||||
linesWithoutPoints[linesWithoutPoints.length - 1].toLowerCase().trim() ===
|
|
||||||
"short_answer="
|
|
||||||
)
|
|
||||||
return QuestionType.SHORT_ANSWER_WITH_ANSWERS;
|
|
||||||
|
|
||||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
|
||||||
linesWithoutPoints,
|
|
||||||
questionIndex
|
|
||||||
);
|
|
||||||
const firstAnswerLine = answerLines[0];
|
|
||||||
const isMultipleChoice = _multipleChoicePrefix.some((prefix) =>
|
|
||||||
firstAnswerLine.startsWith(prefix)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
|
|
||||||
|
|
||||||
const isMultipleAnswer = _multipleAnswerPrefix.some((prefix) =>
|
|
||||||
firstAnswerLine.startsWith(prefix)
|
|
||||||
);
|
|
||||||
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
|
|
||||||
|
|
||||||
const isMatching = firstAnswerLine.startsWith("^");
|
|
||||||
if (isMatching) return QuestionType.MATCHING;
|
|
||||||
|
|
||||||
return QuestionType.NONE;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnswers = (
|
|
||||||
linesWithoutPoints: string[],
|
|
||||||
questionIndex: number,
|
|
||||||
questionType: string
|
|
||||||
): LocalQuizQuestionAnswer[] => {
|
|
||||||
if (questionType == QuestionType.SHORT_ANSWER_WITH_ANSWERS)
|
|
||||||
linesWithoutPoints = linesWithoutPoints.slice(
|
|
||||||
0,
|
|
||||||
linesWithoutPoints.length - 1
|
|
||||||
);
|
|
||||||
const answerLines = getAnswerStringsWithMultilineSupport(
|
|
||||||
linesWithoutPoints,
|
|
||||||
questionIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
const answers = answerLines.map((a) =>
|
|
||||||
quizQuestionAnswerMarkdownUtils.parseMarkdown(a, questionType)
|
|
||||||
);
|
|
||||||
return answers;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAnswerMarkdown = (
|
|
||||||
question: LocalQuizQuestion,
|
|
||||||
answer: LocalQuizQuestionAnswer,
|
|
||||||
index: number
|
|
||||||
): string => {
|
|
||||||
const multilineMarkdownCompatibleText = answer.text.startsWith("```")
|
|
||||||
? "\n" + answer.text
|
|
||||||
: answer.text;
|
|
||||||
|
|
||||||
if (question.questionType === "multiple_answers") {
|
|
||||||
const correctIndicator = answer.correct ? "*" : " ";
|
|
||||||
const questionTypeIndicator = `[${correctIndicator}] `;
|
|
||||||
|
|
||||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
|
||||||
} else if (question.questionType === "matching") {
|
|
||||||
return `^ ${answer.text} - ${answer.matchedText}`;
|
|
||||||
} else {
|
|
||||||
const questionLetter = String.fromCharCode(97 + index);
|
|
||||||
const correctIndicator = answer.correct ? "*" : "";
|
|
||||||
const questionTypeIndicator = `${correctIndicator}${questionLetter}) `;
|
|
||||||
|
|
||||||
return `${questionTypeIndicator}${multilineMarkdownCompatibleText}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const quizQuestionMarkdownUtils = {
|
export const quizQuestionMarkdownUtils = {
|
||||||
toMarkdown(question: LocalQuizQuestion): string {
|
toMarkdown(question: LocalQuizQuestion): string {
|
||||||
@@ -180,7 +38,9 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseMarkdown(input: string, questionIndex: number): LocalQuizQuestion {
|
parseMarkdown(input: string, questionIndex: number): LocalQuizQuestion {
|
||||||
const lines = input.trim().split("\n");
|
const lines = input
|
||||||
|
.trim()
|
||||||
|
.split("\n");
|
||||||
const firstLineIsPoints = lines[0].toLowerCase().includes("points: ");
|
const firstLineIsPoints = lines[0].toLowerCase().includes("points: ");
|
||||||
|
|
||||||
const textHasPoints =
|
const textHasPoints =
|
||||||
@@ -196,25 +56,12 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
|
|
||||||
const linesWithoutPoints = firstLineIsPoints ? lines.slice(1) : lines;
|
const linesWithoutPoints = firstLineIsPoints ? lines.slice(1) : lines;
|
||||||
|
|
||||||
// Extract feedback comments first
|
const { linesWithoutAnswers } = linesWithoutPoints.reduce(
|
||||||
const {
|
|
||||||
correctComments,
|
|
||||||
incorrectComments,
|
|
||||||
neutralComments,
|
|
||||||
linesWithoutFeedback,
|
|
||||||
} = quizFeedbackMarkdownUtils.extractFeedback(
|
|
||||||
linesWithoutPoints,
|
|
||||||
isAnswerLine
|
|
||||||
);
|
|
||||||
|
|
||||||
const { linesWithoutAnswers } = linesWithoutFeedback.reduce(
|
|
||||||
({ linesWithoutAnswers, taking }, currentLine) => {
|
({ linesWithoutAnswers, taking }, currentLine) => {
|
||||||
if (!taking)
|
if (!taking)
|
||||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||||
|
|
||||||
const lineIsAnswer = _validFirstAnswerDelimiters.some((prefix) =>
|
const lineIsAnswer = isAnswerLine(currentLine);
|
||||||
currentLine.trimStart().startsWith(prefix)
|
|
||||||
);
|
|
||||||
if (lineIsAnswer)
|
if (lineIsAnswer)
|
||||||
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
return { linesWithoutAnswers: linesWithoutAnswers, taking: false };
|
||||||
|
|
||||||
@@ -225,7 +72,8 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
},
|
},
|
||||||
{ linesWithoutAnswers: [] as string[], taking: true }
|
{ linesWithoutAnswers: [] as string[], taking: true }
|
||||||
);
|
);
|
||||||
const questionType = getQuestionType(linesWithoutFeedback, questionIndex);
|
|
||||||
|
const questionType = getQuestionType(lines, questionIndex);
|
||||||
|
|
||||||
const questionTypesWithoutAnswers = [
|
const questionTypesWithoutAnswers = [
|
||||||
"essay",
|
"essay",
|
||||||
@@ -233,17 +81,20 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
"short_answer",
|
"short_answer",
|
||||||
];
|
];
|
||||||
|
|
||||||
const descriptionLines = questionTypesWithoutAnswers.includes(
|
const descriptionLines = questionTypesWithoutAnswers.includes(questionType)
|
||||||
questionType.toLowerCase()
|
|
||||||
)
|
|
||||||
? linesWithoutAnswers
|
? linesWithoutAnswers
|
||||||
.slice(0, linesWithoutFeedback.length)
|
.slice(0, linesWithoutPoints.length)
|
||||||
.filter(
|
.filter(
|
||||||
(line) => !questionTypesWithoutAnswers.includes(line.toLowerCase())
|
(line) => !questionTypesWithoutAnswers.includes(line.toLowerCase())
|
||||||
)
|
)
|
||||||
: linesWithoutAnswers;
|
: linesWithoutAnswers;
|
||||||
|
|
||||||
const description = descriptionLines.join("\n");
|
const {
|
||||||
|
correctComments,
|
||||||
|
incorrectComments,
|
||||||
|
neutralComments,
|
||||||
|
otherLines: descriptionWithoutFeedback,
|
||||||
|
} = quizFeedbackMarkdownUtils.extractFeedback(descriptionLines);
|
||||||
|
|
||||||
const typesWithAnswers = [
|
const typesWithAnswers = [
|
||||||
"multiple_choice",
|
"multiple_choice",
|
||||||
@@ -252,7 +103,7 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
"short_answer=",
|
"short_answer=",
|
||||||
];
|
];
|
||||||
const answers = typesWithAnswers.includes(questionType)
|
const answers = typesWithAnswers.includes(questionType)
|
||||||
? getAnswers(linesWithoutFeedback, questionIndex, questionType)
|
? getAnswers(lines, questionIndex, questionType)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const answersWithoutDistractors =
|
const answersWithoutDistractors =
|
||||||
@@ -266,7 +117,7 @@ export const quizQuestionMarkdownUtils = {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const question: LocalQuizQuestion = {
|
const question: LocalQuizQuestion = {
|
||||||
text: description,
|
text: descriptionWithoutFeedback.join("\n"),
|
||||||
questionType,
|
questionType,
|
||||||
points,
|
points,
|
||||||
answers: answersWithoutDistractors,
|
answers: answersWithoutDistractors,
|
||||||
|
|||||||
Reference in New Issue
Block a user