more code refactor to colocate feature code

This commit is contained in:
2025-07-23 11:25:12 -06:00
parent c37ad0708e
commit 815f929c2d
30 changed files with 535 additions and 646 deletions

View File

@@ -1,9 +1,11 @@
import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer"; import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer";
import { groupByStartDate } from "@/features/local/utils/timeUtils"; import { groupByStartDate } from "@/features/local/utils/timeUtils";
import { fileStorageService } from "@/features/local/utils/fileStorageService";
import { createMcpHandler } from "mcp-handler"; import { createMcpHandler } from "mcp-handler";
import { z } from "zod"; import { z } from "zod";
import { githubClassroomUrlPrompt } from "./github-classroom-prompt"; 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( const handler = createMcpHandler(
(server) => { (server) => {
@@ -41,17 +43,17 @@ const handler = createMcpHandler(
courseName: z.string(), courseName: z.string(),
}, },
async ({ courseName }) => { async ({ courseName }) => {
const modules = await fileStorageService.modules.getModuleNames( const modules = await getModuleNamesFromFiles(
courseName courseName
); );
const assignments = ( const assignments = (
await Promise.all( await Promise.all(
modules.map(async (moduleName) => { modules.map(async (moduleName) => {
const assignments = const assignments = await courseItemFileStorageService.getItems({
await fileStorageService.assignments.getAssignments( courseName,
courseName, moduleName,
moduleName type: "Assignment",
); });
return assignments.map((assignment) => ({ return assignments.map((assignment) => ({
assignmentName: assignment.name, assignmentName: assignment.name,
moduleName, moduleName,
@@ -100,11 +102,12 @@ const handler = createMcpHandler(
"courseName, moduleName, and assignmentName must be strings" "courseName, moduleName, and assignmentName must be strings"
); );
} }
const assignment = await fileStorageService.assignments.getAssignment( const assignment = await courseItemFileStorageService.getItem({
courseName, courseName,
moduleName, moduleName,
assignmentName name: assignmentName,
); type: "Assignment",
});
console.log("mcp assignment", assignment); console.log("mcp assignment", assignment);
return { return {

View File

@@ -1,8 +1,15 @@
import publicProcedure from "../../../services/serverFunctions/publicProcedure"; import publicProcedure from "../../../services/serverFunctions/publicProcedure";
import { z } from "zod"; import { z } from "zod";
import { router } from "../../../services/serverFunctions/trpcSetup"; import { router } from "../../../services/serverFunctions/trpcSetup";
import { fileStorageService } from "@/features/local/utils/fileStorageService"; import {
import { zodLocalAssignment } from "@/features/local/assignments/models/localAssignment"; 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({ export const assignmentRouter = router({
getAssignment: publicProcedure getAssignment: publicProcedure
@@ -14,13 +21,12 @@ export const assignmentRouter = router({
}) })
) )
.query(async ({ input: { courseName, moduleName, assignmentName } }) => { .query(async ({ input: { courseName, moduleName, assignmentName } }) => {
const assignment = await fileStorageService.assignments.getAssignment( return await courseItemFileStorageService.getItem({
courseName, courseName,
moduleName, moduleName,
assignmentName name: assignmentName,
); type: "Assignment",
// console.log(assignment); });
return assignment;
}), }),
getAllAssignments: publicProcedure getAllAssignments: publicProcedure
.input( .input(
@@ -30,10 +36,11 @@ export const assignmentRouter = router({
}) })
) )
.query(async ({ input: { courseName, moduleName } }) => { .query(async ({ input: { courseName, moduleName } }) => {
const assignments = await fileStorageService.assignments.getAssignments( const assignments = await courseItemFileStorageService.getItems({
courseName, courseName,
moduleName moduleName,
); type: "Assignment",
});
return assignments; return assignments;
}), }),
createAssignment: publicProcedure createAssignment: publicProcedure
@@ -49,7 +56,7 @@ export const assignmentRouter = router({
async ({ async ({
input: { courseName, moduleName, assignmentName, assignment }, input: { courseName, moduleName, assignmentName, assignment },
}) => { }) => {
await fileStorageService.assignments.updateOrCreateAssignment({ await updateOrCreateAssignmentFile({
courseName, courseName,
moduleName, moduleName,
assignmentName, assignmentName,
@@ -79,7 +86,7 @@ export const assignmentRouter = router({
previousAssignmentName, previousAssignmentName,
}, },
}) => { }) => {
await fileStorageService.assignments.updateOrCreateAssignment({ await updateOrCreateAssignmentFile({
courseName, courseName,
moduleName, moduleName,
assignmentName, assignmentName,
@@ -90,7 +97,7 @@ export const assignmentRouter = router({
assignmentName !== previousAssignmentName || assignmentName !== previousAssignmentName ||
moduleName !== previousModuleName moduleName !== previousModuleName
) { ) {
await fileStorageService.assignments.delete({ await deleteAssignment({
courseName, courseName,
moduleName: previousModuleName, moduleName: previousModuleName,
assignmentName: previousAssignmentName, assignmentName: previousAssignmentName,
@@ -107,10 +114,59 @@ export const assignmentRouter = router({
}) })
) )
.mutation(async ({ input: { courseName, moduleName, assignmentName } }) => { .mutation(async ({ input: { courseName, moduleName, assignmentName } }) => {
await fileStorageService.assignments.delete({ await deleteAssignment({
courseName, courseName,
moduleName, moduleName,
assignmentName, 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);
}

View File

@@ -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);
},
};

View File

@@ -2,10 +2,8 @@ import path from "path";
import { directoryOrFileExists } from "../utils/fileSystemUtils"; import { directoryOrFileExists } from "../utils/fileSystemUtils";
import fs from "fs/promises"; import fs from "fs/promises";
import { import {
LocalAssignment,
localAssignmentMarkdown, localAssignmentMarkdown,
} from "@/features/local/assignments/models/localAssignment"; } from "@/features/local/assignments/models/localAssignment";
import { assignmentMarkdownSerializer } from "@/features/local/assignments/models/utils/assignmentMarkdownSerializer";
import { import {
CourseItemReturnType, CourseItemReturnType,
CourseItemType, CourseItemType,
@@ -14,19 +12,20 @@ import {
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService"; import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
import { import {
localPageMarkdownUtils, localPageMarkdownUtils,
LocalCoursePage,
} from "@/features/local/pages/localCoursePageModels"; } from "@/features/local/pages/localCoursePageModels";
import { import {
LocalQuiz,
localQuizMarkdownUtils, localQuizMarkdownUtils,
} from "@/features/local/quizzes/models/localQuiz"; } from "@/features/local/quizzes/models/localQuiz";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
const getItemFileNames = async ( const getItemFileNames = async ({
courseName: string, courseName,
moduleName: string, moduleName,
type: CourseItemType type,
) => { }: {
courseName: string;
moduleName: string;
type: CourseItemType;
}) => {
const courseDirectory = await getCoursePathByName(courseName); const courseDirectory = await getCoursePathByName(courseName);
const folder = typeToFolder[type]; const folder = typeToFolder[type];
const filePath = path.join(courseDirectory, moduleName, folder); const filePath = path.join(courseDirectory, moduleName, folder);
@@ -41,12 +40,17 @@ const getItemFileNames = async (
return itemFiles.map((f) => f.replace(/\.md$/, "")); return itemFiles.map((f) => f.replace(/\.md$/, ""));
}; };
const getItem = async <T extends CourseItemType>( const getItem = async <T extends CourseItemType>({
courseName: string, courseName,
moduleName: string, moduleName,
name: string, name,
type: T type,
): Promise<CourseItemReturnType<T>> => { }: {
courseName: string;
moduleName: string;
name: string;
type: T;
}): Promise<CourseItemReturnType<T>> => {
const courseDirectory = await getCoursePathByName(courseName); const courseDirectory = await getCoursePathByName(courseName);
const folder = typeToFolder[type]; const folder = typeToFolder[type];
const filePath = path.join(courseDirectory, moduleName, folder, name + ".md"); const filePath = path.join(courseDirectory, moduleName, folder, name + ".md");
@@ -73,17 +77,21 @@ const getItem = async <T extends CourseItemType>(
export const courseItemFileStorageService = { export const courseItemFileStorageService = {
getItem, getItem,
getItems: async <T extends CourseItemType>( getItems: async <T extends CourseItemType>({
courseName: string, courseName,
moduleName: string, moduleName,
type: T type,
): Promise<CourseItemReturnType<T>[]> => { }: {
const fileNames = await getItemFileNames(courseName, moduleName, type); courseName: string;
moduleName: string;
type: T;
}): Promise<CourseItemReturnType<T>[]> => {
const fileNames = await getItemFileNames({ courseName, moduleName, type });
const items = ( const items = (
await Promise.all( await Promise.all(
fileNames.map(async (name) => { fileNames.map(async (name) => {
try { try {
const item = await getItem(courseName, moduleName, name, type); const item = await getItem({ courseName, moduleName, name, type });
return item; return item;
} catch { } catch {
return null; return null;
@@ -93,42 +101,4 @@ export const courseItemFileStorageService = {
).filter((a) => a !== null); ).filter((a) => a !== null);
return items; 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);
},
}; };

View File

@@ -13,10 +13,18 @@ import {
updateGlobalSettings, updateGlobalSettings,
} from "@/features/local/globalSettings/globalSettingsFileStorageService"; } from "@/features/local/globalSettings/globalSettingsFileStorageService";
import { import {
getLectures, LocalCourseSettings,
updateLecture, zodLocalCourseSettings,
} from "@/features/local/lectures/lectureFileStorageService"; } from "@/features/local/course/localCourseSettings";
import { 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({ export const settingsRouter = router({
allCoursesSettings: publicProcedure.query(async () => { allCoursesSettings: publicProcedure.query(async () => {
@@ -71,90 +79,7 @@ export const settingsRouter = router({
}); });
if (settingsFromCourseToImport) { if (settingsFromCourseToImport) {
const oldCourseName = settingsFromCourseToImport.name; await migrateCourseContent(settingsFromCourseToImport, settings);
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);
})
),
]);
})
);
} }
} }
), ),
@@ -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,
]);
})
);
}

View File

@@ -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;
}
};

View File

@@ -1,13 +1,22 @@
import { z } from "zod"; import { z } from "zod";
import publicProcedure from "../../../services/serverFunctions/publicProcedure"; import publicProcedure from "../../../services/serverFunctions/publicProcedure";
import { router } from "../../../services/serverFunctions/trpcSetup"; import { router } from "../../../services/serverFunctions/trpcSetup";
import { zodLecture } from "@/features/local/lectures/lectureModel"; import { Lecture, zodLecture } from "@/features/local/lectures/lectureModel";
import { import {
getLectures, getDayOfWeek,
updateLecture, LocalCourseSettings,
deleteLecture, zodLocalCourseSettings,
} from "./lectureFileStorageService"; } from "../course/localCourseSettings";
import { 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({ export const lectureRouter = router({
getLectures: publicProcedure getLectures: publicProcedure
@@ -49,3 +58,112 @@ export const lectureRouter = router({
await deleteLecture(courseName, settings, lectureDay); 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;
}
};

View File

@@ -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 });
},
};

View File

@@ -1,7 +1,9 @@
import { z } from "zod"; import { z } from "zod";
import { fileStorageService } from "@/features/local/utils/fileStorageService";
import { router } from "@/services/serverFunctions/trpcSetup"; import { router } from "@/services/serverFunctions/trpcSetup";
import publicProcedure from "@/services/serverFunctions/publicProcedure"; 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({ export const moduleRouter = router({
getModuleNames: publicProcedure getModuleNames: publicProcedure
@@ -11,7 +13,7 @@ export const moduleRouter = router({
}) })
) )
.query(async ({ input: { courseName } }) => { .query(async ({ input: { courseName } }) => {
return await fileStorageService.modules.getModuleNames(courseName); return await getModuleNamesFromFiles(courseName);
}), }),
createModule: publicProcedure createModule: publicProcedure
.input( .input(
@@ -21,6 +23,27 @@ export const moduleRouter = router({
}) })
) )
.mutation(async ({ input: { courseName, moduleName } }) => { .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));
}

View File

@@ -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);
},
};

View File

@@ -1,8 +1,11 @@
import publicProcedure from "../../../services/serverFunctions/publicProcedure"; import publicProcedure from "../../../services/serverFunctions/publicProcedure";
import { z } from "zod"; import { z } from "zod";
import { router } from "../../../services/serverFunctions/trpcSetup"; import { router } from "../../../services/serverFunctions/trpcSetup";
import { fileStorageService } from "@/features/local/utils/fileStorageService"; import { LocalCoursePage, localPageMarkdownUtils, zodLocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { 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({ export const pageRouter = router({
getPage: publicProcedure getPage: publicProcedure
@@ -14,11 +17,12 @@ export const pageRouter = router({
}) })
) )
.query(async ({ input: { courseName, moduleName, pageName } }) => { .query(async ({ input: { courseName, moduleName, pageName } }) => {
return await fileStorageService.pages.getPage( return await courseItemFileStorageService.getItem({
courseName, courseName,
moduleName, moduleName,
pageName name: pageName,
); type: "Page",
});
}), }),
getAllPages: publicProcedure getAllPages: publicProcedure
@@ -29,7 +33,11 @@ export const pageRouter = router({
}) })
) )
.query(async ({ input: { courseName, moduleName } }) => { .query(async ({ input: { courseName, moduleName } }) => {
return await fileStorageService.pages.getPages(courseName, moduleName); return await courseItemFileStorageService.getItems({
courseName,
moduleName,
type: "Page",
});
}), }),
createPage: publicProcedure createPage: publicProcedure
.input( .input(
@@ -41,7 +49,7 @@ export const pageRouter = router({
}) })
) )
.mutation(async ({ input: { courseName, moduleName, pageName, page } }) => { .mutation(async ({ input: { courseName, moduleName, pageName, page } }) => {
await fileStorageService.pages.updatePage({ await updatePageFile({
courseName, courseName,
moduleName, moduleName,
pageName, pageName,
@@ -70,7 +78,7 @@ export const pageRouter = router({
previousPageName, previousPageName,
}, },
}) => { }) => {
await fileStorageService.pages.updatePage({ await updatePageFile({
courseName, courseName,
moduleName, moduleName,
pageName, pageName,
@@ -81,7 +89,7 @@ export const pageRouter = router({
pageName !== previousPageName || pageName !== previousPageName ||
moduleName !== previousModuleName moduleName !== previousModuleName
) { ) {
await fileStorageService.pages.delete({ await deletePageFile({
courseName, courseName,
moduleName: previousModuleName, moduleName: previousModuleName,
pageName: previousPageName, pageName: previousPageName,
@@ -98,10 +106,56 @@ export const pageRouter = router({
}) })
) )
.mutation(async ({ input: { courseName, moduleName, pageName } }) => { .mutation(async ({ input: { courseName, moduleName, pageName } }) => {
await fileStorageService.pages.delete({ await deletePageFile({
courseName, courseName,
moduleName, moduleName,
pageName, 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);
}

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { LocalAssignment } from "../../../features/local/assignments/models/localAssignment"; import { LocalAssignment } from "../assignments/models/localAssignment";
import { AssignmentSubmissionType } from "../../../features/local/assignments/models/assignmentSubmissionType"; import { AssignmentSubmissionType } from "../assignments/models/assignmentSubmissionType";
import { assignmentMarkdownSerializer } from "../../../features/local/assignments/models/utils/assignmentMarkdownSerializer"; import { assignmentMarkdownSerializer } from "../assignments/models/utils/assignmentMarkdownSerializer";
import { assignmentMarkdownParser } from "../../../features/local/assignments/models/utils/assignmentMarkdownParser"; import { assignmentMarkdownParser } from "../assignments/models/utils/assignmentMarkdownParser";
describe("AssignmentMarkdownTests", () => { describe("AssignmentMarkdownTests", () => {
it("can parse assignment settings", () => { it("can parse assignment settings", () => {

View File

@@ -2,8 +2,8 @@ import { describe, it, expect } from "vitest";
import { import {
RubricItem, RubricItem,
rubricItemIsExtraCredit, rubricItemIsExtraCredit,
} from "../../../features/local/assignments/models/rubricItem"; } from "../assignments/models/rubricItem";
import { assignmentMarkdownParser } from "../../../features/local/assignments/models/utils/assignmentMarkdownParser"; import { assignmentMarkdownParser } from "../assignments/models/utils/assignmentMarkdownParser";
describe("RubricMarkdownTests", () => { describe("RubricMarkdownTests", () => {
it("can parse one item", () => { it("can parse one item", () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { parseHolidays } from "../../../features/local/utils/settingsUtils"; import { parseHolidays } from "../utils/settingsUtils";
describe("can parse holiday string", () => { describe("can parse holiday string", () => {
it("can parse empty list", () => { it("can parse empty list", () => {

View File

@@ -1,12 +1,12 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { LocalAssignment } from "../../../features/local/assignments/models/localAssignment"; import { LocalAssignment } from "../assignments/models/localAssignment";
import { import {
prepAssignmentForNewSemester, prepAssignmentForNewSemester,
prepLectureForNewSemester, prepLectureForNewSemester,
prepPageForNewSemester, prepPageForNewSemester,
prepQuizForNewSemester, prepQuizForNewSemester,
} from "../../../features/local/utils/semesterTransferUtils"; } from "../utils/semesterTransferUtils";
import { Lecture } from "../../../features/local/lectures/lectureModel"; import { Lecture } from "../lectures/lectureModel";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels"; import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz"; import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";

View File

@@ -1,8 +1,5 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { import { dateToMarkdownString, getDateFromString } from "../utils/timeUtils";
dateToMarkdownString,
getDateFromString,
} from "../../../features/local/utils/timeUtils";
describe("Can properly handle expected date formats", () => { describe("Can properly handle expected date formats", () => {
it("can use AM/PM dates", () => { it("can use AM/PM dates", () => {

View File

@@ -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);
},
};

View File

@@ -1,8 +1,15 @@
import publicProcedure from "../../../services/serverFunctions/publicProcedure"; import publicProcedure from "../../../services/serverFunctions/publicProcedure";
import { z } from "zod"; import { z } from "zod";
import { router } from "../../../services/serverFunctions/trpcSetup"; import { router } from "../../../services/serverFunctions/trpcSetup";
import { fileStorageService } from "@/features/local/utils/fileStorageService"; import {
import { zodLocalQuiz } from "@/features/local/quizzes/models/localQuiz"; 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({ export const quizRouter = router({
getQuiz: publicProcedure getQuiz: publicProcedure
@@ -14,11 +21,12 @@ export const quizRouter = router({
}) })
) )
.query(async ({ input: { courseName, moduleName, quizName } }) => { .query(async ({ input: { courseName, moduleName, quizName } }) => {
return await fileStorageService.quizzes.getQuiz( return await courseItemFileStorageService.getItem({
courseName, courseName,
moduleName, moduleName,
quizName name: quizName,
); type: "Quiz",
});
}), }),
getAllQuizzes: publicProcedure getAllQuizzes: publicProcedure
@@ -29,10 +37,11 @@ export const quizRouter = router({
}) })
) )
.query(async ({ input: { courseName, moduleName } }) => { .query(async ({ input: { courseName, moduleName } }) => {
return await fileStorageService.quizzes.getQuizzes( return await courseItemFileStorageService.getItems({
courseName, courseName,
moduleName moduleName,
); type: "Quiz",
});
}), }),
createQuiz: publicProcedure createQuiz: publicProcedure
.input( .input(
@@ -44,7 +53,7 @@ export const quizRouter = router({
}) })
) )
.mutation(async ({ input: { courseName, moduleName, quizName, quiz } }) => { .mutation(async ({ input: { courseName, moduleName, quizName, quiz } }) => {
await fileStorageService.quizzes.updateQuiz({ await updateQuizFile({
courseName, courseName,
moduleName, moduleName,
quizName, quizName,
@@ -73,7 +82,7 @@ export const quizRouter = router({
previousQuizName, previousQuizName,
}, },
}) => { }) => {
await fileStorageService.quizzes.updateQuiz({ await updateQuizFile({
courseName, courseName,
moduleName, moduleName,
quizName, quizName,
@@ -84,7 +93,7 @@ export const quizRouter = router({
quizName !== previousQuizName || quizName !== previousQuizName ||
moduleName !== previousModuleName moduleName !== previousModuleName
) { ) {
await fileStorageService.quizzes.delete({ await deleteQuizFile({
courseName, courseName,
moduleName: previousModuleName, moduleName: previousModuleName,
quizName: previousQuizName, quizName: previousQuizName,
@@ -101,10 +110,56 @@ export const quizRouter = router({
}) })
) )
.mutation(async ({ input: { courseName, moduleName, quizName } }) => { .mutation(async ({ input: { courseName, moduleName, quizName } }) => {
await fileStorageService.quizzes.delete({ await deleteQuizFile({
courseName, courseName,
moduleName, moduleName,
quizName, 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);
}

View File

@@ -1,19 +1,11 @@
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import path from "path"; import path from "path";
import { basePath, directoryOrFileExists } from "./fileSystemUtils"; 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 { settingsFileStorageService } from "../course/settingsFileStorageService";
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService"; import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
import { assignmentsFileStorageService } from "@/features/local/assignments/assignmentsFileStorageService";
export const fileStorageService = { export const fileStorageService = {
settings: settingsFileStorageService, settings: settingsFileStorageService,
modules: moduleFileStorageService,
assignments: assignmentsFileStorageService,
quizzes: quizFileStorageService,
pages: pageFileStorageService,
async getEmptyDirectories(): Promise<string[]> { async getEmptyDirectories(): Promise<string[]> {
if (!(await directoryOrFileExists(basePath))) { if (!(await directoryOrFileExists(basePath))) {

View File

@@ -5,6 +5,10 @@ import {
LocalCourseSettings, LocalCourseSettings,
DayOfWeek, DayOfWeek,
} from "@/features/local/course/localCourseSettings"; } from "@/features/local/course/localCourseSettings";
import {
createModuleFile,
getModuleNamesFromFiles,
} from "@/features/local/modules/moduleRouter";
describe("FileStorageTests", () => { describe("FileStorageTests", () => {
beforeEach(async () => { beforeEach(async () => {
@@ -51,11 +55,9 @@ describe("FileStorageTests", () => {
const courseName = "test empty course"; const courseName = "test empty course";
const moduleName = "test module 1"; const moduleName = "test module 1";
await fileStorageService.modules.createModule(courseName, moduleName); await createModuleFile(courseName, moduleName);
const moduleNames = await fileStorageService.modules.getModuleNames( const moduleNames = await getModuleNamesFromFiles(courseName);
courseName
);
expect(moduleNames).toContain(moduleName); expect(moduleNames).toContain(moduleName);
}); });

View File

@@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach } from "vitest";
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import { fileStorageService } from "../../features/local/utils/fileStorageService"; import { fileStorageService } from "../../features/local/utils/fileStorageService";
import { basePath } from "../../features/local/utils/fileSystemUtils"; import { basePath } from "../../features/local/utils/fileSystemUtils";
import { courseItemFileStorageService } from "@/features/local/course/courseItemFileStorageService";
import { createModuleFile } from "@/features/local/modules/moduleRouter";
describe("FileStorageTests", () => { describe("FileStorageTests", () => {
beforeEach(async () => { beforeEach(async () => {
@@ -40,7 +42,7 @@ a) truthy
`; `;
const invalidQuizMarkdown = "name: testQuiz\n---\nnot a quiz"; const invalidQuizMarkdown = "name: testQuiz\n---\nnot a quiz";
await fileStorageService.createCourseFolderForTesting(courseName); await fileStorageService.createCourseFolderForTesting(courseName);
await fileStorageService.modules.createModule(courseName, moduleName); await createModuleFile(courseName, moduleName);
await fs.mkdir(`${basePath}/${courseName}/${moduleName}/quizzes`, { await fs.mkdir(`${basePath}/${courseName}/${moduleName}/quizzes`, {
recursive: true, recursive: true,
@@ -54,40 +56,17 @@ a) truthy
validQuizMarkdown validQuizMarkdown
); );
const quizzes = await fileStorageService.quizzes.getQuizzes( const quizzes = await courseItemFileStorageService.getItems({
courseName, courseName,
moduleName moduleName,
); type: "Quiz",
});
const quizNames = quizzes.map((q) => q.name); const quizNames = quizzes.map((q) => q.name);
expect(quizNames).not.includes("testQuiz"); expect(quizNames).not.includes("testQuiz");
expect(quizNames).include("validQuiz"); 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 () => { it("invalid assignments dont get loaded", async () => {
const courseName = "testCourse"; const courseName = "testCourse";
const moduleName = "testModule"; const moduleName = "testModule";
@@ -107,7 +86,7 @@ this is the test description
`; `;
const invalidAssignment = "name: invalidAssignment\n---\nnot an assignment"; const invalidAssignment = "name: invalidAssignment\n---\nnot an assignment";
await fileStorageService.createCourseFolderForTesting(courseName); await fileStorageService.createCourseFolderForTesting(courseName);
await fileStorageService.modules.createModule(courseName, moduleName); await createModuleFile(courseName, moduleName);
await fs.mkdir(`${basePath}/${courseName}/${moduleName}/assignments`, { await fs.mkdir(`${basePath}/${courseName}/${moduleName}/assignments`, {
recursive: true, recursive: true,
@@ -121,10 +100,11 @@ this is the test description
invalidAssignment invalidAssignment
); );
const assignments = await fileStorageService.assignments.getAssignments( const assignments = await courseItemFileStorageService.getItems({
courseName, courseName,
moduleName moduleName,
); type: "Assignment",
});
const assignmentNames = assignments.map((q) => q.name); const assignmentNames = assignments.map((q) => q.name);
expect(assignmentNames).not.includes("invalidAssignment"); expect(assignmentNames).not.includes("invalidAssignment");
@@ -144,7 +124,7 @@ DueDateFo59:00
--- ---
# Deploying React`; # Deploying React`;
await fileStorageService.createCourseFolderForTesting(courseName); await fileStorageService.createCourseFolderForTesting(courseName);
await fileStorageService.modules.createModule(courseName, moduleName); await createModuleFile(courseName, moduleName);
await fs.mkdir(`${basePath}/${courseName}/${moduleName}/pages`, { await fs.mkdir(`${basePath}/${courseName}/${moduleName}/pages`, {
recursive: true, recursive: true,
@@ -158,10 +138,11 @@ DueDateFo59:00
invalidPageMarkdown invalidPageMarkdown
); );
const pages = await fileStorageService.pages.getPages( const pages = await courseItemFileStorageService.getItems({
courseName, courseName,
moduleName moduleName,
); type: "Page",
});
const assignmentNames = pages.map((q) => q.name); const assignmentNames = pages.map((q) => q.name);
expect(assignmentNames).include("validPage"); expect(assignmentNames).include("validPage");