moving v2 to top level

This commit is contained in:
2024-12-17 09:19:21 -07:00
parent 5f0b3554dc
commit 576ee02afb
468 changed files with 79 additions and 15430 deletions

View File

@@ -0,0 +1,97 @@
import {
localAssignmentMarkdown,
LocalAssignment,
} from "@/models/local/assignment/localAssignment";
import { assignmentMarkdownSerializer } from "@/models/local/assignment/utils/assignmentMarkdownSerializer";
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);
return [];
}
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 = {
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 folder = path.join(basePath, courseName, moduleName, "assignments");
await fs.mkdir(folder, { recursive: true });
const filePath = path.join(
basePath,
courseName,
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 filePath = path.join(
basePath,
courseName,
moduleName,
"assignments",
assignmentName + ".md"
);
console.log("removing assignment", filePath);
await fs.unlink(filePath);
},
};

View File

@@ -0,0 +1,134 @@
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";
import { assignmentMarkdownSerializer } from "@/models/local/assignment/utils/assignmentMarkdownSerializer";
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import {
CourseItemReturnType,
CourseItemType,
typeToFolder,
} from "@/models/local/courseItemTypes";
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$/, ""));
};
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;
},
async updateOrCreateAssignment({
courseName,
moduleName,
name,
item,
type,
}: {
courseName: string;
moduleName: string;
name: string;
item: LocalAssignment | LocalQuiz | LocalCoursePage;
type: CourseItemType;
}) {
const typeFolder = typeToFolder[type];
const folder = path.join(basePath, courseName, moduleName, typeFolder);
await fs.mkdir(folder, { recursive: true });
const filePath = path.join(
basePath,
courseName,
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

@@ -0,0 +1,54 @@
import { promises as fs } from "fs";
import path from "path";
import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils";
import { assignmentsFileStorageService } from "./assignmentsFileStorageService";
import { quizFileStorageService } from "./quizFileStorageService";
import { pageFileStorageService } from "./pageFileStorageService";
import { moduleFileStorageService } from "./moduleFileStorageService";
import { settingsFileStorageService } from "./settingsFileStorageService";
export const fileStorageService = {
settings: settingsFileStorageService,
modules: moduleFileStorageService,
assignments: assignmentsFileStorageService,
quizzes: quizFileStorageService,
pages: pageFileStorageService,
async getEmptyDirectories(): Promise<string[]> {
if (!(await directoryOrFileExists(basePath))) {
throw new Error(
`Cannot get empty directories, ${basePath} does not exist`
);
}
const directories = await fs.readdir(basePath, { withFileTypes: true });
const emptyDirectories = (
await Promise.all(
directories
.filter((dirent) => dirent.isDirectory())
.map((dirent) => path.join(dirent.name))
.map(async (directory) => {
return {
directory,
files: await fs.readdir(path.join(basePath, directory)),
};
})
)
)
.filter(({ files }) => files.length === 0)
.map(({ directory }) => directory);
return emptyDirectories;
},
async createCourseFolderForTesting(courseName: string) {
const courseDirectory = path.join(basePath, courseName);
await fs.mkdir(courseDirectory, { recursive: true });
},
async createModuleFolderForTesting(courseName: string, moduleName: string) {
const courseDirectory = path.join(basePath, courseName, moduleName);
await fs.mkdir(courseDirectory, { recursive: true });
},
};

View File

@@ -0,0 +1,121 @@
import path from "path";
import { basePath } from "./utils/fileSystemUtils";
import fs from "fs/promises";
import {
getLectureWeekName,
lectureFolderName,
lectureToString,
parseLecture,
} from "./utils/lectureUtils";
import { Lecture } from "@/models/local/lecture";
import {
getDayOfWeek,
LocalCourseSettings,
} from "@/models/local/localCourseSettings";
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
export async function getLectures(courseName: string) {
const courseLectureRoot = path.join(basePath, courseName, 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 courseLectureRoot = path.join(basePath, courseName, 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 courseLectureRoot = path.join(basePath, courseName, 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}`);
} 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

@@ -0,0 +1,28 @@
import { promises as fs } from "fs";
import path from "path";
import { basePath } from "./utils/fileSystemUtils";
import { lectureFolderName } from "./utils/lectureUtils";
export const moduleFileStorageService = {
async getModuleNames(courseName: string) {
const courseDirectory = path.join(basePath, 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 = path.join(basePath, courseName);
await fs.mkdir(courseDirectory + "/" + moduleName, { recursive: true });
},
};

View File

@@ -0,0 +1,79 @@
import {
localPageMarkdownUtils,
LocalCoursePage,
} from "@/models/local/page/localCoursePage";
import { promises as fs } from "fs";
import path from "path";
import { basePath } from "./utils/fileSystemUtils";
import { courseItemFileStorageService } from "./courseItemFileStorageService";
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 folder = path.join(basePath, courseName, moduleName, "pages");
await fs.mkdir(folder, { recursive: true });
const filePath = path.join(
basePath,
courseName,
moduleName,
"pages",
page.name + ".md"
);
const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
console.log(`Saving page ${filePath}`);
await fs.writeFile(filePath, pageMarkdown);
const pageNameIsChanged = pageName !== page.name;
if (pageNameIsChanged) {
console.log("removing old page after name change " + pageName);
const oldFilePath = path.join(
basePath,
courseName,
moduleName,
"pages",
pageName + ".md"
);
await fs.unlink(oldFilePath);
}
},
async delete({
courseName,
moduleName,
pageName,
}: {
courseName: string;
moduleName: string;
pageName: string;
}) {
const filePath = path.join(
basePath,
courseName,
moduleName,
"pages",
pageName + ".md"
);
console.log("removing page", filePath);
await fs.unlink(filePath);
},
};

View File

@@ -0,0 +1,63 @@
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import path from "path";
import { basePath } from "./utils/fileSystemUtils";
import { promises as fs } from "fs";
import { courseItemFileStorageService } from "./courseItemFileStorageService";
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 folder = path.join(basePath, courseName, moduleName, "quizzes");
await fs.mkdir(folder, { recursive: true });
const filePath = path.join(
basePath,
courseName,
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 filePath = path.join(
basePath,
courseName,
moduleName,
"quizzes",
quizName + ".md"
);
console.log("removing quiz", filePath);
await fs.unlink(filePath);
},
};

View File

@@ -0,0 +1,86 @@
import {
LocalCourseSettings,
localCourseYamlUtils,
} from "@/models/local/localCourseSettings";
import { promises as fs } from "fs";
import path from "path";
import {
basePath,
directoryOrFileExists,
getCourseNames,
} from "./utils/fileSystemUtils";
import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
const getCourseSettings = async (
courseName: string
): Promise<LocalCourseSettings> => {
const courseDirectory = path.join(basePath, courseName);
const settingsPath = path.join(courseDirectory, "settings.yml");
if (!(await directoryOrFileExists(settingsPath))) {
const errorMessage = `could not find settings for ${courseName}, settings file ${settingsPath}`;
console.log(errorMessage);
throw new Error(errorMessage);
}
const settingsString = await fs.readFile(settingsPath, "utf-8");
const settingsFromFile =
localCourseYamlUtils.parseSettingYaml(settingsString);
const settings: LocalCourseSettings = populateDefaultValues(settingsFromFile);
const folderName = path.basename(courseDirectory);
return { ...settings, name: folderName };
};
const populateDefaultValues = (settingsFromFile: LocalCourseSettings) => {
const defaultSubmissionType = [
AssignmentSubmissionType.ONLINE_TEXT_ENTRY,
AssignmentSubmissionType.ONLINE_UPLOAD,
];
const defaultFileUploadTypes = ["pdf", "jpg", "jpeg"];
const settings: LocalCourseSettings = {
...settingsFromFile,
defaultAssignmentSubmissionTypes:
settingsFromFile.defaultAssignmentSubmissionTypes ||
defaultSubmissionType,
defaultFileUploadTypes:
settingsFromFile.defaultFileUploadTypes || defaultFileUploadTypes,
holidays: Array.isArray(settingsFromFile.holidays)
? settingsFromFile.holidays
: [],
assets: Array.isArray(settingsFromFile.assets)
? settingsFromFile.assets
: [],
};
return settings;
};
export const settingsFileStorageService = {
getCourseSettings,
async getAllCoursesSettings() {
const courses = await getCourseNames();
const courseSettings = await Promise.all(
courses.map(async (c) => await getCourseSettings(c))
);
return courseSettings;
},
async updateCourseSettings(
courseName: string,
settings: LocalCourseSettings
) {
const courseDirectory = path.join(basePath, courseName);
const settingsPath = path.join(courseDirectory, "settings.yml");
const { name, ...settingsWithoutName } = settings;
const settingsMarkdown =
localCourseYamlUtils.settingsToYaml(settingsWithoutName);
console.log(`Saving settings ${settingsPath}`);
await fs.writeFile(settingsPath, settingsMarkdown);
},
};

View File

@@ -0,0 +1,52 @@
import { promises as fs } from "fs";
import path from "path";
export const hasFileSystemEntries = async (
directoryPath: string
): Promise<boolean> => {
try {
const entries = await fs.readdir(directoryPath);
return entries.length > 0;
} catch {
return false;
}
};
export const directoryOrFileExists = async (directoryPath: string): Promise<boolean> => {
try {
await fs.access(directoryPath);
return true;
} catch {
return false;
}
};
export async function getCourseNames() {
console.log("loading course ids");
const courseDirectories = await fs.readdir(basePath, {
withFileTypes: true,
});
const coursePromises = await Promise.all(
courseDirectories
.filter((dirent) => dirent.isDirectory())
.map(async (dirent) => {
const coursePath = path.join(basePath, dirent.name);
const settingsPath = path.join(coursePath, "settings.yml");
const hasSettings = await directoryOrFileExists(settingsPath);
return {
dirent,
hasSettings,
};
})
);
const courseNamesFromDirectories = coursePromises
.filter(({ hasSettings }) => hasSettings)
.map(({ dirent }) => dirent.name);
return courseNamesFromDirectories;
}
export const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
console.log("base path", basePath);

View File

@@ -0,0 +1,48 @@
import { getWeekNumber } from "@/app/course/[courseName]/calendar/calendarMonthUtils";
import { extractLabelValue } from "@/models/local/assignment/utils/markdownUtils";
import { Lecture } from "@/models/local/lecture";
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
export function parseLecture(fileContent: string): Lecture {
try {
const settings = fileContent.split("---\n")[0];
const name = extractLabelValue(settings, "Name");
const date = extractLabelValue(settings, "Date");
const content = fileContent.split("---\n").slice(1).join("---\n").trim();
return {
name,
date,
content,
};
} catch (error) {
console.error("Error parsing lecture: ", fileContent);
throw error;
}
}
export function lectureToString(lecture: Lecture) {
return `Name: ${lecture.name}
Date: ${lecture.date}
---
${lecture.content}`;
}
export const lectureFolderName = "00 - lectures";
export function getLectureWeekName(semesterStart: string, lectureDate: string) {
const startDate = getDateFromStringOrThrow(
semesterStart,
"semester start date in update lecture"
);
const targetDate = getDateFromStringOrThrow(
lectureDate,
"lecture start date in update lecture"
);
const weekNumber = getWeekNumber(startDate, targetDate)
.toString()
.padStart(2, "0");
const weekName = `week-${weekNumber}`;
return weekName;
}