mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
more code refactor to colocate feature code
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { zodLocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
||||
import {
|
||||
LocalAssignment,
|
||||
zodLocalAssignment,
|
||||
} from "@/features/local/assignments/models/localAssignment";
|
||||
import path from "path";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import { promises as fs } from "fs";
|
||||
import { courseItemFileStorageService } from "../course/courseItemFileStorageService";
|
||||
import { assignmentMarkdownSerializer } from "./models/utils/assignmentMarkdownSerializer";
|
||||
|
||||
export const assignmentRouter = router({
|
||||
getAssignment: publicProcedure
|
||||
@@ -14,13 +21,12 @@ export const assignmentRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { courseName, moduleName, assignmentName } }) => {
|
||||
const assignment = await fileStorageService.assignments.getAssignment(
|
||||
return await courseItemFileStorageService.getItem({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName
|
||||
);
|
||||
// console.log(assignment);
|
||||
return assignment;
|
||||
name: assignmentName,
|
||||
type: "Assignment",
|
||||
});
|
||||
}),
|
||||
getAllAssignments: publicProcedure
|
||||
.input(
|
||||
@@ -30,10 +36,11 @@ export const assignmentRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { courseName, moduleName } }) => {
|
||||
const assignments = await fileStorageService.assignments.getAssignments(
|
||||
const assignments = await courseItemFileStorageService.getItems({
|
||||
courseName,
|
||||
moduleName
|
||||
);
|
||||
moduleName,
|
||||
type: "Assignment",
|
||||
});
|
||||
return assignments;
|
||||
}),
|
||||
createAssignment: publicProcedure
|
||||
@@ -49,7 +56,7 @@ export const assignmentRouter = router({
|
||||
async ({
|
||||
input: { courseName, moduleName, assignmentName, assignment },
|
||||
}) => {
|
||||
await fileStorageService.assignments.updateOrCreateAssignment({
|
||||
await updateOrCreateAssignmentFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName,
|
||||
@@ -79,7 +86,7 @@ export const assignmentRouter = router({
|
||||
previousAssignmentName,
|
||||
},
|
||||
}) => {
|
||||
await fileStorageService.assignments.updateOrCreateAssignment({
|
||||
await updateOrCreateAssignmentFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName,
|
||||
@@ -90,7 +97,7 @@ export const assignmentRouter = router({
|
||||
assignmentName !== previousAssignmentName ||
|
||||
moduleName !== previousModuleName
|
||||
) {
|
||||
await fileStorageService.assignments.delete({
|
||||
await deleteAssignment({
|
||||
courseName,
|
||||
moduleName: previousModuleName,
|
||||
assignmentName: previousAssignmentName,
|
||||
@@ -107,10 +114,59 @@ export const assignmentRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { courseName, moduleName, assignmentName } }) => {
|
||||
await fileStorageService.assignments.delete({
|
||||
await deleteAssignment({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
export async function updateOrCreateAssignmentFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName,
|
||||
assignment,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
assignmentName: string;
|
||||
assignment: LocalAssignment;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = path.join(courseDirectory, moduleName, "assignments");
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"assignments",
|
||||
assignmentName + ".md"
|
||||
);
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
console.log(`Saving assignment ${filePath}`);
|
||||
|
||||
await fs.writeFile(filePath, assignmentMarkdown);
|
||||
}
|
||||
|
||||
async function deleteAssignment({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
assignmentName: string;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"assignments",
|
||||
assignmentName + ".md"
|
||||
);
|
||||
console.log("removing assignment", filePath);
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import {
|
||||
localAssignmentMarkdown,
|
||||
LocalAssignment,
|
||||
} from "@/features/local/assignments/models/localAssignment";
|
||||
import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { courseItemFileStorageService } from "@/features/local/course/courseItemFileStorageService";
|
||||
import { getCoursePathByName } from "@/features/local/globalSettings/globalSettingsFileStorageService";
|
||||
import { directoryOrFileExists } from "@/features/local/utils/fileSystemUtils";
|
||||
|
||||
const getAssignmentNames = async (courseName: string, moduleName: string) => {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(courseDirectory, moduleName, "assignments");
|
||||
if (!(await directoryOrFileExists(filePath))) {
|
||||
console.log(
|
||||
`Error loading course by name, assignments folder does not exist in ${filePath}`
|
||||
);
|
||||
// await fs.mkdir(filePath);
|
||||
return [];
|
||||
}
|
||||
|
||||
const assignmentFiles = await fs.readdir(filePath);
|
||||
return assignmentFiles.map((f) => f.replace(/\.md$/, ""));
|
||||
};
|
||||
const getAssignment = async (
|
||||
courseName: string,
|
||||
moduleName: string,
|
||||
assignmentName: string
|
||||
) => {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"assignments",
|
||||
assignmentName + ".md"
|
||||
);
|
||||
const rawFile = (await fs.readFile(filePath, "utf-8")).replace(/\r\n/g, "\n");
|
||||
return localAssignmentMarkdown.parseMarkdown(rawFile, assignmentName);
|
||||
};
|
||||
|
||||
export const assignmentsFileStorageService = {
|
||||
getAssignmentNames,
|
||||
getAssignment,
|
||||
async getAssignments(courseName: string, moduleName: string) {
|
||||
return await courseItemFileStorageService.getItems(
|
||||
courseName,
|
||||
moduleName,
|
||||
"Assignment"
|
||||
);
|
||||
},
|
||||
async updateOrCreateAssignment({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName,
|
||||
assignment,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
assignmentName: string;
|
||||
assignment: LocalAssignment;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = path.join(courseDirectory, moduleName, "assignments");
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"assignments",
|
||||
assignmentName + ".md"
|
||||
);
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
console.log(`Saving assignment ${filePath}`);
|
||||
|
||||
await fs.writeFile(filePath, assignmentMarkdown);
|
||||
},
|
||||
|
||||
async delete({
|
||||
courseName,
|
||||
moduleName,
|
||||
assignmentName,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
assignmentName: string;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"assignments",
|
||||
assignmentName + ".md"
|
||||
);
|
||||
console.log("removing assignment", filePath);
|
||||
await fs.unlink(filePath);
|
||||
},
|
||||
};
|
||||
@@ -2,10 +2,8 @@ import path from "path";
|
||||
import { directoryOrFileExists } from "../utils/fileSystemUtils";
|
||||
import fs from "fs/promises";
|
||||
import {
|
||||
LocalAssignment,
|
||||
localAssignmentMarkdown,
|
||||
} from "@/features/local/assignments/models/localAssignment";
|
||||
import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer";
|
||||
import {
|
||||
CourseItemReturnType,
|
||||
CourseItemType,
|
||||
@@ -14,19 +12,20 @@ import {
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
localPageMarkdownUtils,
|
||||
LocalCoursePage,
|
||||
} from "@/features/local/pages/localCoursePageModels";
|
||||
import {
|
||||
LocalQuiz,
|
||||
localQuizMarkdownUtils,
|
||||
} from "@/features/local/quizzes/models/localQuiz";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
|
||||
const getItemFileNames = async (
|
||||
courseName: string,
|
||||
moduleName: string,
|
||||
type: CourseItemType
|
||||
) => {
|
||||
const getItemFileNames = async ({
|
||||
courseName,
|
||||
moduleName,
|
||||
type,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
type: CourseItemType;
|
||||
}) => {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = typeToFolder[type];
|
||||
const filePath = path.join(courseDirectory, moduleName, folder);
|
||||
@@ -41,12 +40,17 @@ const getItemFileNames = async (
|
||||
return itemFiles.map((f) => f.replace(/\.md$/, ""));
|
||||
};
|
||||
|
||||
const getItem = async <T extends CourseItemType>(
|
||||
courseName: string,
|
||||
moduleName: string,
|
||||
name: string,
|
||||
type: T
|
||||
): Promise<CourseItemReturnType<T>> => {
|
||||
const getItem = async <T extends CourseItemType>({
|
||||
courseName,
|
||||
moduleName,
|
||||
name,
|
||||
type,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
name: string;
|
||||
type: T;
|
||||
}): Promise<CourseItemReturnType<T>> => {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = typeToFolder[type];
|
||||
const filePath = path.join(courseDirectory, moduleName, folder, name + ".md");
|
||||
@@ -73,17 +77,21 @@ const getItem = async <T extends CourseItemType>(
|
||||
|
||||
export const courseItemFileStorageService = {
|
||||
getItem,
|
||||
getItems: async <T extends CourseItemType>(
|
||||
courseName: string,
|
||||
moduleName: string,
|
||||
type: T
|
||||
): Promise<CourseItemReturnType<T>[]> => {
|
||||
const fileNames = await getItemFileNames(courseName, moduleName, type);
|
||||
getItems: async <T extends CourseItemType>({
|
||||
courseName,
|
||||
moduleName,
|
||||
type,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
type: T;
|
||||
}): Promise<CourseItemReturnType<T>[]> => {
|
||||
const fileNames = await getItemFileNames({ courseName, moduleName, type });
|
||||
const items = (
|
||||
await Promise.all(
|
||||
fileNames.map(async (name) => {
|
||||
try {
|
||||
const item = await getItem(courseName, moduleName, name, type);
|
||||
const item = await getItem({ courseName, moduleName, name, type });
|
||||
return item;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -93,42 +101,4 @@ export const courseItemFileStorageService = {
|
||||
).filter((a) => a !== null);
|
||||
return items;
|
||||
},
|
||||
async updateOrCreateAssignment({
|
||||
courseName,
|
||||
moduleName,
|
||||
name,
|
||||
item,
|
||||
type,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
name: string;
|
||||
item: LocalAssignment | LocalQuiz | LocalCoursePage;
|
||||
type: CourseItemType;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const typeFolder = typeToFolder[type];
|
||||
const folder = path.join(courseDirectory, moduleName, typeFolder);
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
typeFolder,
|
||||
name + ".md"
|
||||
);
|
||||
|
||||
const markdownDictionary: {
|
||||
[_key in CourseItemType]: () => string;
|
||||
} = {
|
||||
Assignment: () =>
|
||||
assignmentMarkdownSerializer.toMarkdown(item as LocalAssignment),
|
||||
Quiz: () => quizMarkdownUtils.toMarkdown(item as LocalQuiz),
|
||||
Page: () => localPageMarkdownUtils.toMarkdown(item as LocalCoursePage),
|
||||
};
|
||||
const itemMarkdown = markdownDictionary[type]();
|
||||
|
||||
console.log(`Saving ${type} ${filePath}`);
|
||||
await fs.writeFile(filePath, itemMarkdown);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,10 +13,18 @@ import {
|
||||
updateGlobalSettings,
|
||||
} from "@/features/local/globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
getLectures,
|
||||
updateLecture,
|
||||
} from "@/features/local/lectures/lectureFileStorageService";
|
||||
import { zodLocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
||||
LocalCourseSettings,
|
||||
zodLocalCourseSettings,
|
||||
} from "@/features/local/course/localCourseSettings";
|
||||
import { courseItemFileStorageService } from "./courseItemFileStorageService";
|
||||
import { updateOrCreateAssignmentFile } from "../assignments/assignmentRouter";
|
||||
import { updateQuizFile } from "../quizzes/quizRouter";
|
||||
import { updatePageFile } from "../pages/pageRouter";
|
||||
import { getLectures, updateLecture } from "../lectures/lectureRouter";
|
||||
import {
|
||||
createModuleFile,
|
||||
getModuleNamesFromFiles,
|
||||
} from "../modules/moduleRouter";
|
||||
|
||||
export const settingsRouter = router({
|
||||
allCoursesSettings: publicProcedure.query(async () => {
|
||||
@@ -71,90 +79,7 @@ export const settingsRouter = router({
|
||||
});
|
||||
|
||||
if (settingsFromCourseToImport) {
|
||||
const oldCourseName = settingsFromCourseToImport.name;
|
||||
const newCourseName = settings.name;
|
||||
const oldModules = await fileStorageService.modules.getModuleNames(
|
||||
oldCourseName
|
||||
);
|
||||
await Promise.all(
|
||||
oldModules.map(async (moduleName) => {
|
||||
await fileStorageService.modules.createModule(
|
||||
newCourseName,
|
||||
moduleName
|
||||
);
|
||||
|
||||
const [oldAssignments, oldQuizzes, oldPages, oldLecturesByWeek] =
|
||||
await Promise.all([
|
||||
fileStorageService.assignments.getAssignments(
|
||||
oldCourseName,
|
||||
moduleName
|
||||
),
|
||||
await fileStorageService.quizzes.getQuizzes(
|
||||
oldCourseName,
|
||||
moduleName
|
||||
),
|
||||
await fileStorageService.pages.getPages(
|
||||
oldCourseName,
|
||||
moduleName
|
||||
),
|
||||
await getLectures(oldCourseName),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
...oldAssignments.map(async (oldAssignment) => {
|
||||
const newAssignment = prepAssignmentForNewSemester(
|
||||
oldAssignment,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await fileStorageService.assignments.updateOrCreateAssignment(
|
||||
{
|
||||
courseName: newCourseName,
|
||||
moduleName,
|
||||
assignmentName: newAssignment.name,
|
||||
assignment: newAssignment,
|
||||
}
|
||||
);
|
||||
}),
|
||||
...oldQuizzes.map(async (oldQuiz) => {
|
||||
const newQuiz = prepQuizForNewSemester(
|
||||
oldQuiz,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await fileStorageService.quizzes.updateQuiz({
|
||||
courseName: newCourseName,
|
||||
moduleName,
|
||||
quizName: newQuiz.name,
|
||||
quiz: newQuiz,
|
||||
});
|
||||
}),
|
||||
...oldPages.map(async (oldPage) => {
|
||||
const newPage = prepPageForNewSemester(
|
||||
oldPage,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await fileStorageService.pages.updatePage({
|
||||
courseName: newCourseName,
|
||||
moduleName,
|
||||
pageName: newPage.name,
|
||||
page: newPage,
|
||||
});
|
||||
}),
|
||||
...oldLecturesByWeek.flatMap(async (oldLectureByWeek) =>
|
||||
oldLectureByWeek.lectures.map(async (oldLecture) => {
|
||||
const newLecture = prepLectureForNewSemester(
|
||||
oldLecture,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await updateLecture(newCourseName, settings, newLecture);
|
||||
})
|
||||
),
|
||||
]);
|
||||
})
|
||||
);
|
||||
await migrateCourseContent(settingsFromCourseToImport, settings);
|
||||
}
|
||||
}
|
||||
),
|
||||
@@ -171,3 +96,96 @@ export const settingsRouter = router({
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
async function migrateCourseContent(
|
||||
settingsFromCourseToImport: LocalCourseSettings,
|
||||
settings: LocalCourseSettings
|
||||
) {
|
||||
const oldCourseName = settingsFromCourseToImport.name;
|
||||
const newCourseName = settings.name;
|
||||
const oldModules = await getModuleNamesFromFiles(oldCourseName);
|
||||
await Promise.all(
|
||||
oldModules.map(async (moduleName) => {
|
||||
await createModuleFile(newCourseName, moduleName);
|
||||
const [oldAssignments, oldQuizzes, oldPages, oldLecturesByWeek] =
|
||||
await Promise.all([
|
||||
await courseItemFileStorageService.getItems({
|
||||
courseName: oldCourseName,
|
||||
moduleName,
|
||||
type: "Assignment",
|
||||
}),
|
||||
await courseItemFileStorageService.getItems({
|
||||
courseName: oldCourseName,
|
||||
moduleName,
|
||||
type: "Quiz",
|
||||
}),
|
||||
await courseItemFileStorageService.getItems({
|
||||
courseName: oldCourseName,
|
||||
moduleName,
|
||||
type: "Page",
|
||||
}),
|
||||
await getLectures(oldCourseName),
|
||||
]);
|
||||
|
||||
const updateAssignmentPromises = oldAssignments.map(
|
||||
async (oldAssignment) => {
|
||||
const newAssignment = prepAssignmentForNewSemester(
|
||||
oldAssignment,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await updateOrCreateAssignmentFile({
|
||||
courseName: newCourseName,
|
||||
moduleName,
|
||||
assignmentName: newAssignment.name,
|
||||
assignment: newAssignment,
|
||||
});
|
||||
}
|
||||
);
|
||||
const updateQuizzesPromises = oldQuizzes.map(async (oldQuiz) => {
|
||||
const newQuiz = prepQuizForNewSemester(
|
||||
oldQuiz,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await updateQuizFile({
|
||||
courseName: newCourseName,
|
||||
moduleName,
|
||||
quizName: newQuiz.name,
|
||||
quiz: newQuiz,
|
||||
});
|
||||
});
|
||||
const updatePagesPromises = oldPages.map(async (oldPage) => {
|
||||
const newPage = prepPageForNewSemester(
|
||||
oldPage,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await updatePageFile({
|
||||
courseName: newCourseName,
|
||||
moduleName,
|
||||
pageName: newPage.name,
|
||||
page: newPage,
|
||||
});
|
||||
});
|
||||
const updateLecturePromises = oldLecturesByWeek.flatMap(
|
||||
async (oldLectureByWeek) =>
|
||||
oldLectureByWeek.lectures.map(async (oldLecture) => {
|
||||
const newLecture = prepLectureForNewSemester(
|
||||
oldLecture,
|
||||
settingsFromCourseToImport.startDate,
|
||||
settings.startDate
|
||||
);
|
||||
await updateLecture(newCourseName, settings, newLecture);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
...updateAssignmentPromises,
|
||||
...updateQuizzesPromises,
|
||||
...updatePagesPromises,
|
||||
...updateLecturePromises,
|
||||
]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { Lecture } from "@/features/local/lectures/lectureModel";
|
||||
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
|
||||
import { getCoursePathByName } from "@/features/local/globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
lectureFolderName,
|
||||
parseLecture,
|
||||
getLectureWeekName,
|
||||
lectureToString,
|
||||
} from "@/features/local/lectures/lectureUtils";
|
||||
import {
|
||||
LocalCourseSettings,
|
||||
getDayOfWeek,
|
||||
} from "../course/localCourseSettings";
|
||||
|
||||
export async function getLectures(courseName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const courseLectureRoot = path.join(courseDirectory, lectureFolderName);
|
||||
if (!(await directoryExists(courseLectureRoot))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(courseLectureRoot, { withFileTypes: true });
|
||||
const lectureWeekFolders = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
|
||||
const lecturesByWeek = await Promise.all(
|
||||
lectureWeekFolders.map(async (weekName) => {
|
||||
const weekBasePath = path.join(courseLectureRoot, weekName);
|
||||
const fileNames = await fs.readdir(weekBasePath);
|
||||
const lectures = await Promise.all(
|
||||
fileNames.map(async (fileName) => {
|
||||
const filePath = path.join(weekBasePath, fileName);
|
||||
const fileContent = await fs.readFile(filePath, "utf-8");
|
||||
const lecture = parseLecture(fileContent);
|
||||
return lecture;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
weekName,
|
||||
lectures,
|
||||
};
|
||||
})
|
||||
);
|
||||
return lecturesByWeek;
|
||||
}
|
||||
|
||||
export async function updateLecture(
|
||||
courseName: string,
|
||||
courseSettings: LocalCourseSettings,
|
||||
lecture: Lecture
|
||||
) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const courseLectureRoot = path.join(courseDirectory, lectureFolderName);
|
||||
const lectureDate = getDateFromStringOrThrow(
|
||||
lecture.date,
|
||||
"lecture start date in update lecture"
|
||||
);
|
||||
|
||||
const weekFolderName = getLectureWeekName(
|
||||
courseSettings.startDate,
|
||||
lecture.date
|
||||
);
|
||||
const weekPath = path.join(courseLectureRoot, weekFolderName);
|
||||
if (!(await directoryExists(weekPath))) {
|
||||
await fs.mkdir(weekPath, { recursive: true });
|
||||
}
|
||||
|
||||
const lecturePath = path.join(
|
||||
weekPath,
|
||||
`${lectureDate.getDay()}-${getDayOfWeek(lectureDate)}.md`
|
||||
);
|
||||
const lectureContents = lectureToString(lecture);
|
||||
await fs.writeFile(lecturePath, lectureContents);
|
||||
}
|
||||
|
||||
export async function deleteLecture(
|
||||
courseName: string,
|
||||
courseSettings: LocalCourseSettings,
|
||||
dayAsString: string
|
||||
) {
|
||||
console.log("deleting lecture", courseName, dayAsString);
|
||||
const lectureDate = getDateFromStringOrThrow(
|
||||
dayAsString,
|
||||
"lecture start date in update lecture"
|
||||
);
|
||||
|
||||
const weekFolderName = getLectureWeekName(
|
||||
courseSettings.startDate,
|
||||
dayAsString
|
||||
);
|
||||
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const courseLectureRoot = path.join(courseDirectory, lectureFolderName);
|
||||
const weekPath = path.join(courseLectureRoot, weekFolderName);
|
||||
const lecturePath = path.join(
|
||||
weekPath,
|
||||
`${lectureDate.getDay()}-${getDayOfWeek(lectureDate)}.md`
|
||||
);
|
||||
try {
|
||||
await fs.access(lecturePath); // throws error if no file
|
||||
await fs.unlink(lecturePath);
|
||||
console.log(`File deleted: ${lecturePath}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if (error?.code === "ENOENT") {
|
||||
console.log(`Cannot delete lecture, file does not exist: ${lecturePath}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const directoryExists = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fs.stat(path);
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,13 +1,22 @@
|
||||
import { z } from "zod";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { zodLecture } from "@/features/local/lectures/lectureModel";
|
||||
import { Lecture, zodLecture } from "@/features/local/lectures/lectureModel";
|
||||
import {
|
||||
getLectures,
|
||||
updateLecture,
|
||||
deleteLecture,
|
||||
} from "./lectureFileStorageService";
|
||||
import { zodLocalCourseSettings } from "../course/localCourseSettings";
|
||||
getDayOfWeek,
|
||||
LocalCourseSettings,
|
||||
zodLocalCourseSettings,
|
||||
} from "../course/localCourseSettings";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import { getDateFromStringOrThrow } from "../utils/timeUtils";
|
||||
import {
|
||||
lectureFolderName,
|
||||
parseLecture,
|
||||
getLectureWeekName,
|
||||
lectureToString,
|
||||
} from "./lectureUtils";
|
||||
|
||||
export const lectureRouter = router({
|
||||
getLectures: publicProcedure
|
||||
@@ -49,3 +58,112 @@ export const lectureRouter = router({
|
||||
await deleteLecture(courseName, settings, lectureDay);
|
||||
}),
|
||||
});
|
||||
|
||||
export async function getLectures(courseName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const courseLectureRoot = path.join(courseDirectory, lectureFolderName);
|
||||
if (!(await directoryExists(courseLectureRoot))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(courseLectureRoot, { withFileTypes: true });
|
||||
const lectureWeekFolders = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
|
||||
const lecturesByWeek = await Promise.all(
|
||||
lectureWeekFolders.map(async (weekName) => {
|
||||
const weekBasePath = path.join(courseLectureRoot, weekName);
|
||||
const fileNames = await fs.readdir(weekBasePath);
|
||||
const lectures = await Promise.all(
|
||||
fileNames.map(async (fileName) => {
|
||||
const filePath = path.join(weekBasePath, fileName);
|
||||
const fileContent = await fs.readFile(filePath, "utf-8");
|
||||
const lecture = parseLecture(fileContent);
|
||||
return lecture;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
weekName,
|
||||
lectures,
|
||||
};
|
||||
})
|
||||
);
|
||||
return lecturesByWeek;
|
||||
}
|
||||
|
||||
export async function updateLecture(
|
||||
courseName: string,
|
||||
courseSettings: LocalCourseSettings,
|
||||
lecture: Lecture
|
||||
) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const courseLectureRoot = path.join(courseDirectory, lectureFolderName);
|
||||
const lectureDate = getDateFromStringOrThrow(
|
||||
lecture.date,
|
||||
"lecture start date in update lecture"
|
||||
);
|
||||
|
||||
const weekFolderName = getLectureWeekName(
|
||||
courseSettings.startDate,
|
||||
lecture.date
|
||||
);
|
||||
const weekPath = path.join(courseLectureRoot, weekFolderName);
|
||||
if (!(await directoryExists(weekPath))) {
|
||||
await fs.mkdir(weekPath, { recursive: true });
|
||||
}
|
||||
|
||||
const lecturePath = path.join(
|
||||
weekPath,
|
||||
`${lectureDate.getDay()}-${getDayOfWeek(lectureDate)}.md`
|
||||
);
|
||||
const lectureContents = lectureToString(lecture);
|
||||
await fs.writeFile(lecturePath, lectureContents);
|
||||
}
|
||||
|
||||
export async function deleteLecture(
|
||||
courseName: string,
|
||||
courseSettings: LocalCourseSettings,
|
||||
dayAsString: string
|
||||
) {
|
||||
console.log("deleting lecture", courseName, dayAsString);
|
||||
const lectureDate = getDateFromStringOrThrow(
|
||||
dayAsString,
|
||||
"lecture start date in update lecture"
|
||||
);
|
||||
|
||||
const weekFolderName = getLectureWeekName(
|
||||
courseSettings.startDate,
|
||||
dayAsString
|
||||
);
|
||||
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const courseLectureRoot = path.join(courseDirectory, lectureFolderName);
|
||||
const weekPath = path.join(courseLectureRoot, weekFolderName);
|
||||
const lecturePath = path.join(
|
||||
weekPath,
|
||||
`${lectureDate.getDay()}-${getDayOfWeek(lectureDate)}.md`
|
||||
);
|
||||
try {
|
||||
await fs.access(lecturePath); // throws error if no file
|
||||
await fs.unlink(lecturePath);
|
||||
console.log(`File deleted: ${lecturePath}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if (error?.code === "ENOENT") {
|
||||
console.log(`Cannot delete lecture, file does not exist: ${lecturePath}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const directoryExists = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fs.stat(path);
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { promises as fs } from "fs";
|
||||
import { lectureFolderName } from "../lectures/lectureUtils";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
|
||||
export const moduleFileStorageService = {
|
||||
async getModuleNames(courseName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const moduleDirectories = await fs.readdir(courseDirectory, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
const modulePromises = moduleDirectories
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
const modules = await Promise.all(modulePromises);
|
||||
const modulesWithoutLectures = modules.filter(
|
||||
(m) => m !== lectureFolderName
|
||||
);
|
||||
return modulesWithoutLectures.sort((a, b) => a.localeCompare(b));
|
||||
},
|
||||
async createModule(courseName: string, moduleName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
|
||||
await fs.mkdir(courseDirectory + "/" + moduleName, { recursive: true });
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { router } from "@/services/serverFunctions/trpcSetup";
|
||||
import publicProcedure from "@/services/serverFunctions/publicProcedure";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import { promises as fs } from "fs";
|
||||
import { lectureFolderName } from "../lectures/lectureUtils";
|
||||
|
||||
export const moduleRouter = router({
|
||||
getModuleNames: publicProcedure
|
||||
@@ -11,7 +13,7 @@ export const moduleRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { courseName } }) => {
|
||||
return await fileStorageService.modules.getModuleNames(courseName);
|
||||
return await getModuleNamesFromFiles(courseName);
|
||||
}),
|
||||
createModule: publicProcedure
|
||||
.input(
|
||||
@@ -21,6 +23,27 @@ export const moduleRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { courseName, moduleName } }) => {
|
||||
await fileStorageService.modules.createModule(courseName, moduleName);
|
||||
await createModuleFile(courseName, moduleName);
|
||||
}),
|
||||
});
|
||||
|
||||
export async function createModuleFile(courseName: string, moduleName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
|
||||
await fs.mkdir(courseDirectory + "/" + moduleName, { recursive: true });
|
||||
}
|
||||
|
||||
export async function getModuleNamesFromFiles(courseName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const moduleDirectories = await fs.readdir(courseDirectory, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
const modulePromises = moduleDirectories
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
const modules = await Promise.all(modulePromises);
|
||||
const modulesWithoutLectures = modules.filter((m) => m !== lectureFolderName);
|
||||
return modulesWithoutLectures.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { courseItemFileStorageService } from "../course/courseItemFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
LocalCoursePage,
|
||||
localPageMarkdownUtils,
|
||||
} from "@/features/local/pages/localCoursePageModels";
|
||||
|
||||
export const pageFileStorageService = {
|
||||
getPage: async (courseName: string, moduleName: string, name: string) =>
|
||||
await courseItemFileStorageService.getItem(
|
||||
courseName,
|
||||
moduleName,
|
||||
name,
|
||||
"Page"
|
||||
),
|
||||
getPages: async (courseName: string, moduleName: string) =>
|
||||
await courseItemFileStorageService.getItems(courseName, moduleName, "Page"),
|
||||
|
||||
async updatePage({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName,
|
||||
page,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
pageName: string;
|
||||
page: LocalCoursePage;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = path.join(courseDirectory, moduleName, "pages");
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"pages",
|
||||
pageName + ".md"
|
||||
);
|
||||
|
||||
const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
|
||||
console.log(`Saving page ${filePath}`);
|
||||
await fs.writeFile(filePath, pageMarkdown);
|
||||
},
|
||||
async delete({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
pageName: string;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"pages",
|
||||
pageName + ".md"
|
||||
);
|
||||
console.log("removing page", filePath);
|
||||
await fs.unlink(filePath);
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { zodLocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||
import { LocalCoursePage, localPageMarkdownUtils, zodLocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||
import { courseItemFileStorageService } from "../course/courseItemFileStorageService";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
|
||||
export const pageRouter = router({
|
||||
getPage: publicProcedure
|
||||
@@ -14,11 +17,12 @@ export const pageRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { courseName, moduleName, pageName } }) => {
|
||||
return await fileStorageService.pages.getPage(
|
||||
return await courseItemFileStorageService.getItem({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName
|
||||
);
|
||||
name: pageName,
|
||||
type: "Page",
|
||||
});
|
||||
}),
|
||||
|
||||
getAllPages: publicProcedure
|
||||
@@ -29,7 +33,11 @@ export const pageRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { courseName, moduleName } }) => {
|
||||
return await fileStorageService.pages.getPages(courseName, moduleName);
|
||||
return await courseItemFileStorageService.getItems({
|
||||
courseName,
|
||||
moduleName,
|
||||
type: "Page",
|
||||
});
|
||||
}),
|
||||
createPage: publicProcedure
|
||||
.input(
|
||||
@@ -41,7 +49,7 @@ export const pageRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { courseName, moduleName, pageName, page } }) => {
|
||||
await fileStorageService.pages.updatePage({
|
||||
await updatePageFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName,
|
||||
@@ -70,7 +78,7 @@ export const pageRouter = router({
|
||||
previousPageName,
|
||||
},
|
||||
}) => {
|
||||
await fileStorageService.pages.updatePage({
|
||||
await updatePageFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName,
|
||||
@@ -81,7 +89,7 @@ export const pageRouter = router({
|
||||
pageName !== previousPageName ||
|
||||
moduleName !== previousModuleName
|
||||
) {
|
||||
await fileStorageService.pages.delete({
|
||||
await deletePageFile({
|
||||
courseName,
|
||||
moduleName: previousModuleName,
|
||||
pageName: previousPageName,
|
||||
@@ -98,10 +106,56 @@ export const pageRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { courseName, moduleName, pageName } }) => {
|
||||
await fileStorageService.pages.delete({
|
||||
await deletePageFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
export async function updatePageFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName,
|
||||
page,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
pageName: string;
|
||||
page: LocalCoursePage;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = path.join(courseDirectory, moduleName, "pages");
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"pages",
|
||||
pageName + ".md"
|
||||
);
|
||||
|
||||
const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
|
||||
console.log(`Saving page ${filePath}`);
|
||||
await fs.writeFile(filePath, pageMarkdown);
|
||||
}
|
||||
async function deletePageFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
pageName,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
pageName: string;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"pages",
|
||||
pageName + ".md"
|
||||
);
|
||||
console.log("removing page", filePath);
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
|
||||
236
src/features/local/parsingTests/assignmentMarkdown.test.ts
Normal file
236
src/features/local/parsingTests/assignmentMarkdown.test.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { LocalAssignment } from "../assignments/models/localAssignment";
|
||||
import { AssignmentSubmissionType } from "../assignments/models/assignmentSubmissionType";
|
||||
import { assignmentMarkdownSerializer } from "../assignments/models/utils/assignmentMarkdownSerializer";
|
||||
import { assignmentMarkdownParser } from "../assignments/models/utils/assignmentMarkdownParser";
|
||||
|
||||
describe("AssignmentMarkdownTests", () => {
|
||||
it("can parse assignment settings", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment with empty rubric can be parsed", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment with empty submission types can be parsed", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment without lockAt date can be parsed", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: undefined,
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment without description can be parsed", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [
|
||||
{ points: 4, label: "do task 1" },
|
||||
{ points: 2, label: "do task 2" },
|
||||
],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignments can have three dashes", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "test assignment\n---\nsomestuff",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignments can restrict upload types", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
allowedFileUploadExtensions: ["pdf", "txt"],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment with githubClassroomAssignmentShareLink and githubClassroomAssignmentLink can be parsed", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
githubClassroomAssignmentShareLink: "https://github.com/share-link",
|
||||
githubClassroomAssignmentLink: "https://github.com/assignment-link",
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment.githubClassroomAssignmentShareLink).toEqual(
|
||||
"https://github.com/share-link"
|
||||
);
|
||||
expect(parsedAssignment.githubClassroomAssignmentLink).toEqual(
|
||||
"https://github.com/assignment-link"
|
||||
);
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
|
||||
it("assignment without githubClassroomAssignmentShareLink and githubClassroomAssignmentLink can be parsed", () => {
|
||||
const name = "test assignment";
|
||||
const assignment: LocalAssignment = {
|
||||
name,
|
||||
description: "here is the description",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const assignmentMarkdown =
|
||||
assignmentMarkdownSerializer.toMarkdown(assignment);
|
||||
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
|
||||
assignmentMarkdown,
|
||||
name
|
||||
);
|
||||
|
||||
expect(parsedAssignment.githubClassroomAssignmentShareLink).toBeUndefined();
|
||||
expect(parsedAssignment.githubClassroomAssignmentLink).toBeUndefined();
|
||||
expect(parsedAssignment).toEqual(assignment);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { GlobalSettings } from "@/features/local/globalSettings/globalSettingsModels";
|
||||
import { globalSettingsToYaml, parseGlobalSettingsYaml } from "@/features/local/globalSettings/globalSettingsUtils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("GlobalSettingsMarkdownTests", () => {
|
||||
it("can parse global settings", () => {
|
||||
const globalSettings: GlobalSettings = {
|
||||
courses: [
|
||||
{
|
||||
path: "./distributed/2025-alex/modules",
|
||||
name: "distributed",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const globalSettingsMarkdown = globalSettingsToYaml(globalSettings);
|
||||
const parsedGlobalSettings = parseGlobalSettingsYaml(
|
||||
globalSettingsMarkdown
|
||||
);
|
||||
|
||||
expect(parsedGlobalSettings).toEqual(globalSettings);
|
||||
});
|
||||
});
|
||||
19
src/features/local/parsingTests/pageMarkdown.test.ts
Normal file
19
src/features/local/parsingTests/pageMarkdown.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { LocalCoursePage, localPageMarkdownUtils } from "@/features/local/pages/localCoursePageModels";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("PageMarkdownTests", () => {
|
||||
it("can parse page", () => {
|
||||
const name = "test title"
|
||||
const page: LocalCoursePage = {
|
||||
name,
|
||||
text: "test text content",
|
||||
dueAt: "07/09/2024 23:59:00",
|
||||
};
|
||||
|
||||
const pageMarkdownString = localPageMarkdownUtils.toMarkdown(page);
|
||||
|
||||
const parsedPage = localPageMarkdownUtils.parseMarkdown(pageMarkdownString, name);
|
||||
|
||||
expect(parsedPage).toEqual(page);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("Matching Answer Error Messages", () => {
|
||||
it("can parse matching 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:
|
||||
---
|
||||
|
||||
question without answer
|
||||
|
||||
`;
|
||||
|
||||
expect(() =>
|
||||
quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name)
|
||||
).toThrowError(/question type/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("MatchingTests", () => {
|
||||
it("can parse matching 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:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ identifier - name of a variable
|
||||
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MATCHING);
|
||||
expect(firstQuestion.text).not.toContain("statement");
|
||||
expect(firstQuestion.answers[0].matchedText).toBe(
|
||||
"a single command to be executed"
|
||||
);
|
||||
});
|
||||
|
||||
it("can create markdown for matching 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:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ identifier - name of a variable
|
||||
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const questionMarkdown = quizQuestionMarkdownUtils.toMarkdown(
|
||||
quiz.questions[0]
|
||||
);
|
||||
const expectedMarkdown = `Points: 1
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ identifier - name of a variable
|
||||
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)`;
|
||||
|
||||
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||
});
|
||||
|
||||
it("whitespace is optional", () => {
|
||||
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:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^statement - a single command to be executed
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
expect(quiz.questions[0].answers[0].text).toBe("statement");
|
||||
});
|
||||
|
||||
it("can have distractors", () => {
|
||||
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:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ - this is the distractor
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
expect(quiz.questions[0].matchDistractors).toEqual([
|
||||
"this is the distractor",
|
||||
]);
|
||||
});
|
||||
|
||||
it("can have distractors and be persisted", () => {
|
||||
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:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ statement - a single command to be executed
|
||||
^ - this is the distractor
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
|
||||
expect(quizMarkdown).toContain(
|
||||
"^ statement - a single command to be executed\n^ - this is the distractor"
|
||||
);
|
||||
});
|
||||
it("can escape - characters", () => {
|
||||
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:
|
||||
---
|
||||
Match the following terms & definitions
|
||||
|
||||
^ git add \-\-all - start tracking all files in the current directory and subdirectories
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.answers[0].text).toBe("git add --all");
|
||||
expect(firstQuestion.answers[0].matchedText).toBe(
|
||||
"start tracking all files in the current directory and subdirectories"
|
||||
);
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
|
||||
expect(quizMarkdown).toContain(
|
||||
"^ git add --all - start tracking all files in the current directory and subdirectories"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("MultipleAnswersTests", () => {
|
||||
it("quiz markdown includes multiple answer question", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "desc",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
showCorrectAnswers: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
questions: [
|
||||
{
|
||||
text: "oneline question",
|
||||
points: 1,
|
||||
questionType: QuestionType.MULTIPLE_ANSWERS,
|
||||
answers: [
|
||||
{ correct: true, text: "true" },
|
||||
{ correct: true, text: "false" },
|
||||
{ correct: false, text: "neither" },
|
||||
],
|
||||
matchDistractors: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const expectedQuestionString = `Points: 1
|
||||
oneline question
|
||||
[*] true
|
||||
[*] false
|
||||
[ ] neither`;
|
||||
expect(markdown).toContain(expectedQuestionString);
|
||||
});
|
||||
|
||||
it("can parse question with multiple 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?
|
||||
[*] click
|
||||
[*] focus
|
||||
[*] mousedown
|
||||
[ ] submit
|
||||
[ ] change
|
||||
[ ] mouseout
|
||||
[ ] keydown
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_ANSWERS);
|
||||
expect(firstQuestion.text).toContain(
|
||||
"Which events are triggered when the user clicks on an input field?"
|
||||
);
|
||||
expect(firstQuestion.answers[0].text).toBe("click");
|
||||
expect(firstQuestion.answers[0].correct).toBe(true);
|
||||
expect(firstQuestion.answers[3].correct).toBe(false);
|
||||
expect(firstQuestion.answers[3].text).toBe("submit");
|
||||
});
|
||||
|
||||
it("can parse question with multiple answers without a space in false 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?
|
||||
[*] click
|
||||
[] submit
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
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 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: 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, name);
|
||||
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?
|
||||
[*] \`int[] theThing()\`
|
||||
[ ] keydown
|
||||
`;
|
||||
|
||||
const question = quizQuestionMarkdownUtils.parseMarkdown(
|
||||
rawMarkdownQuestion,
|
||||
0
|
||||
);
|
||||
|
||||
expect(question.answers[0].text).toBe("`int[] theThing()`");
|
||||
expect(question.answers.length).toBe(2);
|
||||
});
|
||||
|
||||
it("can use braces in answer for multiple answer with multiline", () => {
|
||||
const rawMarkdownQuestion = `
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
[*]
|
||||
\`\`\`
|
||||
int[] myNumbers = new int[] { };
|
||||
DoSomething(ref myNumbers);
|
||||
static void DoSomething(ref int[] numbers)
|
||||
{
|
||||
// do something
|
||||
}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const question = quizQuestionMarkdownUtils.parseMarkdown(
|
||||
rawMarkdownQuestion,
|
||||
0
|
||||
);
|
||||
|
||||
expect(question.answers[0].text).toBe(`\`\`\`
|
||||
int[] myNumbers = new int[] { };
|
||||
DoSomething(ref myNumbers);
|
||||
static void DoSomething(ref int[] numbers)
|
||||
{
|
||||
// do something
|
||||
}
|
||||
\`\`\``);
|
||||
expect(question.answers.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("MultipleChoiceTests", () => {
|
||||
it("quiz markdown includes multiple choice question", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: "desc",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
showCorrectAnswers: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
questions: [
|
||||
{
|
||||
points: 2,
|
||||
text: `
|
||||
\`some type\` of question
|
||||
|
||||
with many
|
||||
|
||||
\`\`\`
|
||||
lines
|
||||
\`\`\`
|
||||
`,
|
||||
questionType: QuestionType.MULTIPLE_CHOICE,
|
||||
answers: [
|
||||
{ correct: true, text: "true" },
|
||||
{ correct: false, text: "false\n\nendline" },
|
||||
],
|
||||
matchDistractors: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const expectedQuestionString = `
|
||||
Points: 2
|
||||
|
||||
\`some type\` of question
|
||||
|
||||
with many
|
||||
|
||||
\`\`\`
|
||||
lines
|
||||
\`\`\`
|
||||
|
||||
*a) true
|
||||
b) false
|
||||
|
||||
endline`;
|
||||
expect(markdown).toContain(expectedQuestionString);
|
||||
});
|
||||
|
||||
it("letter optional for multiple choice", () => {
|
||||
const questionMarkdown = `
|
||||
Points: 2
|
||||
\`some type\` of question
|
||||
*) true
|
||||
) false
|
||||
`;
|
||||
|
||||
const question = quizQuestionMarkdownUtils.parseMarkdown(
|
||||
questionMarkdown,
|
||||
0
|
||||
);
|
||||
expect(question.answers.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
// Test suite for deterministic checks on LocalQuiz
|
||||
describe("QuizDeterministicChecks", () => {
|
||||
it("SerializationIsDeterministic_EmptyQuiz", () => {
|
||||
const name = "Test Quiz";
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_ShowCorrectAnswers", () => {
|
||||
const name = "Test Quiz";
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
showCorrectAnswers: false,
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [],
|
||||
allowedAttempts: -1,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_ShortAnswer", () => {
|
||||
const name = "Test Quiz";
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test short answer",
|
||||
questionType: QuestionType.SHORT_ANSWER,
|
||||
points: 1,
|
||||
answers: [],
|
||||
matchDistractors: [],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_Essay", () => {
|
||||
const name = "Test Quiz";
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test essay",
|
||||
questionType: QuestionType.ESSAY,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_MultipleAnswer", () => {
|
||||
const name = "Test Quiz";
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test multiple answer",
|
||||
questionType: QuestionType.MULTIPLE_ANSWERS,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [
|
||||
{ text: "yes", correct: true },
|
||||
{ text: "no", correct: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_MultipleChoice", () => {
|
||||
const name = "Test Quiz";
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
password: undefined,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test multiple choice",
|
||||
questionType: QuestionType.MULTIPLE_CHOICE,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [
|
||||
{ text: "yes", correct: true },
|
||||
{ text: "no", correct: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
|
||||
it("SerializationIsDeterministic_Matching", () => {
|
||||
const name = "Test Quiz";
|
||||
const quiz: LocalQuiz = {
|
||||
name,
|
||||
description: "quiz description",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: true,
|
||||
password: undefined,
|
||||
localAssignmentGroupName: "Assignments",
|
||||
questions: [
|
||||
{
|
||||
text: "test matching",
|
||||
questionType: QuestionType.MATCHING,
|
||||
points: 1,
|
||||
matchDistractors: [],
|
||||
answers: [
|
||||
{ text: "yes", correct: true, matchedText: "testing yes" },
|
||||
{ text: "no", correct: true, matchedText: "testing no" },
|
||||
],
|
||||
},
|
||||
],
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: true,
|
||||
};
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
|
||||
|
||||
expect(parsedQuiz).toEqual(quiz);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { markdownToHtmlNoImages } from "@/services/htmlMarkdownUtils";
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
|
||||
|
||||
// Test suite for QuizMarkdown
|
||||
describe("QuizMarkdownTests", () => {
|
||||
it("can serialize quiz to markdown", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: `
|
||||
# quiz description
|
||||
|
||||
this is my description in markdown
|
||||
|
||||
\`here is code\`
|
||||
`,
|
||||
lockAt: new Date(8640000000000000).toISOString(), // DateTime.MaxValue equivalent in TypeScript
|
||||
dueAt: new Date(8640000000000000).toISOString(),
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: false,
|
||||
questions: [],
|
||||
};
|
||||
|
||||
const markdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
|
||||
expect(markdown).not.toContain("Name: Test Quiz");
|
||||
expect(markdown).toContain(quiz.description);
|
||||
expect(markdown).toContain("ShuffleAnswers: true");
|
||||
expect(markdown).toContain("OneQuestionAtATime: false");
|
||||
expect(markdown).toContain("AssignmentGroup: someId");
|
||||
expect(markdown).toContain("AllowedAttempts: -1");
|
||||
});
|
||||
|
||||
it("can parse markdown quiz with no questions", () => {
|
||||
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
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
|
||||
const expectedDescription = `
|
||||
this is the
|
||||
multi line
|
||||
description`;
|
||||
|
||||
expect(quiz.name).toBe("Test Quiz");
|
||||
expect(quiz.shuffleAnswers).toBe(true);
|
||||
expect(quiz.oneQuestionAtATime).toBe(false);
|
||||
expect(quiz.allowedAttempts).toBe(-1);
|
||||
expect(quiz.description.trim()).toBe(expectedDescription.trim());
|
||||
});
|
||||
|
||||
it("can parse markdown quiz with password", () => {
|
||||
const password = "this-is-the-password";
|
||||
const name = "Test Quiz";
|
||||
const rawMarkdownQuiz = `
|
||||
Password: ${password}
|
||||
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
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
|
||||
expect(quiz.password).toBe(password);
|
||||
});
|
||||
|
||||
it("can parse markdown quiz and configure to show correct answers", () => {
|
||||
const name = "Test Quiz";
|
||||
const rawMarkdownQuiz = `
|
||||
ShuffleAnswers: true
|
||||
OneQuestionAtATime: false
|
||||
ShowCorrectAnswers: 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
|
||||
---
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
|
||||
expect(quiz.showCorrectAnswers).toBe(false);
|
||||
});
|
||||
|
||||
it("can parse quiz with questions", () => {
|
||||
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
|
||||
\`some type\` of question
|
||||
|
||||
with many
|
||||
|
||||
\`\`\`
|
||||
lines
|
||||
\`\`\`
|
||||
|
||||
*a) true
|
||||
b) false
|
||||
|
||||
endline`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_CHOICE);
|
||||
expect(firstQuestion.points).toBe(2);
|
||||
expect(firstQuestion.text).toContain("```");
|
||||
expect(firstQuestion.text).toContain("`some type` of question");
|
||||
expect(firstQuestion.answers[0].text).toBe("true");
|
||||
expect(firstQuestion.answers[0].correct).toBe(true);
|
||||
expect(firstQuestion.answers[1].correct).toBe(false);
|
||||
expect(firstQuestion.answers[1].text).toContain("endline");
|
||||
});
|
||||
|
||||
it("can parse multiple questions", () => {
|
||||
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?
|
||||
[*] click
|
||||
---
|
||||
Points: 2
|
||||
\`some type\` of question
|
||||
*a) true
|
||||
b) false
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_ANSWERS);
|
||||
|
||||
const secondQuestion = quiz.questions[1];
|
||||
expect(secondQuestion.points).toBe(2);
|
||||
expect(secondQuestion.questionType).toBe(QuestionType.MULTIPLE_CHOICE);
|
||||
});
|
||||
|
||||
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?
|
||||
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?
|
||||
short_answer`;
|
||||
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||
});
|
||||
|
||||
it("negative points is allowed", () => {
|
||||
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: -4
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
expect(firstQuestion.points).toBe(-4);
|
||||
});
|
||||
|
||||
it("floating point points is allowed", () => {
|
||||
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: 4.56
|
||||
Which events are triggered when the user clicks on an input field?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
expect(firstQuestion.points).toBe(4.56);
|
||||
});
|
||||
|
||||
it("can parse quiz with latex in a question", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
278
src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts
Normal file
278
src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { QuestionType, zodQuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
|
||||
import {
|
||||
getAnswers,
|
||||
getQuestionType,
|
||||
} from "@/services/canvas/canvasQuizService";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("TextAnswerTests", () => {
|
||||
it("can parse essay", () => {
|
||||
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?
|
||||
essay
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.ESSAY);
|
||||
expect(firstQuestion.text).not.toContain("essay");
|
||||
});
|
||||
|
||||
it("can parse short answer", () => {
|
||||
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?
|
||||
short answer
|
||||
`;
|
||||
|
||||
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
|
||||
const firstQuestion = quiz.questions[0];
|
||||
|
||||
expect(firstQuestion.points).toBe(1);
|
||||
expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER);
|
||||
expect(firstQuestion.text).not.toContain("short answer");
|
||||
});
|
||||
|
||||
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?
|
||||
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?
|
||||
short_answer`;
|
||||
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", () => {
|
||||
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?
|
||||
essay
|
||||
`;
|
||||
|
||||
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?
|
||||
essay`;
|
||||
expect(questionMarkdown).toContain(expectedMarkdown);
|
||||
});
|
||||
|
||||
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("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");
|
||||
});
|
||||
});
|
||||
100
src/features/local/parsingTests/rubricMarkdown.test.ts
Normal file
100
src/features/local/parsingTests/rubricMarkdown.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
RubricItem,
|
||||
rubricItemIsExtraCredit,
|
||||
} from "../assignments/models/rubricItem";
|
||||
import { assignmentMarkdownParser } from "../assignments/models/utils/assignmentMarkdownParser";
|
||||
|
||||
describe("RubricMarkdownTests", () => {
|
||||
it("can parse one item", () => {
|
||||
const rawRubric = `
|
||||
- 2pts: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric.length).toBe(1);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
|
||||
expect(rubric[0].label).toBe("this is the task");
|
||||
expect(rubric[0].points).toBe(2);
|
||||
});
|
||||
|
||||
it("can parse multiple items", () => {
|
||||
const rawRubric = `
|
||||
- 2pts: this is the task
|
||||
- 3pts: this is the other task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric.length).toBe(2);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
|
||||
expect(rubric[1].label).toBe("this is the other task");
|
||||
expect(rubric[1].points).toBe(3);
|
||||
});
|
||||
|
||||
it("can parse single point", () => {
|
||||
const rawRubric = `
|
||||
- 1pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
|
||||
expect(rubric[0].label).toBe("this is the task");
|
||||
expect(rubric[0].points).toBe(1);
|
||||
});
|
||||
|
||||
it("can parse single extra credit (lower case)", () => {
|
||||
const rawRubric = `
|
||||
- 1pt: (extra credit) this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(true);
|
||||
expect(rubric[0].label).toBe("(extra credit) this is the task");
|
||||
});
|
||||
|
||||
it("can parse single extra credit (upper case)", () => {
|
||||
const rawRubric = `
|
||||
- 1pt: (Extra Credit) this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubricItemIsExtraCredit(rubric[0])).toBe(true);
|
||||
expect(rubric[0].label).toBe("(Extra Credit) this is the task");
|
||||
});
|
||||
|
||||
it("can parse floating point numbers", () => {
|
||||
const rawRubric = `
|
||||
- 1.5pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric[0].points).toBe(1.5);
|
||||
});
|
||||
|
||||
it("can parse negative numbers", () => {
|
||||
const rawRubric = `
|
||||
- -2pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric[0].points).toBe(-2);
|
||||
});
|
||||
|
||||
it("can parse negative floating point numbers", () => {
|
||||
const rawRubric = `
|
||||
- -2895.00053pt: this is the task
|
||||
`;
|
||||
|
||||
const rubric: RubricItem[] =
|
||||
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
|
||||
expect(rubric[0].points).toBe(-2895.00053);
|
||||
});
|
||||
});
|
||||
31
src/features/local/parsingTests/testHolidayParsing.test.ts
Normal file
31
src/features/local/parsingTests/testHolidayParsing.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseHolidays } from "../utils/settingsUtils";
|
||||
|
||||
describe("can parse holiday string", () => {
|
||||
it("can parse empty list", () => {
|
||||
const testString = `
|
||||
springBreak:
|
||||
`;
|
||||
const output = parseHolidays(testString);
|
||||
expect(output).toEqual([{ name: "springBreak", days: [] }]);
|
||||
});
|
||||
it("can parse list with date", () => {
|
||||
const testString = `
|
||||
springBreak:
|
||||
- 10/12/2024
|
||||
`;
|
||||
const output = parseHolidays(testString);
|
||||
expect(output).toEqual([{ name: "springBreak", days: ["10/12/2024"] }]);
|
||||
});
|
||||
it("can parse list with two dates", () => {
|
||||
const testString = `
|
||||
springBreak:
|
||||
- 10/12/2024
|
||||
- 10/13/2024
|
||||
`;
|
||||
const output = parseHolidays(testString);
|
||||
expect(output).toEqual([
|
||||
{ name: "springBreak", days: ["10/12/2024", "10/13/2024"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
222
src/features/local/parsingTests/testSemesterImport.test.ts
Normal file
222
src/features/local/parsingTests/testSemesterImport.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { LocalAssignment } from "../assignments/models/localAssignment";
|
||||
import {
|
||||
prepAssignmentForNewSemester,
|
||||
prepLectureForNewSemester,
|
||||
prepPageForNewSemester,
|
||||
prepQuizForNewSemester,
|
||||
} from "../utils/semesterTransferUtils";
|
||||
import { Lecture } from "../lectures/lectureModel";
|
||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
|
||||
describe("can take an assignment and template it for a new semester", () => {
|
||||
it("can sanitize assignment github classroom repo url", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `
|
||||
## test description
|
||||
|
||||
[GitHub Classroom Assignment](https://classroom.github.com/a/y_eOxTfL)
|
||||
|
||||
other stuff below`,
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.description).toEqual(`
|
||||
## test description
|
||||
|
||||
[GitHub Classroom Assignment](insert_github_classroom_url)
|
||||
|
||||
other stuff below`);
|
||||
});
|
||||
|
||||
it("can sanitize assignment github classroom repo url 2", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `
|
||||
<https://classroom.github.com/a/y_eOxTfL>
|
||||
other stuff below`,
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.description).toEqual(`
|
||||
<insert_github_classroom_url>
|
||||
other stuff below`);
|
||||
});
|
||||
|
||||
it("can sanitize assignment github classroom repo url 3", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `https://classroom.github.com/a/y_eOxTfL other things`,
|
||||
dueAt: "08/21/2023 23:59:00",
|
||||
lockAt: "08/21/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.description).toEqual(
|
||||
`insert_github_classroom_url other things`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can offset date based on new semester start", () => {
|
||||
it("assignment with new semester start", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `https://classroom.github.com/a/y_eOxTfL other things`,
|
||||
dueAt: "08/29/2023 23:59:00",
|
||||
lockAt: "08/29/2023 23:59:00",
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.dueAt).toEqual("01/11/2024 23:59:00");
|
||||
expect(sanitizedAssignment.lockAt).toEqual("01/11/2024 23:59:00");
|
||||
});
|
||||
it("assignment with new semester start, no lock date", () => {
|
||||
const assignment: LocalAssignment = {
|
||||
name: "test assignment",
|
||||
description: `https://classroom.github.com/a/y_eOxTfL other things`,
|
||||
dueAt: "08/29/2023 23:59:00",
|
||||
lockAt: undefined,
|
||||
submissionTypes: [],
|
||||
localAssignmentGroupName: "Final Project",
|
||||
rubric: [],
|
||||
allowedFileUploadExtensions: [],
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedAssignment = prepAssignmentForNewSemester(
|
||||
assignment,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedAssignment.dueAt).toEqual("01/11/2024 23:59:00");
|
||||
expect(sanitizedAssignment.lockAt).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can prep quizzes", () => {
|
||||
it("quiz gets new lock and due dates", () => {
|
||||
const quiz: LocalQuiz = {
|
||||
name: "Test Quiz",
|
||||
description: `
|
||||
# quiz description
|
||||
`,
|
||||
dueAt: "08/29/2023 23:59:00",
|
||||
lockAt: "08/30/2023 23:59:00",
|
||||
shuffleAnswers: true,
|
||||
oneQuestionAtATime: false,
|
||||
localAssignmentGroupName: "someId",
|
||||
allowedAttempts: -1,
|
||||
showCorrectAnswers: false,
|
||||
questions: [],
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedQuiz = prepQuizForNewSemester(
|
||||
quiz,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedQuiz.dueAt).toEqual("01/11/2024 23:59:00");
|
||||
expect(sanitizedQuiz.lockAt).toEqual("01/12/2024 23:59:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("can prep pages", () => {
|
||||
it("page gets new due date and github url changes", () => {
|
||||
const page: LocalCoursePage = {
|
||||
name: "test title",
|
||||
text: "test text content",
|
||||
dueAt: "08/30/2023 23:59:00",
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedPage = prepPageForNewSemester(
|
||||
page,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedPage.dueAt).toEqual("01/12/2024 23:59:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("can prep lecture", () => {
|
||||
it("lecture gets new date, github url changes", () => {
|
||||
const lecture: Lecture = {
|
||||
name: "test title",
|
||||
date: "08/30/2023",
|
||||
content: "test text content",
|
||||
};
|
||||
|
||||
const oldSemesterStartDate = "08/26/2023 23:59:00";
|
||||
const newSemesterStartDate = "01/08/2024 23:59:00";
|
||||
|
||||
const sanitizedLecture = prepLectureForNewSemester(
|
||||
lecture,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
|
||||
expect(sanitizedLecture.date).toEqual("01/12/2024");
|
||||
});
|
||||
});
|
||||
71
src/features/local/parsingTests/timeUtils.test.ts
Normal file
71
src/features/local/parsingTests/timeUtils.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { dateToMarkdownString, getDateFromString } from "../utils/timeUtils";
|
||||
|
||||
describe("Can properly handle expected date formats", () => {
|
||||
it("can use AM/PM dates", () => {
|
||||
const dateString = "8/27/2024 1:00:00 AM";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can use 24 hour dates", () => {
|
||||
const dateString = "8/27/2024 23:95:00";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can use ISO format", () => {
|
||||
const dateString = "2024-08-26T00:00:00.0000000";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can use other ISO format", () => {
|
||||
const dateString = "2024-08-26T06:00:00Z";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
});
|
||||
it("can get correct time from format", () => {
|
||||
const dateString = "08/28/2024 23:59:00";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject?.getDate()).toBe(28);
|
||||
expect(dateObject?.getMonth()).toBe(8 - 1); // 0 based
|
||||
expect(dateObject?.getFullYear()).toBe(2024);
|
||||
expect(dateObject?.getMinutes()).toBe(59);
|
||||
expect(dateObject?.getHours()).toBe(23);
|
||||
expect(dateObject?.getSeconds()).toBe(0);
|
||||
});
|
||||
it("can get correct time from format", () => {
|
||||
const dateString = "8/27/2024 1:00:00 AM";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject?.getDate()).toBe(27);
|
||||
expect(dateObject?.getMonth()).toBe(8 - 1); // 0 based
|
||||
expect(dateObject?.getFullYear()).toBe(2024);
|
||||
expect(dateObject?.getMinutes()).toBe(0);
|
||||
expect(dateObject?.getHours()).toBe(1);
|
||||
expect(dateObject?.getSeconds()).toBe(0);
|
||||
});
|
||||
it("can get correct time from format", () => {
|
||||
const dateString = "08/27/2024 23:59:00";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
const updatedString = dateToMarkdownString(dateObject!);
|
||||
expect(updatedString).toBe(dateString);
|
||||
});
|
||||
it("can handle canvas time format", () => {
|
||||
const dateString = "8/29/2024, 5:00:00 PM";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
const updatedString = dateToMarkdownString(dateObject!);
|
||||
expect(updatedString).toBe("08/29/2024 17:00:00");
|
||||
});
|
||||
it("can handle date without time", () => {
|
||||
const dateString = "8/29/2024";
|
||||
const dateObject = getDateFromString(dateString);
|
||||
|
||||
expect(dateObject).not.toBeUndefined();
|
||||
const updatedString = dateToMarkdownString(dateObject!);
|
||||
expect(updatedString).toBe("08/29/2024 00:00:00");
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { courseItemFileStorageService } from "../course/courseItemFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
|
||||
export const quizFileStorageService = {
|
||||
getQuiz: async (courseName: string, moduleName: string, quizName: string) =>
|
||||
await courseItemFileStorageService.getItem(
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
"Quiz"
|
||||
),
|
||||
getQuizzes: async (courseName: string, moduleName: string) =>
|
||||
await courseItemFileStorageService.getItems(courseName, moduleName, "Quiz"),
|
||||
|
||||
async updateQuiz({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
quiz,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
quizName: string;
|
||||
quiz: LocalQuiz;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = path.join(courseDirectory, moduleName, "quizzes");
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"quizzes",
|
||||
quizName + ".md"
|
||||
);
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
console.log(`Saving quiz ${filePath}`);
|
||||
await fs.writeFile(filePath, quizMarkdown);
|
||||
},
|
||||
async delete({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
quizName: string;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"quizzes",
|
||||
quizName + ".md"
|
||||
);
|
||||
console.log("removing quiz", filePath);
|
||||
await fs.unlink(filePath);
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,15 @@
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { zodLocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import {
|
||||
LocalQuiz,
|
||||
zodLocalQuiz,
|
||||
} from "@/features/local/quizzes/models/localQuiz";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { quizMarkdownUtils } from "./models/utils/quizMarkdownUtils";
|
||||
import { courseItemFileStorageService } from "../course/courseItemFileStorageService";
|
||||
|
||||
export const quizRouter = router({
|
||||
getQuiz: publicProcedure
|
||||
@@ -14,11 +21,12 @@ export const quizRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { courseName, moduleName, quizName } }) => {
|
||||
return await fileStorageService.quizzes.getQuiz(
|
||||
return await courseItemFileStorageService.getItem({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName
|
||||
);
|
||||
name: quizName,
|
||||
type: "Quiz",
|
||||
});
|
||||
}),
|
||||
|
||||
getAllQuizzes: publicProcedure
|
||||
@@ -29,10 +37,11 @@ export const quizRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { courseName, moduleName } }) => {
|
||||
return await fileStorageService.quizzes.getQuizzes(
|
||||
return await courseItemFileStorageService.getItems({
|
||||
courseName,
|
||||
moduleName
|
||||
);
|
||||
moduleName,
|
||||
type: "Quiz",
|
||||
});
|
||||
}),
|
||||
createQuiz: publicProcedure
|
||||
.input(
|
||||
@@ -44,7 +53,7 @@ export const quizRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { courseName, moduleName, quizName, quiz } }) => {
|
||||
await fileStorageService.quizzes.updateQuiz({
|
||||
await updateQuizFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
@@ -73,7 +82,7 @@ export const quizRouter = router({
|
||||
previousQuizName,
|
||||
},
|
||||
}) => {
|
||||
await fileStorageService.quizzes.updateQuiz({
|
||||
await updateQuizFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
@@ -84,7 +93,7 @@ export const quizRouter = router({
|
||||
quizName !== previousQuizName ||
|
||||
moduleName !== previousModuleName
|
||||
) {
|
||||
await fileStorageService.quizzes.delete({
|
||||
await deleteQuizFile({
|
||||
courseName,
|
||||
moduleName: previousModuleName,
|
||||
quizName: previousQuizName,
|
||||
@@ -101,10 +110,56 @@ export const quizRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { courseName, moduleName, quizName } }) => {
|
||||
await fileStorageService.quizzes.delete({
|
||||
await deleteQuizFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
export async function deleteQuizFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
quizName: string;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"quizzes",
|
||||
quizName + ".md"
|
||||
);
|
||||
console.log("removing quiz", filePath);
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
|
||||
export async function updateQuizFile({
|
||||
courseName,
|
||||
moduleName,
|
||||
quizName,
|
||||
quiz,
|
||||
}: {
|
||||
courseName: string;
|
||||
moduleName: string;
|
||||
quizName: string;
|
||||
quiz: LocalQuiz;
|
||||
}) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
const folder = path.join(courseDirectory, moduleName, "quizzes");
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
const filePath = path.join(
|
||||
courseDirectory,
|
||||
moduleName,
|
||||
"quizzes",
|
||||
quizName + ".md"
|
||||
);
|
||||
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
console.log(`Saving quiz ${filePath}`);
|
||||
await fs.writeFile(filePath, quizMarkdown);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { basePath, directoryOrFileExists } from "./fileSystemUtils";
|
||||
import { quizFileStorageService } from "../quizzes/quizFileStorageService";
|
||||
import { pageFileStorageService } from "../pages/pageFileStorageService";
|
||||
import { moduleFileStorageService } from "../modules/moduleFileStorageService";
|
||||
import { settingsFileStorageService } from "../course/settingsFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import { assignmentsFileStorageService } from "@/features/local/assignments/assignmentsFileStorageService";
|
||||
|
||||
export const fileStorageService = {
|
||||
settings: settingsFileStorageService,
|
||||
modules: moduleFileStorageService,
|
||||
assignments: assignmentsFileStorageService,
|
||||
quizzes: quizFileStorageService,
|
||||
pages: pageFileStorageService,
|
||||
|
||||
async getEmptyDirectories(): Promise<string[]> {
|
||||
if (!(await directoryOrFileExists(basePath))) {
|
||||
|
||||
Reference in New Issue
Block a user