centralizing logic

This commit is contained in:
2024-09-27 10:36:09 -06:00
parent 1112b646f3
commit ae8bd1297e
8 changed files with 335 additions and 176 deletions

View File

@@ -8,25 +8,10 @@ export const GET = async (
}: { params: { courseName: string; moduleName: string } } }: { params: { courseName: string; moduleName: string } }
) => ) =>
await withErrorHandling(async () => { await withErrorHandling(async () => {
const names = await fileStorageService.assignments.getAssignmentNames( const assignments = await fileStorageService.assignments.getAssignments(
courseName, courseName,
moduleName 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); return Response.json(assignments);
}); });

View File

@@ -15,6 +15,7 @@ export interface LocalAssignment extends IModuleItem {
rubric: RubricItem[]; rubric: RubricItem[];
} }
export const localAssignmentMarkdown = { export const localAssignmentMarkdown = {
parseMarkdown: assignmentMarkdownParser.parseMarkdown, parseMarkdown: assignmentMarkdownParser.parseMarkdown,
toMarkdown: assignmentMarkdownSerializer.toMarkdown, toMarkdown: assignmentMarkdownSerializer.toMarkdown,

View File

@@ -6,37 +6,44 @@ import { assignmentMarkdownSerializer } from "@/models/local/assignment/utils/as
import path from "path"; import path from "path";
import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils"; import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils";
import { promises as fs } from "fs"; 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 = { export const assignmentsFileStorageService = {
async getAssignmentNames(courseName: string, moduleName: string) { getAssignmentNames,
const filePath = path.join(basePath, courseName, moduleName, "assignments"); getAssignment,
if (!(await directoryOrFileExists(filePath))) { async getAssignments(courseName: string, moduleName: string) {
console.log( return await courseItemFileStorageService.getItems(
`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,
courseName, courseName,
moduleName, moduleName,
"assignments", "Assignment"
assignmentName + ".md"
); );
const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
/\r\n/g,
"\n"
);
return localAssignmentMarkdown.parseMarkdown(rawFile);
}, },
async updateOrCreateAssignment({ async updateOrCreateAssignment({
courseName, courseName,
@@ -74,7 +81,6 @@ export const assignmentsFileStorageService = {
moduleName: string; moduleName: string;
assignmentName: string; assignmentName: string;
}) { }) {
const filePath = path.join( const filePath = path.join(
basePath, basePath,
courseName, courseName,
@@ -83,6 +89,6 @@ export const assignmentsFileStorageService = {
assignmentName + ".md" assignmentName + ".md"
); );
console.log("removing assignment", filePath); console.log("removing assignment", filePath);
await fs.unlink(filePath) await fs.unlink(filePath);
} },
}; };

View File

@@ -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 CourseItemType> = T extends "Assignment"
? LocalAssignment
: T extends "Quiz"
? LocalQuiz
: LocalCoursePage;
const getItem = async <T extends CourseItemType>(
courseName: string,
moduleName: string,
name: string,
type: T
): Promise<CourseItemReturnType<T>> => {
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<T>;
} else if (type === "Quiz") {
return localQuizMarkdownUtils.parseMarkdown(
rawFile
) as CourseItemReturnType<T>;
} else if (type === "Page") {
return localPageMarkdownUtils.parseMarkdown(
rawFile
) as CourseItemReturnType<T>;
}
throw Error(`cannot read item, invalid type: ${type} in ${filePath}`);
};
export const courseItemFileStorageService = {
getItem,
getItems: async <T extends CourseItemType>(
courseName: string,
moduleName: string,
type: T
): Promise<CourseItemReturnType<T>[]> => {
const fileNames = await getItemFileNames(courseName, moduleName, type);
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;
},
};

View File

@@ -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 { promises as fs } from "fs";
import path from "path"; import path from "path";
import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils"; import { basePath } from "./utils/fileSystemUtils";
import { courseItemFileStorageService } from "./courseItemFileStorageService";
export const pageFileStorageService = { export const pageFileStorageService = {
async getPageNames(courseName: string, moduleName: string) { getPage: async (courseName: string, moduleName: string, name: string) =>
const filePath = path.join(basePath, courseName, moduleName, "pages"); await courseItemFileStorageService.getItem(
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,
courseName, courseName,
moduleName, moduleName,
"pages", name,
pageName + ".md" "Page"
); ),
const rawFile = (await fs.readFile(filePath, "utf-8")).replace( getPages: async (courseName: string, moduleName: string) =>
/\r\n/g, await courseItemFileStorageService.getItems(courseName, moduleName, "Page"),
"\n"
);
return localPageMarkdownUtils.parseMarkdown(rawFile);
},
async updatePage( async updatePage(
courseName: string, courseName: string,
moduleName: string, moduleName: string,
@@ -82,6 +69,6 @@ export const pageFileStorageService = {
pageName + ".md" pageName + ".md"
); );
console.log("removing page", filePath); console.log("removing page", filePath);
await fs.unlink(filePath) await fs.unlink(filePath);
} },
}; };

View File

@@ -1,60 +1,23 @@
import { import {
localQuizMarkdownUtils,
LocalQuiz, LocalQuiz,
} from "@/models/local/quiz/localQuiz"; } from "@/models/local/quiz/localQuiz";
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import path from "path"; import path from "path";
import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils"; import { basePath } from "./utils/fileSystemUtils";
import { promises as fs } from "fs"; 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 = { export const quizFileStorageService = {
getQuizNames, getQuiz: async (courseName: string, moduleName: string, quizName: string) =>
getQuiz, await courseItemFileStorageService.getItem(
async getQuizzes(courseName: string, moduleName: string) { courseName,
const fileNames = await getQuizNames(courseName, moduleName); moduleName,
const quizzes = ( quizName,
await Promise.all( "Quiz"
fileNames.map(async (name) => { ),
try { getQuizzes: async (courseName: string, moduleName: string) =>
return await getQuiz(courseName, moduleName, name); await courseItemFileStorageService.getItems(courseName, moduleName, "Quiz"),
} catch {
return null;
}
})
)
).filter((a) => a !== null);
return quizzes;
},
async updateQuiz( async updateQuiz(
courseName: string, courseName: string,

View File

@@ -1,14 +1,10 @@
import path from "path";
import { describe, it, expect, beforeEach } from "vitest"; import { describe, it, expect, beforeEach } from "vitest";
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import { import {
DayOfWeek, DayOfWeek,
LocalCourse,
LocalCourseSettings, LocalCourseSettings,
} from "@/models/local/localCourse"; } from "@/models/local/localCourse";
import { QuestionType } from "@/models/local/quiz/localQuizQuestion";
import { fileStorageService } from "../fileStorage/fileStorageService"; import { fileStorageService } from "../fileStorage/fileStorageService";
import { basePath } from "../fileStorage/utils/fileSystemUtils";
describe("FileStorageTests", () => { describe("FileStorageTests", () => {
beforeEach(async () => { beforeEach(async () => {
@@ -55,52 +51,4 @@ describe("FileStorageTests", () => {
expect(moduleNames).toContain(moduleName); 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");
});
}); });

View File

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