mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
adding support for empty multiple answer
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user