From 815f929c2df3b7df339fe7a2aa454763caa74b88 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Wed, 23 Jul 2025 11:25:12 -0600 Subject: [PATCH] more code refactor to colocate feature code --- src/app/api/mcp/[transport]/route.ts | 23 ++- .../local/assignments/assignmentRouter.ts | 84 ++++++-- .../assignmentsFileStorageService.ts | 100 --------- .../course/courseItemFileStorageService.ts | 92 +++------ src/features/local/course/settingsRouter.ts | 194 ++++++++++-------- .../lectures/lectureFileStorageService.ts | 124 ----------- src/features/local/lectures/lectureRouter.ts | 130 +++++++++++- .../local/modules/moduleFileStorageService.ts | 27 --- src/features/local/modules/moduleRouter.ts | 29 ++- .../local/pages/pageFileStorageService.ts | 66 ------ src/features/local/pages/pageRouter.ts | 74 ++++++- .../parsingTests}/assignmentMarkdown.test.ts | 8 +- .../globalSettingsMarkdown.test.ts | 0 .../local/parsingTests}/pageMarkdown.test.ts | 0 .../quizMarkdown/matchingAnswerErrors.test.ts | 0 .../quizMarkdown/matchingAnswers.test.ts | 0 .../quizMarkdown/multipleAnswers.test.ts | 0 .../quizMarkdown/multipleChoice.test.ts | 0 .../quizDeterministicChecks.test.ts | 0 .../quizMarkdown/quizMarkdown.test.ts | 0 .../quizMarkdown/testAnswer.test.ts | 0 .../parsingTests}/rubricMarkdown.test.ts | 4 +- .../parsingTests}/testHolidayParsing.test.ts | 2 +- .../parsingTests}/testSemesterImport.test.ts | 6 +- .../local/parsingTests}/timeUtils.test.ts | 5 +- .../local/quizzes/quizFileStorageService.ts | 63 ------ src/features/local/quizzes/quizRouter.ts | 79 +++++-- .../local/utils/fileStorageService.ts | 8 - src/services/tests/fileStorage.test.ts | 10 +- .../tests/fileStorageParsingErrors.test.ts | 53 ++--- 30 files changed, 535 insertions(+), 646 deletions(-) delete mode 100644 src/features/local/assignments/assignmentsFileStorageService.ts delete mode 100644 src/features/local/lectures/lectureFileStorageService.ts delete mode 100644 src/features/local/modules/moduleFileStorageService.ts delete mode 100644 src/features/local/pages/pageFileStorageService.ts rename src/{models/local/tests => features/local/parsingTests}/assignmentMarkdown.test.ts (94%) rename src/{models/local/tests => features/local/parsingTests}/globalSettingsMarkdown.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/pageMarkdown.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/quizMarkdown/matchingAnswerErrors.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/quizMarkdown/matchingAnswers.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/quizMarkdown/multipleAnswers.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/quizMarkdown/multipleChoice.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/quizMarkdown/quizDeterministicChecks.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/quizMarkdown/quizMarkdown.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/quizMarkdown/testAnswer.test.ts (100%) rename src/{models/local/tests => features/local/parsingTests}/rubricMarkdown.test.ts (93%) rename src/{models/local/tests => features/local/parsingTests}/testHolidayParsing.test.ts (91%) rename src/{models/local/tests => features/local/parsingTests}/testSemesterImport.test.ts (96%) rename src/{models/local/tests => features/local/parsingTests}/timeUtils.test.ts (96%) delete mode 100644 src/features/local/quizzes/quizFileStorageService.ts diff --git a/src/app/api/mcp/[transport]/route.ts b/src/app/api/mcp/[transport]/route.ts index abe9b1b..f96d245 100644 --- a/src/app/api/mcp/[transport]/route.ts +++ b/src/app/api/mcp/[transport]/route.ts @@ -1,9 +1,11 @@ import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer"; import { groupByStartDate } from "@/features/local/utils/timeUtils"; -import { fileStorageService } from "@/features/local/utils/fileStorageService"; import { createMcpHandler } from "mcp-handler"; import { z } from "zod"; import { githubClassroomUrlPrompt } from "./github-classroom-prompt"; +import { courseItemFileStorageService } from "@/features/local/course/courseItemFileStorageService"; +import { fileStorageService } from "@/features/local/utils/fileStorageService"; +import { getModuleNamesFromFiles } from "@/features/local/modules/moduleRouter"; const handler = createMcpHandler( (server) => { @@ -41,17 +43,17 @@ const handler = createMcpHandler( courseName: z.string(), }, async ({ courseName }) => { - const modules = await fileStorageService.modules.getModuleNames( + const modules = await getModuleNamesFromFiles( courseName ); const assignments = ( await Promise.all( modules.map(async (moduleName) => { - const assignments = - await fileStorageService.assignments.getAssignments( - courseName, - moduleName - ); + const assignments = await courseItemFileStorageService.getItems({ + courseName, + moduleName, + type: "Assignment", + }); return assignments.map((assignment) => ({ assignmentName: assignment.name, moduleName, @@ -100,11 +102,12 @@ const handler = createMcpHandler( "courseName, moduleName, and assignmentName must be strings" ); } - const assignment = await fileStorageService.assignments.getAssignment( + const assignment = await courseItemFileStorageService.getItem({ courseName, moduleName, - assignmentName - ); + name: assignmentName, + type: "Assignment", + }); console.log("mcp assignment", assignment); return { diff --git a/src/features/local/assignments/assignmentRouter.ts b/src/features/local/assignments/assignmentRouter.ts index b7a86f1..86716d8 100644 --- a/src/features/local/assignments/assignmentRouter.ts +++ b/src/features/local/assignments/assignmentRouter.ts @@ -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); +} diff --git a/src/features/local/assignments/assignmentsFileStorageService.ts b/src/features/local/assignments/assignmentsFileStorageService.ts deleted file mode 100644 index b60d61e..0000000 --- a/src/features/local/assignments/assignmentsFileStorageService.ts +++ /dev/null @@ -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); - }, -}; diff --git a/src/features/local/course/courseItemFileStorageService.ts b/src/features/local/course/courseItemFileStorageService.ts index e8fb670..0cb9d86 100644 --- a/src/features/local/course/courseItemFileStorageService.ts +++ b/src/features/local/course/courseItemFileStorageService.ts @@ -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 ( - courseName: string, - moduleName: string, - name: string, - type: T -): Promise> => { +const getItem = async ({ + courseName, + moduleName, + name, + type, +}: { + courseName: string; + moduleName: string; + name: string; + type: T; +}): Promise> => { 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 ( export const courseItemFileStorageService = { getItem, - getItems: async ( - courseName: string, - moduleName: string, - type: T - ): Promise[]> => { - const fileNames = await getItemFileNames(courseName, moduleName, type); + getItems: async ({ + courseName, + moduleName, + type, + }: { + courseName: string; + moduleName: string; + type: T; + }): Promise[]> => { + 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); - }, }; diff --git a/src/features/local/course/settingsRouter.ts b/src/features/local/course/settingsRouter.ts index 94e8a8a..9bc3f23 100644 --- a/src/features/local/course/settingsRouter.ts +++ b/src/features/local/course/settingsRouter.ts @@ -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, + ]); + }) + ); +} diff --git a/src/features/local/lectures/lectureFileStorageService.ts b/src/features/local/lectures/lectureFileStorageService.ts deleted file mode 100644 index 6da4979..0000000 --- a/src/features/local/lectures/lectureFileStorageService.ts +++ /dev/null @@ -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 => { - try { - const stat = await fs.stat(path); - return stat.isDirectory(); - } catch { - return false; - } -}; diff --git a/src/features/local/lectures/lectureRouter.ts b/src/features/local/lectures/lectureRouter.ts index 11f82c7..cc61c2a 100644 --- a/src/features/local/lectures/lectureRouter.ts +++ b/src/features/local/lectures/lectureRouter.ts @@ -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 => { + try { + const stat = await fs.stat(path); + return stat.isDirectory(); + } catch { + return false; + } +}; diff --git a/src/features/local/modules/moduleFileStorageService.ts b/src/features/local/modules/moduleFileStorageService.ts deleted file mode 100644 index 55543e9..0000000 --- a/src/features/local/modules/moduleFileStorageService.ts +++ /dev/null @@ -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 }); - }, -}; diff --git a/src/features/local/modules/moduleRouter.ts b/src/features/local/modules/moduleRouter.ts index 27eb4a9..4360291 100644 --- a/src/features/local/modules/moduleRouter.ts +++ b/src/features/local/modules/moduleRouter.ts @@ -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)); +} diff --git a/src/features/local/pages/pageFileStorageService.ts b/src/features/local/pages/pageFileStorageService.ts deleted file mode 100644 index 4b2690b..0000000 --- a/src/features/local/pages/pageFileStorageService.ts +++ /dev/null @@ -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); - }, -}; diff --git a/src/features/local/pages/pageRouter.ts b/src/features/local/pages/pageRouter.ts index 06cd85f..0290a01 100644 --- a/src/features/local/pages/pageRouter.ts +++ b/src/features/local/pages/pageRouter.ts @@ -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); +} diff --git a/src/models/local/tests/assignmentMarkdown.test.ts b/src/features/local/parsingTests/assignmentMarkdown.test.ts similarity index 94% rename from src/models/local/tests/assignmentMarkdown.test.ts rename to src/features/local/parsingTests/assignmentMarkdown.test.ts index 024441e..b4df4cc 100644 --- a/src/models/local/tests/assignmentMarkdown.test.ts +++ b/src/features/local/parsingTests/assignmentMarkdown.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from "vitest"; -import { LocalAssignment } from "../../../features/local/assignments/models/localAssignment"; -import { AssignmentSubmissionType } from "../../../features/local/assignments/models/assignmentSubmissionType"; -import { assignmentMarkdownSerializer } from "../../../features/local/assignments/models/utils/assignmentMarkdownSerializer"; -import { assignmentMarkdownParser } from "../../../features/local/assignments/models/utils/assignmentMarkdownParser"; +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", () => { diff --git a/src/models/local/tests/globalSettingsMarkdown.test.ts b/src/features/local/parsingTests/globalSettingsMarkdown.test.ts similarity index 100% rename from src/models/local/tests/globalSettingsMarkdown.test.ts rename to src/features/local/parsingTests/globalSettingsMarkdown.test.ts diff --git a/src/models/local/tests/pageMarkdown.test.ts b/src/features/local/parsingTests/pageMarkdown.test.ts similarity index 100% rename from src/models/local/tests/pageMarkdown.test.ts rename to src/features/local/parsingTests/pageMarkdown.test.ts diff --git a/src/models/local/tests/quizMarkdown/matchingAnswerErrors.test.ts b/src/features/local/parsingTests/quizMarkdown/matchingAnswerErrors.test.ts similarity index 100% rename from src/models/local/tests/quizMarkdown/matchingAnswerErrors.test.ts rename to src/features/local/parsingTests/quizMarkdown/matchingAnswerErrors.test.ts diff --git a/src/models/local/tests/quizMarkdown/matchingAnswers.test.ts b/src/features/local/parsingTests/quizMarkdown/matchingAnswers.test.ts similarity index 100% rename from src/models/local/tests/quizMarkdown/matchingAnswers.test.ts rename to src/features/local/parsingTests/quizMarkdown/matchingAnswers.test.ts diff --git a/src/models/local/tests/quizMarkdown/multipleAnswers.test.ts b/src/features/local/parsingTests/quizMarkdown/multipleAnswers.test.ts similarity index 100% rename from src/models/local/tests/quizMarkdown/multipleAnswers.test.ts rename to src/features/local/parsingTests/quizMarkdown/multipleAnswers.test.ts diff --git a/src/models/local/tests/quizMarkdown/multipleChoice.test.ts b/src/features/local/parsingTests/quizMarkdown/multipleChoice.test.ts similarity index 100% rename from src/models/local/tests/quizMarkdown/multipleChoice.test.ts rename to src/features/local/parsingTests/quizMarkdown/multipleChoice.test.ts diff --git a/src/models/local/tests/quizMarkdown/quizDeterministicChecks.test.ts b/src/features/local/parsingTests/quizMarkdown/quizDeterministicChecks.test.ts similarity index 100% rename from src/models/local/tests/quizMarkdown/quizDeterministicChecks.test.ts rename to src/features/local/parsingTests/quizMarkdown/quizDeterministicChecks.test.ts diff --git a/src/models/local/tests/quizMarkdown/quizMarkdown.test.ts b/src/features/local/parsingTests/quizMarkdown/quizMarkdown.test.ts similarity index 100% rename from src/models/local/tests/quizMarkdown/quizMarkdown.test.ts rename to src/features/local/parsingTests/quizMarkdown/quizMarkdown.test.ts diff --git a/src/models/local/tests/quizMarkdown/testAnswer.test.ts b/src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts similarity index 100% rename from src/models/local/tests/quizMarkdown/testAnswer.test.ts rename to src/features/local/parsingTests/quizMarkdown/testAnswer.test.ts diff --git a/src/models/local/tests/rubricMarkdown.test.ts b/src/features/local/parsingTests/rubricMarkdown.test.ts similarity index 93% rename from src/models/local/tests/rubricMarkdown.test.ts rename to src/features/local/parsingTests/rubricMarkdown.test.ts index 124778f..1fbbc0a 100644 --- a/src/models/local/tests/rubricMarkdown.test.ts +++ b/src/features/local/parsingTests/rubricMarkdown.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from "vitest"; import { RubricItem, rubricItemIsExtraCredit, -} from "../../../features/local/assignments/models/rubricItem"; -import { assignmentMarkdownParser } from "../../../features/local/assignments/models/utils/assignmentMarkdownParser"; +} from "../assignments/models/rubricItem"; +import { assignmentMarkdownParser } from "../assignments/models/utils/assignmentMarkdownParser"; describe("RubricMarkdownTests", () => { it("can parse one item", () => { diff --git a/src/models/local/tests/testHolidayParsing.test.ts b/src/features/local/parsingTests/testHolidayParsing.test.ts similarity index 91% rename from src/models/local/tests/testHolidayParsing.test.ts rename to src/features/local/parsingTests/testHolidayParsing.test.ts index 6c7126b..9d2bafe 100644 --- a/src/models/local/tests/testHolidayParsing.test.ts +++ b/src/features/local/parsingTests/testHolidayParsing.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { parseHolidays } from "../../../features/local/utils/settingsUtils"; +import { parseHolidays } from "../utils/settingsUtils"; describe("can parse holiday string", () => { it("can parse empty list", () => { diff --git a/src/models/local/tests/testSemesterImport.test.ts b/src/features/local/parsingTests/testSemesterImport.test.ts similarity index 96% rename from src/models/local/tests/testSemesterImport.test.ts rename to src/features/local/parsingTests/testSemesterImport.test.ts index bf367e1..101c251 100644 --- a/src/models/local/tests/testSemesterImport.test.ts +++ b/src/features/local/parsingTests/testSemesterImport.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect } from "vitest"; -import { LocalAssignment } from "../../../features/local/assignments/models/localAssignment"; +import { LocalAssignment } from "../assignments/models/localAssignment"; import { prepAssignmentForNewSemester, prepLectureForNewSemester, prepPageForNewSemester, prepQuizForNewSemester, -} from "../../../features/local/utils/semesterTransferUtils"; -import { Lecture } from "../../../features/local/lectures/lectureModel"; +} from "../utils/semesterTransferUtils"; +import { Lecture } from "../lectures/lectureModel"; import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels"; import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz"; diff --git a/src/models/local/tests/timeUtils.test.ts b/src/features/local/parsingTests/timeUtils.test.ts similarity index 96% rename from src/models/local/tests/timeUtils.test.ts rename to src/features/local/parsingTests/timeUtils.test.ts index 4fca93c..218616b 100644 --- a/src/models/local/tests/timeUtils.test.ts +++ b/src/features/local/parsingTests/timeUtils.test.ts @@ -1,8 +1,5 @@ import { describe, it, expect } from "vitest"; -import { - dateToMarkdownString, - getDateFromString, -} from "../../../features/local/utils/timeUtils"; +import { dateToMarkdownString, getDateFromString } from "../utils/timeUtils"; describe("Can properly handle expected date formats", () => { it("can use AM/PM dates", () => { diff --git a/src/features/local/quizzes/quizFileStorageService.ts b/src/features/local/quizzes/quizFileStorageService.ts deleted file mode 100644 index 6189e71..0000000 --- a/src/features/local/quizzes/quizFileStorageService.ts +++ /dev/null @@ -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); - }, -}; diff --git a/src/features/local/quizzes/quizRouter.ts b/src/features/local/quizzes/quizRouter.ts index da3ed3a..0c8d956 100644 --- a/src/features/local/quizzes/quizRouter.ts +++ b/src/features/local/quizzes/quizRouter.ts @@ -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); +} diff --git a/src/features/local/utils/fileStorageService.ts b/src/features/local/utils/fileStorageService.ts index 0a26462..4ede0eb 100644 --- a/src/features/local/utils/fileStorageService.ts +++ b/src/features/local/utils/fileStorageService.ts @@ -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 { if (!(await directoryOrFileExists(basePath))) { diff --git a/src/services/tests/fileStorage.test.ts b/src/services/tests/fileStorage.test.ts index 92331d4..439f901 100644 --- a/src/services/tests/fileStorage.test.ts +++ b/src/services/tests/fileStorage.test.ts @@ -5,6 +5,10 @@ import { LocalCourseSettings, DayOfWeek, } from "@/features/local/course/localCourseSettings"; +import { + createModuleFile, + getModuleNamesFromFiles, +} from "@/features/local/modules/moduleRouter"; describe("FileStorageTests", () => { beforeEach(async () => { @@ -51,11 +55,9 @@ describe("FileStorageTests", () => { const courseName = "test empty course"; const moduleName = "test module 1"; - await fileStorageService.modules.createModule(courseName, moduleName); + await createModuleFile(courseName, moduleName); - const moduleNames = await fileStorageService.modules.getModuleNames( - courseName - ); + const moduleNames = await getModuleNamesFromFiles(courseName); expect(moduleNames).toContain(moduleName); }); diff --git a/src/services/tests/fileStorageParsingErrors.test.ts b/src/services/tests/fileStorageParsingErrors.test.ts index 2f3e443..f11122a 100644 --- a/src/services/tests/fileStorageParsingErrors.test.ts +++ b/src/services/tests/fileStorageParsingErrors.test.ts @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach } from "vitest"; import { promises as fs } from "fs"; import { fileStorageService } from "../../features/local/utils/fileStorageService"; import { basePath } from "../../features/local/utils/fileSystemUtils"; +import { courseItemFileStorageService } from "@/features/local/course/courseItemFileStorageService"; +import { createModuleFile } from "@/features/local/modules/moduleRouter"; describe("FileStorageTests", () => { beforeEach(async () => { @@ -40,7 +42,7 @@ a) truthy `; const invalidQuizMarkdown = "name: testQuiz\n---\nnot a quiz"; await fileStorageService.createCourseFolderForTesting(courseName); - await fileStorageService.modules.createModule(courseName, moduleName); + await createModuleFile(courseName, moduleName); await fs.mkdir(`${basePath}/${courseName}/${moduleName}/quizzes`, { recursive: true, @@ -54,40 +56,17 @@ a) truthy validQuizMarkdown ); - const quizzes = await fileStorageService.quizzes.getQuizzes( + const quizzes = await courseItemFileStorageService.getItems({ courseName, - moduleName - ); + moduleName, + type: "Quiz", + }); const quizNames = quizzes.map((q) => q.name); expect(quizNames).not.includes("testQuiz"); expect(quizNames).include("validQuiz"); }); - // it("invalid quizes give error messages", async () => { - // const courseName = "testCourse"; - // const moduleName = "testModule"; - // const invalidQuizMarkdown = "name: testQuiz\n---\nnot a quiz"; - // await fileStorageService.createCourseFolderForTesting(courseName); - // await fileStorageService.modules.createModule(courseName, moduleName); - - // await fs.mkdir(`${basePath}/${courseName}/${moduleName}/quizzes`, { - // recursive: true, - // }); - // await fs.writeFile( - // `${basePath}/${courseName}/${moduleName}/quizzes/testQuiz.md`, - // invalidQuizMarkdown - // ); - - // const invalidReasons = await fileStorageService.quizzes.getInvalidQuizzes( - // courseName, - // moduleName - // ); - // const invalidQuiz = invalidReasons.filter((q) => q.quizName === "testQuiz"); - - // expect(invalidQuiz.reason).is("testQuiz"); - // }); - it("invalid assignments dont get loaded", async () => { const courseName = "testCourse"; const moduleName = "testModule"; @@ -107,7 +86,7 @@ this is the test description `; const invalidAssignment = "name: invalidAssignment\n---\nnot an assignment"; await fileStorageService.createCourseFolderForTesting(courseName); - await fileStorageService.modules.createModule(courseName, moduleName); + await createModuleFile(courseName, moduleName); await fs.mkdir(`${basePath}/${courseName}/${moduleName}/assignments`, { recursive: true, @@ -121,10 +100,11 @@ this is the test description invalidAssignment ); - const assignments = await fileStorageService.assignments.getAssignments( + const assignments = await courseItemFileStorageService.getItems({ courseName, - moduleName - ); + moduleName, + type: "Assignment", + }); const assignmentNames = assignments.map((q) => q.name); expect(assignmentNames).not.includes("invalidAssignment"); @@ -144,7 +124,7 @@ DueDateFo59:00 --- # Deploying React`; await fileStorageService.createCourseFolderForTesting(courseName); - await fileStorageService.modules.createModule(courseName, moduleName); + await createModuleFile(courseName, moduleName); await fs.mkdir(`${basePath}/${courseName}/${moduleName}/pages`, { recursive: true, @@ -158,10 +138,11 @@ DueDateFo59:00 invalidPageMarkdown ); - const pages = await fileStorageService.pages.getPages( + const pages = await courseItemFileStorageService.getItems({ courseName, - moduleName - ); + moduleName, + type: "Page", + }); const assignmentNames = pages.map((q) => q.name); expect(assignmentNames).include("validPage");