diff --git a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/route.ts b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/route.ts index 0fad941..aa361a9 100644 --- a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/route.ts +++ b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/route.ts @@ -8,25 +8,10 @@ export const GET = async ( }: { params: { courseName: string; moduleName: string } } ) => await withErrorHandling(async () => { - const names = await fileStorageService.assignments.getAssignmentNames( + const assignments = await fileStorageService.assignments.getAssignments( courseName, moduleName ); - const assignments = ( - await Promise.all( - names.map(async (name) => { - try { - return await fileStorageService.assignments.getAssignment( - courseName, - moduleName, - name - ); - } catch { - return null; - } - }) - ) - ).filter((a) => a !== null); - + return Response.json(assignments); }); diff --git a/nextjs/src/models/local/assignment/localAssignment.ts b/nextjs/src/models/local/assignment/localAssignment.ts index 9beeea1..7f9d21d 100644 --- a/nextjs/src/models/local/assignment/localAssignment.ts +++ b/nextjs/src/models/local/assignment/localAssignment.ts @@ -15,6 +15,7 @@ export interface LocalAssignment extends IModuleItem { rubric: RubricItem[]; } + export const localAssignmentMarkdown = { parseMarkdown: assignmentMarkdownParser.parseMarkdown, toMarkdown: assignmentMarkdownSerializer.toMarkdown, diff --git a/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts b/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts index 8316594..c10fb9e 100644 --- a/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts +++ b/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts @@ -6,37 +6,44 @@ import { assignmentMarkdownSerializer } from "@/models/local/assignment/utils/as import path from "path"; import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils"; import { promises as fs } from "fs"; +import { courseItemFileStorageService } from "./courseItemFileStorageService"; +const getAssignmentNames = async (courseName: string, moduleName: string) => { + const filePath = path.join(basePath, courseName, moduleName, "assignments"); + if (!(await directoryOrFileExists(filePath))) { + console.log( + `Error loading course by name, assignments folder does not exist in ${filePath}` + ); + await fs.mkdir(filePath); + } + + const assignmentFiles = await fs.readdir(filePath); + return assignmentFiles.map((f) => f.replace(/\.md$/, "")); +}; +const getAssignment = async ( + courseName: string, + moduleName: string, + assignmentName: string +) => { + const filePath = path.join( + basePath, + courseName, + moduleName, + "assignments", + assignmentName + ".md" + ); + const rawFile = (await fs.readFile(filePath, "utf-8")).replace(/\r\n/g, "\n"); + return localAssignmentMarkdown.parseMarkdown(rawFile); +}; export const assignmentsFileStorageService = { - async getAssignmentNames(courseName: string, moduleName: string) { - const filePath = path.join(basePath, courseName, moduleName, "assignments"); - if (!(await directoryOrFileExists(filePath))) { - console.log( - `Error loading course by name, assignments folder does not exist in ${filePath}` - ); - await fs.mkdir(filePath); - } - - const assignmentFiles = await fs.readdir(filePath); - return assignmentFiles.map((f) => f.replace(/\.md$/, "")); - }, - async getAssignment( - courseName: string, - moduleName: string, - assignmentName: string - ) { - const filePath = path.join( - basePath, + getAssignmentNames, + getAssignment, + async getAssignments(courseName: string, moduleName: string) { + return await courseItemFileStorageService.getItems( courseName, moduleName, - "assignments", - assignmentName + ".md" + "Assignment" ); - const rawFile = (await fs.readFile(filePath, "utf-8")).replace( - /\r\n/g, - "\n" - ); - return localAssignmentMarkdown.parseMarkdown(rawFile); }, async updateOrCreateAssignment({ courseName, @@ -74,7 +81,6 @@ export const assignmentsFileStorageService = { moduleName: string; assignmentName: string; }) { - const filePath = path.join( basePath, courseName, @@ -83,6 +89,6 @@ export const assignmentsFileStorageService = { assignmentName + ".md" ); console.log("removing assignment", filePath); - await fs.unlink(filePath) - } + await fs.unlink(filePath); + }, }; diff --git a/nextjs/src/services/fileStorage/courseItemFileStorageService.ts b/nextjs/src/services/fileStorage/courseItemFileStorageService.ts new file mode 100644 index 0000000..ad2363d --- /dev/null +++ b/nextjs/src/services/fileStorage/courseItemFileStorageService.ts @@ -0,0 +1,102 @@ +import path from "path"; +import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils"; +import fs from "fs/promises"; +import { + LocalAssignment, + localAssignmentMarkdown, +} from "@/models/local/assignment/localAssignment"; +import { + LocalQuiz, + localQuizMarkdownUtils, +} from "@/models/local/quiz/localQuiz"; +import { + LocalCoursePage, + localPageMarkdownUtils, +} from "@/models/local/page/localCoursePage"; + +const typeToFolder = { + Assignment: "assignments", + Quiz: "quizzes", + Page: "pages", +} as const; + +export type CourseItemType = "Assignment" | "Quiz" | "Page"; + +const getItemFileNames = async ( + courseName: string, + moduleName: string, + type: CourseItemType +) => { + const folder = typeToFolder[type]; + const filePath = path.join(basePath, courseName, moduleName, folder); + if (!(await directoryOrFileExists(filePath))) { + console.log( + `Error loading ${type}, ${folder} folder does not exist in ${filePath}` + ); + await fs.mkdir(filePath); + } + + const itemFiles = await fs.readdir(filePath); + return itemFiles.map((f) => f.replace(/\.md$/, "")); +}; +type CourseItemReturnType = T extends "Assignment" + ? LocalAssignment + : T extends "Quiz" + ? LocalQuiz + : LocalCoursePage; + +const getItem = async ( + courseName: string, + moduleName: string, + name: string, + type: T +): Promise> => { + const folder = typeToFolder[type]; + const filePath = path.join( + basePath, + courseName, + moduleName, + folder, + name + ".md" + ); + const rawFile = (await fs.readFile(filePath, "utf-8")).replace(/\r\n/g, "\n"); + if (type === "Assignment") { + return localAssignmentMarkdown.parseMarkdown( + rawFile + ) as CourseItemReturnType; + } else if (type === "Quiz") { + return localQuizMarkdownUtils.parseMarkdown( + rawFile + ) as CourseItemReturnType; + } else if (type === "Page") { + return localPageMarkdownUtils.parseMarkdown( + rawFile + ) as CourseItemReturnType; + } + + throw Error(`cannot read item, invalid type: ${type} in ${filePath}`); +}; + +export const courseItemFileStorageService = { + getItem, + getItems: async ( + 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); + return item; + } catch { + return null; + } + }) + ) + ).filter((a) => a !== null); + return items; + }, +}; diff --git a/nextjs/src/services/fileStorage/pageFileStorageService.ts b/nextjs/src/services/fileStorage/pageFileStorageService.ts index 1568c60..d5e1f33 100644 --- a/nextjs/src/services/fileStorage/pageFileStorageService.ts +++ b/nextjs/src/services/fileStorage/pageFileStorageService.ts @@ -1,36 +1,23 @@ -import { localPageMarkdownUtils, LocalCoursePage } from "@/models/local/page/localCoursePage"; +import { + localPageMarkdownUtils, + LocalCoursePage, +} from "@/models/local/page/localCoursePage"; import { promises as fs } from "fs"; import path from "path"; -import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils"; +import { basePath } from "./utils/fileSystemUtils"; +import { courseItemFileStorageService } from "./courseItemFileStorageService"; export const pageFileStorageService = { - async getPageNames(courseName: string, moduleName: string) { - const filePath = path.join(basePath, courseName, moduleName, "pages"); - if (!(await directoryOrFileExists(filePath))) { - console.log( - `Error loading course by name, pages folder does not exist in ${filePath}` - ); - await fs.mkdir(filePath); - } - - const files = await fs.readdir(filePath); - return files.map((f) => f.replace(/\.md$/, "")); - }, - - async getPage(courseName: string, moduleName: string, pageName: string) { - const filePath = path.join( - basePath, + getPage: async (courseName: string, moduleName: string, name: string) => + await courseItemFileStorageService.getItem( courseName, moduleName, - "pages", - pageName + ".md" - ); - const rawFile = (await fs.readFile(filePath, "utf-8")).replace( - /\r\n/g, - "\n" - ); - return localPageMarkdownUtils.parseMarkdown(rawFile); - }, + name, + "Page" + ), + getPages: async (courseName: string, moduleName: string) => + await courseItemFileStorageService.getItems(courseName, moduleName, "Page"), + async updatePage( courseName: string, moduleName: string, @@ -82,6 +69,6 @@ export const pageFileStorageService = { pageName + ".md" ); console.log("removing page", filePath); - await fs.unlink(filePath) - } -}; \ No newline at end of file + await fs.unlink(filePath); + }, +}; diff --git a/nextjs/src/services/fileStorage/quizFileStorageService.ts b/nextjs/src/services/fileStorage/quizFileStorageService.ts index 3a4279a..8edeb15 100644 --- a/nextjs/src/services/fileStorage/quizFileStorageService.ts +++ b/nextjs/src/services/fileStorage/quizFileStorageService.ts @@ -1,60 +1,23 @@ import { - localQuizMarkdownUtils, LocalQuiz, } from "@/models/local/quiz/localQuiz"; import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; import path from "path"; -import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils"; +import { basePath } from "./utils/fileSystemUtils"; import { promises as fs } from "fs"; +import { courseItemFileStorageService } from "./courseItemFileStorageService"; -const getQuizNames = async (courseName: string, moduleName: string) => { - const filePath = path.join(basePath, courseName, moduleName, "quizzes"); - if (!(await directoryOrFileExists(filePath))) { - console.log( - `Error loading course by name, quiz folder does not exist in ${filePath}` - ); - await fs.mkdir(filePath); - } - - const files = await fs.readdir(filePath); - return files.map((f) => f.replace(/\.md$/, "")); -}; - -const getQuiz = async ( - courseName: string, - moduleName: string, - quizName: string -) => { - const filePath = path.join( - basePath, - courseName, - moduleName, - "quizzes", - quizName + ".md" - ); - const rawFile = (await fs.readFile(filePath, "utf-8")).replace(/\r\n/g, "\n"); - return localQuizMarkdownUtils.parseMarkdown(rawFile); -}; export const quizFileStorageService = { - getQuizNames, - getQuiz, - async getQuizzes(courseName: string, moduleName: string) { - const fileNames = await getQuizNames(courseName, moduleName); - const quizzes = ( - await Promise.all( - fileNames.map(async (name) => { - try { - return await getQuiz(courseName, moduleName, name); - } catch { - return null; - } - }) - ) - ).filter((a) => a !== null); - - return quizzes; - }, + 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: string, diff --git a/nextjs/src/services/tests/fileStorage.test.ts b/nextjs/src/services/tests/fileStorage.test.ts index fbbeae0..8933b42 100644 --- a/nextjs/src/services/tests/fileStorage.test.ts +++ b/nextjs/src/services/tests/fileStorage.test.ts @@ -1,14 +1,10 @@ -import path from "path"; import { describe, it, expect, beforeEach } from "vitest"; import { promises as fs } from "fs"; import { DayOfWeek, - LocalCourse, LocalCourseSettings, } from "@/models/local/localCourse"; -import { QuestionType } from "@/models/local/quiz/localQuizQuestion"; import { fileStorageService } from "../fileStorage/fileStorageService"; -import { basePath } from "../fileStorage/utils/fileSystemUtils"; describe("FileStorageTests", () => { beforeEach(async () => { @@ -55,52 +51,4 @@ describe("FileStorageTests", () => { expect(moduleNames).toContain(moduleName); }); - - it("invalid quizzes do not get loaded", async () => { - const courseName = "testCourse"; - const moduleName = "testModule"; - const validQuizMarkdown = `Name: validQuiz -LockAt: 08/28/2024 23:59:00 -DueAt: 08/28/2024 23:59:00 -Password: -ShuffleAnswers: true -ShowCorrectAnswers: false -OneQuestionAtATime: false -AssignmentGroup: Assignments -AllowedAttempts: -1 -Description: Repeat this quiz until you can complete it without notes/help. ---- -Points: 0.25 - -An empty string is - -a) truthy -*b) falsy -`; - 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 - ); - await fs.writeFile( - `${basePath}/${courseName}/${moduleName}/quizzes/validQuiz.md`, - validQuizMarkdown - ); - - const quizzes = await fileStorageService.quizzes.getQuizzes( - courseName, - moduleName - ); - const quizNames = quizzes.map((q) => q.name); - - expect(quizNames).not.includes("testQuiz"); - expect(quizNames).include("validQuiz"); - }); - }); diff --git a/nextjs/src/services/tests/fileStorageParsingErrors.test.ts b/nextjs/src/services/tests/fileStorageParsingErrors.test.ts new file mode 100644 index 0000000..3232b33 --- /dev/null +++ b/nextjs/src/services/tests/fileStorageParsingErrors.test.ts @@ -0,0 +1,167 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { promises as fs } from "fs"; +import { fileStorageService } from "../fileStorage/fileStorageService"; +import { basePath } from "../fileStorage/utils/fileSystemUtils"; + +describe("FileStorageTests", () => { + beforeEach(async () => { + const storageDirectory = + process.env.STORAGE_DIRECTORY ?? "/tmp/canvasManagerTests"; + try { + await fs.access(storageDirectory); + await fs.rm(storageDirectory, { recursive: true }); + } catch (error) {} + await fs.mkdir(storageDirectory, { recursive: true }); + }); + + it("invalid quizzes do not get loaded", async () => { + const courseName = "testCourse"; + const moduleName = "testModule"; + const validQuizMarkdown = `Name: validQuiz +LockAt: 08/28/2024 23:59:00 +DueAt: 08/28/2024 23:59:00 +Password: +ShuffleAnswers: true +ShowCorrectAnswers: false +OneQuestionAtATime: false +AssignmentGroup: Assignments +AllowedAttempts: -1 +Description: Repeat this quiz until you can complete it without notes/help. +--- +Points: 0.25 + +An empty string is + +a) truthy +*b) falsy +`; + 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 + ); + await fs.writeFile( + `${basePath}/${courseName}/${moduleName}/quizzes/validQuiz.md`, + validQuizMarkdown + ); + + const quizzes = await fileStorageService.quizzes.getQuizzes( + courseName, + moduleName + ); + 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"; + const validAssignmentMarkdown = `Name: testAssignment +LockAt: 09/19/2024 23:59:00 +DueAt: 09/19/2024 23:59:00 +AssignmentGroupName: Assignments +SubmissionTypes: +- online_text_entry +- online_upload +AllowedFileUploadExtensions: +- pdf +--- +description +## Rubric +- 2pts: animation has at least 5 transition states +`; + const invalidAssignment = "name: invalidAssignment\n---\nnot an assignment"; + await fileStorageService.createCourseFolderForTesting(courseName); + await fileStorageService.modules.createModule(courseName, moduleName); + + await fs.mkdir(`${basePath}/${courseName}/${moduleName}/assignments`, { + recursive: true, + }); + await fs.writeFile( + `${basePath}/${courseName}/${moduleName}/assignments/testAssignment.md`, + validAssignmentMarkdown + ); + await fs.writeFile( + `${basePath}/${courseName}/${moduleName}/assignments/invalidAssignment.md`, + invalidAssignment + ); + + const assignments = await fileStorageService.assignments.getAssignments( + courseName, + moduleName + ); + const assignmentNames = assignments.map((q) => q.name); + + expect(assignmentNames).not.includes("invalidAssignment"); + expect(assignmentNames).include("testAssignment"); + }); + + it("invalid pages dont get loaded", async () => { + const courseName = "testCourse"; + const moduleName = "testModule"; + const validPageMarkdown = `Name: validPage +DueDateForOrdering: 08/31/2024 23:59:00 +--- +# Deploying React +`; + const invalidPageMarkdown = `Name: invalidPage +DueDateFo59:00 +--- +# Deploying React`; + await fileStorageService.createCourseFolderForTesting(courseName); + await fileStorageService.modules.createModule(courseName, moduleName); + + await fs.mkdir(`${basePath}/${courseName}/${moduleName}/pages`, { + recursive: true, + }); + await fs.writeFile( + `${basePath}/${courseName}/${moduleName}/pages/validPage.md`, + validPageMarkdown + ); + await fs.writeFile( + `${basePath}/${courseName}/${moduleName}/pages/invalidPage.md`, + invalidPageMarkdown + ); + + const pages = await fileStorageService.pages.getPages( + courseName, + moduleName + ); + const assignmentNames = pages.map((q) => q.name); + + expect(assignmentNames).include("validPage"); + expect(assignmentNames).not.includes("invalidPage"); + }); +});