adding support for empty multiple answer

This commit is contained in:
2024-10-09 12:06:40 -06:00
parent 2eaca984df
commit 527029fa52
11 changed files with 197 additions and 26 deletions

View File

@@ -10,20 +10,14 @@
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"jsdom": "^25.0.0",
"next": "^14.2.7", "next": "^14.2.7",
"react": "^18", "react": "^18",
"jsdom": "^25.0.0",
"react-dom": "^18" "react-dom": "^18"
}, },
"devDependencies": { "devDependencies": {
"yaml": "^2.5.0",
"axios": "^1.7.5",
"marked": "^14.1.2",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@tanstack/react-query": "^5.54.1", "@tanstack/react-query": "^5.54.1",
"isomorphic-dompurify": "^2.15.0",
"react-error-boundary": "^4.0.13",
"react-hot-toast": "^2.4.1",
"@tanstack/react-query-devtools": "^5.54.1", "@tanstack/react-query-devtools": "^5.54.1",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
@@ -31,10 +25,16 @@
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"axios": "^1.7.5",
"eslint-config-next": "^14.2.7", "eslint-config-next": "^14.2.7",
"isomorphic-dompurify": "^2.15.0",
"marked": "^14.1.2",
"postcss": "^8", "postcss": "^8",
"react-error-boundary": "^4.0.13",
"react-hot-toast": "^2.4.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5", "typescript": "^5",
"vitest": "^2.0.5" "vitest": "^2.0.5",
"yaml": "^2.5.0"
} }
} }

View File

@@ -1,17 +1,20 @@
import { Expandable } from "@/components/Expandable";
import TextInput from "@/components/form/TextInput"; import TextInput from "@/components/form/TextInput";
import { useCreateModuleMutation } from "@/hooks/localCourse/localCourseModuleHooks"; import { useCreateModuleMutation } from "@/hooks/localCourse/localCourseModuleHooks";
import React, { useState } from "react"; import React, { useState } from "react";
export default function CreateModule() { export default function CreateModule() {
const createModule = useCreateModuleMutation(); const createModule = useCreateModuleMutation();
const [showForm, setShowForm] = useState(false);
const [moduleName, setModuleName] = useState(""); const [moduleName, setModuleName] = useState("");
return ( return (
<> <>
<button onClick={() => setShowForm((v) => !v)}> <Expandable
{showForm ? "Hide Form" : "Create Module"} ExpandableElement={({ setIsExpanded, isExpanded }) => (
</button> <button onClick={() => setIsExpanded((v) => !v)}>
<div className={"collapsible " + (showForm ? "expand" : "")}> {isExpanded ? "Hide Form" : "Create Module"}
</button>
)}
>
<form <form
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
@@ -30,7 +33,7 @@ export default function CreateModule() {
/> />
<button className="mt-auto">Add</button> <button className="mt-auto">Add</button>
</form> </form>
</div> </Expandable>
</> </>
); );
} }

View File

@@ -7,10 +7,12 @@ export default function ModuleList() {
const { data: moduleNames } = useModuleNamesQuery(); const { data: moduleNames } = useModuleNamesQuery();
return ( return (
<div> <div>
<CreateModule />
{moduleNames.map((m) => ( {moduleNames.map((m) => (
<ExpandableModule key={m} moduleName={m} /> <ExpandableModule key={m} moduleName={m} />
))} ))}
<div className="flex flex-col justify-center">
<CreateModule />
</div>
<br /> <br />
<br /> <br />
<br /> <br />

View File

@@ -0,0 +1,85 @@
"use client";
import TextInput from "@/components/form/TextInput";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import {
dateToMarkdownString,
getDateFromStringOrThrow,
} from "@/models/local/timeUtils";
import { useState } from "react";
const exampleString = `springBreak:
- 10/12/2024
- 10/13/2024
- 10/14/2024
laborDay:
- 9/1/2024`;
export default function HolidayConfig() {
const { data: settings } = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
const [rawText, setRawText] = useState("");
const parsedText = parseHolidays(rawText);
return (
<div className="flex flex-row gap-3 border w-fit p-3 m-3 rounded-md">
<TextInput
value={rawText}
setValue={setRawText}
label={"Holiday Days"}
isTextArea={true}
/>
<div>
Format your holidays like so:
<pre>
<code>{exampleString}</code>
</pre>
</div>
<div>
{Object.keys(parsedText).map((k) => (
<div key={k}>
<div>{k}</div>
<div>
{parsedText[k].map((day) => {
const parsedDate = getDateFromStringOrThrow(
day,
"holiday preview display"
);
return <div key={day}>{dateToMarkdownString(parsedDate)}</div>;
})}
</div>
</div>
))}
</div>
</div>
);
}
const parseHolidays = (
inputText: string
): { [holidayName: string]: string[] } => {
const holidays: { [holidayName: string]: string[] } = {};
const lines = inputText.split("\n").filter(line => line.trim() !== "");
let currentHoliday: string | null = null;
lines.forEach(line => {
if (line.includes(":")) {
// It's a holiday name
const holidayName = line.split(":")[0].trim();
currentHoliday = holidayName;
holidays[currentHoliday] = [];
} else if (currentHoliday && line.startsWith("-")) {
// It's a date under the current holiday
const date = line.replace("-", "").trim();
holidays[currentHoliday].push(date);
}
});
return holidays;
};

View File

@@ -7,6 +7,7 @@ import DaysOfWeekSettings from "./DaysOfWeekSettings";
import AssignmentGroupManagement from "./AssignmentGroupManagement"; import AssignmentGroupManagement from "./AssignmentGroupManagement";
import SubmissionDefaults from "./SubmissionDefaults"; import SubmissionDefaults from "./SubmissionDefaults";
import DefaultFileUploadTypes from "./DefaultFileUploadTypes"; import DefaultFileUploadTypes from "./DefaultFileUploadTypes";
import HolidayConfig from "./HolidayConfig";
export default function page() { export default function page() {
return ( return (
@@ -19,6 +20,7 @@ export default function page() {
<DefaultFileUploadTypes /> <DefaultFileUploadTypes />
<DefaultDueTime /> <DefaultDueTime />
<AssignmentGroupManagement /> <AssignmentGroupManagement />
<HolidayConfig />
<br /> <br />
<br /> <br />
<br /> <br />

View File

@@ -5,21 +5,32 @@ export default function TextInput({
setValue, setValue,
label, label,
className, className,
isTextArea = false,
}: { }: {
value: string; value: string;
setValue: (newValue: string) => void; setValue: (newValue: string) => void;
label: string; label: string;
className?: string; className?: string;
isTextArea?: boolean;
}) { }) {
return ( return (
<label className={"block " + className}> <label className={"flex flex-col " + className}>
{label} {label}
<br /> <br />
<input {!isTextArea && (
className="bg-slate-800 border border-slate-500 rounded-md w-full px-1" <input
value={value} className="bg-slate-800 border border-slate-500 rounded-md w-full px-1"
onChange={(e) => setValue(e.target.value)} value={value}
/> onChange={(e) => setValue(e.target.value)}
/>
)}
{isTextArea && (
<textarea
className="bg-slate-800 border border-slate-500 rounded-md w-full px-1 flex-grow"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)}
</label> </label>
); );
} }

View File

@@ -24,6 +24,9 @@ export interface LocalCourseSettings {
defaultLockHoursOffset?: number; defaultLockHoursOffset?: number;
defaultAssignmentSubmissionTypes: AssignmentSubmissionType[]; defaultAssignmentSubmissionTypes: AssignmentSubmissionType[];
defaultFileUploadTypes: string[]; defaultFileUploadTypes: string[];
holidays: {
[key: string]: string[]; // e.g. "spring break": ["datestring", "datestring", "datestring", "datestring"]
};
} }
export enum DayOfWeek { export enum DayOfWeek {

View File

@@ -3,7 +3,7 @@ import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer"; import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils"; import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
const _validFirstAnswerDelimiters = ["*a)", "a)", "*)", ")", "[ ]", "[*]", "^"]; const _validFirstAnswerDelimiters = ["*a)", "a)", "*)", ")", "[ ]", "[]", "[*]", "^"];
const getAnswerStringsWithMultilineSupport = ( const getAnswerStringsWithMultilineSupport = (
linesWithoutPoints: string[], linesWithoutPoints: string[],
@@ -70,7 +70,7 @@ const getQuestionType = (
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE; if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
const isMultipleAnswer = ["[ ]", "[*]"].some((prefix) => const isMultipleAnswer = ["[ ]", "[*]", "[]"].some((prefix) =>
firstAnswerLine.startsWith(prefix) firstAnswerLine.startsWith(prefix)
); );
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS; if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
@@ -96,6 +96,7 @@ const getAnswers = (
); );
return answers; return answers;
}; };
const getAnswerMarkdown = ( const getAnswerMarkdown = (
question: LocalQuizQuestion, question: LocalQuizQuestion,
answer: LocalQuizQuestionAnswer, answer: LocalQuizQuestionAnswer,

View File

@@ -78,6 +78,61 @@ Which events are triggered when the user clicks on an input field?
expect(firstQuestion.answers[3].text).toBe("submit"); expect(firstQuestion.answers[3].text).toBe("submit");
}); });
it("can parse question with multiple answers without a space in false answers", () => {
const rawMarkdownQuiz = `
Name: Test Quiz
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?
[*] click
[] submit
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.answers.length).toBe(2);
expect(firstQuestion.answers[0].correct).toBe(true);
expect(firstQuestion.answers[1].correct).toBe(false);
});
it("can parse question with multiple answers without a space in false answers other example", () => {
const rawMarkdownQuiz = `
Name: Test Quiz
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: 1
Which tool(s) will let you: create a database migration or reverse-engineer an existing database
[] swagger
[] a .http file
[*] dotnet ef command line interface
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.answers.length).toBe(3);
expect(firstQuestion.answers[0].correct).toBe(false);
expect(firstQuestion.answers[1].correct).toBe(false);
expect(firstQuestion.answers[2].correct).toBe(true);
});
it("can use braces in answer for multiple answer", () => { it("can use braces in answer for multiple answer", () => {
const rawMarkdownQuestion = ` const rawMarkdownQuestion = `
Which events are triggered when the user clicks on an input field? Which events are triggered when the user clicks on an input field?

View File

@@ -27,6 +27,13 @@ const getCourseSettings = async (
const settingsFromFile = const settingsFromFile =
localCourseYamlUtils.parseSettingYaml(settingsString); localCourseYamlUtils.parseSettingYaml(settingsString);
const settings: LocalCourseSettings = populateDefaultValues(settingsFromFile);
const folderName = path.basename(courseDirectory);
return { ...settings, name: folderName };
};
const populateDefaultValues = (settingsFromFile: LocalCourseSettings) => {
const defaultSubmissionType = [ const defaultSubmissionType = [
AssignmentSubmissionType.ONLINE_TEXT_ENTRY, AssignmentSubmissionType.ONLINE_TEXT_ENTRY,
AssignmentSubmissionType.ONLINE_UPLOAD, AssignmentSubmissionType.ONLINE_UPLOAD,
@@ -40,10 +47,9 @@ const getCourseSettings = async (
defaultSubmissionType, defaultSubmissionType,
defaultFileUploadTypes: defaultFileUploadTypes:
settingsFromFile.defaultFileUploadTypes || defaultFileUploadTypes, settingsFromFile.defaultFileUploadTypes || defaultFileUploadTypes,
holidays: !!settingsFromFile.holidays ? settingsFromFile.holidays : {},
}; };
return settings;
const folderName = path.basename(courseDirectory);
return { ...settings, name: folderName };
}; };
export const settingsFileStorageService = { export const settingsFileStorageService = {

View File

@@ -28,6 +28,9 @@ describe("FileStorageTests", () => {
endDate: "07/09/2024 23:59:00", endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, defaultDueTime: { hour: 1, minute: 59 },
canvasId: 0, canvasId: 0,
defaultAssignmentSubmissionTypes: [],
defaultFileUploadTypes: [],
holidays: {}
}; };
await fileStorageService.settings.updateCourseSettings(name, settings); await fileStorageService.settings.updateCourseSettings(name, settings);