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"
},
"dependencies": {
"jsdom": "^25.0.0",
"next": "^14.2.7",
"react": "^18",
"jsdom": "^25.0.0",
"react-dom": "^18"
},
"devDependencies": {
"yaml": "^2.5.0",
"axios": "^1.7.5",
"marked": "^14.1.2",
"@monaco-editor/react": "^4.6.0",
"@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",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.0",
@@ -31,10 +25,16 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitejs/plugin-react": "^4.3.1",
"axios": "^1.7.5",
"eslint-config-next": "^14.2.7",
"isomorphic-dompurify": "^2.15.0",
"marked": "^14.1.2",
"postcss": "^8",
"react-error-boundary": "^4.0.13",
"react-hot-toast": "^2.4.1",
"tailwindcss": "^3.4.1",
"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 { useCreateModuleMutation } from "@/hooks/localCourse/localCourseModuleHooks";
import React, { useState } from "react";
export default function CreateModule() {
const createModule = useCreateModuleMutation();
const [showForm, setShowForm] = useState(false);
const [moduleName, setModuleName] = useState("");
return (
<>
<button onClick={() => setShowForm((v) => !v)}>
{showForm ? "Hide Form" : "Create Module"}
<Expandable
ExpandableElement={({ setIsExpanded, isExpanded }) => (
<button onClick={() => setIsExpanded((v) => !v)}>
{isExpanded ? "Hide Form" : "Create Module"}
</button>
<div className={"collapsible " + (showForm ? "expand" : "")}>
)}
>
<form
onSubmit={async (e) => {
e.preventDefault();
@@ -30,7 +33,7 @@ export default function CreateModule() {
/>
<button className="mt-auto">Add</button>
</form>
</div>
</Expandable>
</>
);
}

View File

@@ -7,10 +7,12 @@ export default function ModuleList() {
const { data: moduleNames } = useModuleNamesQuery();
return (
<div>
<CreateModule />
{moduleNames.map((m) => (
<ExpandableModule key={m} moduleName={m} />
))}
<div className="flex flex-col justify-center">
<CreateModule />
</div>
<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 SubmissionDefaults from "./SubmissionDefaults";
import DefaultFileUploadTypes from "./DefaultFileUploadTypes";
import HolidayConfig from "./HolidayConfig";
export default function page() {
return (
@@ -19,6 +20,7 @@ export default function page() {
<DefaultFileUploadTypes />
<DefaultDueTime />
<AssignmentGroupManagement />
<HolidayConfig />
<br />
<br />
<br />

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion";
import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer";
import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils";
const _validFirstAnswerDelimiters = ["*a)", "a)", "*)", ")", "[ ]", "[*]", "^"];
const _validFirstAnswerDelimiters = ["*a)", "a)", "*)", ")", "[ ]", "[]", "[*]", "^"];
const getAnswerStringsWithMultilineSupport = (
linesWithoutPoints: string[],
@@ -70,7 +70,7 @@ const getQuestionType = (
if (isMultipleChoice) return QuestionType.MULTIPLE_CHOICE;
const isMultipleAnswer = ["[ ]", "[*]"].some((prefix) =>
const isMultipleAnswer = ["[ ]", "[*]", "[]"].some((prefix) =>
firstAnswerLine.startsWith(prefix)
);
if (isMultipleAnswer) return QuestionType.MULTIPLE_ANSWERS;
@@ -96,6 +96,7 @@ const getAnswers = (
);
return answers;
};
const getAnswerMarkdown = (
question: LocalQuizQuestion,
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");
});
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", () => {
const rawMarkdownQuestion = `
Which events are triggered when the user clicks on an input field?

View File

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

View File

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