mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
adding storage for lectures
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
dateToMarkdownString,
|
dateToMarkdownString,
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
|
|||||||
@@ -1,10 +1,53 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
import { MonacoEditor } from "@/components/editor/MonacoEditor";
|
||||||
import { useState } from "react";
|
import {
|
||||||
|
useLecturesByWeekQuery,
|
||||||
|
useLectureUpdateMutation,
|
||||||
|
} from "@/hooks/localCourse/lectureHooks";
|
||||||
|
import { Lecture } from "@/models/local/lecture";
|
||||||
|
import {
|
||||||
|
lectureToString,
|
||||||
|
parseLecture,
|
||||||
|
} from "@/services/fileStorage/utils/lectureUtils";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import LecturePreview from "./LecturePreview";
|
||||||
|
|
||||||
export default function EditLecture({ lectureDay }: { lectureDay: string }) {
|
export default function EditLecture({ lectureDay }: { lectureDay: string }) {
|
||||||
const [text, setText] = useState("");
|
const { data: weeks } = useLecturesByWeekQuery();
|
||||||
|
const updateLecture = useLectureUpdateMutation();
|
||||||
|
const lecture = weeks
|
||||||
|
.flatMap(({ lectures }) => lectures.map((lecture) => lecture))
|
||||||
|
.find((l) => l.date === lectureDay);
|
||||||
|
|
||||||
|
const startingText = lecture ? lectureToString(lecture) : `Name: Name Here
|
||||||
|
Date: ${lectureDay}
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [text, setText] = useState(startingText);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const delay = 500;
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const parsed = parseLecture(text);
|
||||||
|
if (!lecture || lectureToString(parsed) !== lectureToString(lecture)) {
|
||||||
|
console.log("updating lecture");
|
||||||
|
updateLecture.mutate(parsed);
|
||||||
|
}
|
||||||
|
setError("");
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.toString());
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [lecture, text, updateLecture]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="columns-2 min-h-0 flex-1">
|
<div className="columns-2 min-h-0 flex-1">
|
||||||
@@ -12,8 +55,8 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) {
|
|||||||
<MonacoEditor value={text} onChange={setText} />
|
<MonacoEditor value={text} onChange={setText} />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{/* <div className="text-red-300">{error && error}</div> */}
|
<div className="text-red-300">{error && error}</div>
|
||||||
{/* <AssignmentPreview assignment={assignment} /> */}
|
{lecture && <LecturePreview lecture={lecture} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Lecture } from "@/models/local/lecture";
|
||||||
|
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||||
|
|
||||||
|
export default function LecturePreview({ lecture }: { lecture: Lecture }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1 text-end pe-3">Name</div>
|
||||||
|
<div className="flex-1">{lecture.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1 text-end pe-3">Date</div>
|
||||||
|
<div className="flex-1">{lecture.date}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div
|
||||||
|
className="markdownPreview"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: markdownToHTMLSafe(lecture.content),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import EditLecture from "./EditLecture";
|
import EditLecture from "./EditLecture";
|
||||||
|
import { getDateFromStringOrThrow, getDateOnlyMarkdownString } from "@/models/local/timeUtils";
|
||||||
|
|
||||||
export default function page({
|
export default function page({
|
||||||
params: { lectureDay },
|
params: { lectureDay },
|
||||||
@@ -8,5 +9,7 @@ export default function page({
|
|||||||
}) {
|
}) {
|
||||||
const decodedLectureDay = decodeURIComponent(lectureDay);
|
const decodedLectureDay = decodeURIComponent(lectureDay);
|
||||||
console.log(decodedLectureDay);
|
console.log(decodedLectureDay);
|
||||||
return <EditLecture lectureDay={decodedLectureDay} />;
|
const lectureDate = getDateFromStringOrThrow(decodedLectureDay, "lecture day in lecture page")
|
||||||
|
const lectureDayOnly = getDateOnlyMarkdownString(lectureDate)
|
||||||
|
return <EditLecture lectureDay={lectureDayOnly} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
useSuspenseQuery,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import { lectureKeys } from "./lectureKeys";
|
import { lectureKeys } from "./lectureKeys";
|
||||||
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
|
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
|
||||||
import { getLectures } from "@/services/fileStorage/lectureFileStorageService";
|
import {
|
||||||
|
getLectures,
|
||||||
|
updateLecture,
|
||||||
|
} from "@/services/fileStorage/lectureFileStorageService";
|
||||||
|
import { Lecture } from "@/models/local/lecture";
|
||||||
|
import { useLocalCourseSettingsQuery } from "./localCoursesHooks";
|
||||||
|
|
||||||
export const getLecturesQueryConfig = (courseName: string) =>
|
export const getLecturesQueryConfig = (courseName: string) =>
|
||||||
({
|
({
|
||||||
@@ -9,7 +18,23 @@ export const getLecturesQueryConfig = (courseName: string) =>
|
|||||||
queryFn: async () => await getLectures(courseName),
|
queryFn: async () => await getLectures(courseName),
|
||||||
} as const);
|
} as const);
|
||||||
|
|
||||||
export const useLecturesQuery = () => {
|
export const useLecturesByWeekQuery = () => {
|
||||||
const { courseName } = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
return useSuspenseQuery(getLecturesQueryConfig(courseName));
|
return useSuspenseQuery(getLecturesQueryConfig(courseName));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useLectureUpdateMutation = () => {
|
||||||
|
const { courseName } = useCourseContext();
|
||||||
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (lecture: Lecture) => {
|
||||||
|
await updateLecture(courseName, settings, lecture);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: lectureKeys.allLectures(courseName),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ const getAssignmentNames = async (courseName: string, moduleName: string) => {
|
|||||||
console.log(
|
console.log(
|
||||||
`Error loading course by name, assignments folder does not exist in ${filePath}`
|
`Error loading course by name, assignments folder does not exist in ${filePath}`
|
||||||
);
|
);
|
||||||
await fs.mkdir(filePath);
|
// await fs.mkdir(filePath);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const assignmentFiles = await fs.readdir(filePath);
|
const assignmentFiles = await fs.readdir(filePath);
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { basePath } from "./utils/fileSystemUtils";
|
import { basePath } from "./utils/fileSystemUtils";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
|
import {
|
||||||
|
lectureFolderName,
|
||||||
|
lectureToString,
|
||||||
|
parseLecture,
|
||||||
|
} from "./utils/lectureUtils";
|
||||||
import { Lecture } from "@/models/local/lecture";
|
import { Lecture } from "@/models/local/lecture";
|
||||||
import { extractLabelValue } from "@/models/local/assignment/utils/markdownUtils";
|
import { getDayOfWeek, LocalCourseSettings } from "@/models/local/localCourse";
|
||||||
import { getDateOnlyMarkdownString } from "@/models/local/timeUtils";
|
import { getWeekNumber } from "@/app/course/[courseName]/calendar/calendarMonthUtils";
|
||||||
|
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
|
||||||
|
|
||||||
export async function getLectures(courseName: string) {
|
export async function getLectures(courseName: string) {
|
||||||
const courseLectureRoot = path.join(basePath, courseName, "lectures");
|
const courseLectureRoot = path.join(basePath, courseName, lectureFolderName);
|
||||||
if (!(await directoryExists(courseLectureRoot))) {
|
if (!(await directoryExists(courseLectureRoot))) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -39,30 +45,36 @@ export async function getLectures(courseName: string) {
|
|||||||
return lecturesByWeek;
|
return lecturesByWeek;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseLecture(fileContent: string): Lecture {
|
export async function updateLecture(
|
||||||
try {
|
courseName: string,
|
||||||
const settings = fileContent.split("---\n")[0];
|
courseSettings: LocalCourseSettings,
|
||||||
const name = extractLabelValue(settings, "Name");
|
lecture: Lecture
|
||||||
const date = extractLabelValue(settings, "Date");
|
) {
|
||||||
|
const courseLectureRoot = path.join(basePath, courseName, lectureFolderName);
|
||||||
|
const startDate = getDateFromStringOrThrow(
|
||||||
|
courseSettings.startDate,
|
||||||
|
"semester start date in update lecture"
|
||||||
|
);
|
||||||
|
const lectureDate = getDateFromStringOrThrow(
|
||||||
|
lecture.date,
|
||||||
|
"lecture start date in update lecture"
|
||||||
|
);
|
||||||
|
const weekNumber = getWeekNumber(startDate, lectureDate)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0");
|
||||||
|
|
||||||
const content = fileContent.split("---\n")[1].trim();
|
const weekFolderName = `week-${weekNumber}`;
|
||||||
|
const weekPath = path.join(courseLectureRoot, weekFolderName);
|
||||||
return {
|
if (!(await directoryExists(weekPath))) {
|
||||||
name,
|
await fs.mkdir(weekPath, { recursive: true });
|
||||||
date,
|
|
||||||
content,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing lecture", fileContent);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function lectureToString(lecture: Lecture) {
|
const lecturePath = path.join(
|
||||||
return `Name: ${lecture.name}
|
weekPath,
|
||||||
Date: ${lecture.date}
|
`${lectureDate.getDay()}-${getDayOfWeek(lectureDate)}.md`
|
||||||
---
|
);
|
||||||
${lecture.content}`;
|
const lectureContents = lectureToString(lecture);
|
||||||
|
await fs.writeFile(lecturePath, lectureContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
const directoryExists = async (path: string): Promise<boolean> => {
|
const directoryExists = async (path: string): Promise<boolean> => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { basePath } from "./utils/fileSystemUtils";
|
import { basePath } from "./utils/fileSystemUtils";
|
||||||
|
import { lectureFolderName } from "./utils/lectureUtils";
|
||||||
|
|
||||||
export const moduleFileStorageService = {
|
export const moduleFileStorageService = {
|
||||||
async getModuleNames(courseName: string) {
|
async getModuleNames(courseName: string) {
|
||||||
@@ -14,9 +15,10 @@ export const moduleFileStorageService = {
|
|||||||
.map((dirent) => dirent.name);
|
.map((dirent) => dirent.name);
|
||||||
|
|
||||||
const modules = await Promise.all(modulePromises);
|
const modules = await Promise.all(modulePromises);
|
||||||
return modules
|
const modulesWithoutLectures = modules.filter(
|
||||||
.filter((m) => m !== "lectures")
|
(m) => m !== lectureFolderName
|
||||||
.sort((a, b) => a.localeCompare(b));
|
);
|
||||||
|
return modulesWithoutLectures.sort((a, b) => a.localeCompare(b));
|
||||||
},
|
},
|
||||||
async createModule(courseName: string, moduleName: string) {
|
async createModule(courseName: string, moduleName: string) {
|
||||||
const courseDirectory = path.join(basePath, courseName);
|
const courseDirectory = path.join(basePath, courseName);
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import {
|
|
||||||
LocalAssignment,
|
|
||||||
localAssignmentMarkdown,
|
|
||||||
} from "@/models/local/assignment/localAssignment";
|
|
||||||
import {
|
|
||||||
LocalCourse,
|
|
||||||
LocalCourseSettings,
|
|
||||||
localCourseYamlUtils,
|
|
||||||
} from "@/models/local/localCourse";
|
|
||||||
import { LocalModule } from "@/models/local/localModules";
|
|
||||||
import {
|
|
||||||
LocalCoursePage,
|
|
||||||
localPageMarkdownUtils,
|
|
||||||
} from "@/models/local/page/localCoursePage";
|
|
||||||
import {
|
|
||||||
LocalQuiz,
|
|
||||||
localQuizMarkdownUtils,
|
|
||||||
} from "@/models/local/quiz/localQuiz";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { directoryOrFileExists } from "./fileSystemUtils";
|
|
||||||
|
|
||||||
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
|
|
||||||
|
|
||||||
export const courseMarkdownLoader = {
|
|
||||||
// async loadSavedCourses(): Promise<LocalCourse[]> {
|
|
||||||
// const courseDirectories = await fs.readdir(basePath, {
|
|
||||||
// withFileTypes: true,
|
|
||||||
// });
|
|
||||||
// const coursePromises = courseDirectories
|
|
||||||
// .filter((dirent) => dirent.isDirectory())
|
|
||||||
// .map(async (dirent) => {
|
|
||||||
// const coursePath = path.join(basePath, dirent.name);
|
|
||||||
// const settingsPath = path.join(coursePath, "settings.yml");
|
|
||||||
// if (await directoryOrFileExists(settingsPath)) {
|
|
||||||
// return this.loadCourseByPath(coursePath);
|
|
||||||
// }
|
|
||||||
// return null;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const courses = (await Promise.all(coursePromises)).filter(
|
|
||||||
// (course) => course !== null
|
|
||||||
// ) as LocalCourse[];
|
|
||||||
// return courses.sort((a, b) =>
|
|
||||||
// a.settings.name.localeCompare(b.settings.name)
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
|
|
||||||
// async loadCourseByPath(courseDirectory: string): Promise<LocalCourse> {
|
|
||||||
// if (!(await directoryOrFileExists(courseDirectory))) {
|
|
||||||
// const errorMessage = `Error loading course by name, could not find folder ${courseDirectory}`;
|
|
||||||
// console.log(errorMessage);
|
|
||||||
// throw new Error(errorMessage);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const settings = await this.loadCourseSettings(courseDirectory);
|
|
||||||
// const modules = await this.loadCourseModules(courseDirectory);
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// settings,
|
|
||||||
// modules,
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
|
|
||||||
// async loadCourseSettings(
|
|
||||||
// courseDirectory: string
|
|
||||||
// ): Promise<LocalCourseSettings> {
|
|
||||||
// const settingsPath = path.join(courseDirectory, "settings.yml");
|
|
||||||
// if (!(await directoryOrFileExists(settingsPath))) {
|
|
||||||
// const errorMessage = `Error loading course by name, settings file ${settingsPath}`;
|
|
||||||
// console.log(errorMessage);
|
|
||||||
// throw new Error(errorMessage);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const settingsString = await fs.readFile(settingsPath, "utf-8");
|
|
||||||
// const settings = localCourseYamlUtils.parseSettingYaml(settingsString);
|
|
||||||
|
|
||||||
// const folderName = path.basename(courseDirectory);
|
|
||||||
// return { ...settings, name: folderName };
|
|
||||||
// },
|
|
||||||
|
|
||||||
// async loadCourseModules(courseDirectory: string): Promise<LocalModule[]> {
|
|
||||||
// const moduleDirectories = await fs.readdir(courseDirectory, {
|
|
||||||
// withFileTypes: true,
|
|
||||||
// });
|
|
||||||
// const modulePromises = moduleDirectories
|
|
||||||
// .filter((dirent) => dirent.isDirectory())
|
|
||||||
// .map((dirent) =>
|
|
||||||
// this.loadModuleFromPath(path.join(courseDirectory, dirent.name))
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const modules = await Promise.all(modulePromises);
|
|
||||||
// return modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
// },
|
|
||||||
|
|
||||||
// async loadModuleFromPath(modulePath: string): Promise<LocalModule> {
|
|
||||||
// const moduleName = path.basename(modulePath);
|
|
||||||
// const assignments = await this.loadAssignmentsFromPath(modulePath);
|
|
||||||
// const quizzes = await this.loadQuizzesFromPath(modulePath);
|
|
||||||
// const pages = await this.loadModulePagesFromPath(modulePath);
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// name: moduleName,
|
|
||||||
// assignments,
|
|
||||||
// quizzes,
|
|
||||||
// pages,
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
|
|
||||||
// async loadAssignmentsFromPath(
|
|
||||||
// modulePath: string
|
|
||||||
// ): Promise<LocalAssignment[]> {
|
|
||||||
// const assignmentsPath = path.join(modulePath, "assignments");
|
|
||||||
// if (!(await directoryOrFileExists(assignmentsPath))) {
|
|
||||||
// console.log(
|
|
||||||
// `Error loading course by name, assignments folder does not exist in ${modulePath}`
|
|
||||||
// );
|
|
||||||
// await fs.mkdir(assignmentsPath);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const assignmentFiles = await fs.readdir(assignmentsPath);
|
|
||||||
// const assignmentPromises = assignmentFiles.map(async (file) => {
|
|
||||||
// const filePath = path.join(assignmentsPath, file);
|
|
||||||
// const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
|
|
||||||
// /\r\n/g,
|
|
||||||
// "\n"
|
|
||||||
// );
|
|
||||||
// return localAssignmentMarkdown.parseMarkdown(rawFile);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return await Promise.all(assignmentPromises);
|
|
||||||
// },
|
|
||||||
|
|
||||||
// async loadQuizzesFromPath(modulePath: string): Promise<LocalQuiz[]> {
|
|
||||||
// const quizzesPath = path.join(modulePath, "quizzes");
|
|
||||||
// if (!(await directoryOrFileExists(quizzesPath))) {
|
|
||||||
// console.log(
|
|
||||||
// `Quizzes folder does not exist in ${modulePath}, creating now`
|
|
||||||
// );
|
|
||||||
// await fs.mkdir(quizzesPath);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const quizFiles = await fs.readdir(quizzesPath);
|
|
||||||
// const quizPromises = quizFiles.map(async (file) => {
|
|
||||||
// const filePath = path.join(quizzesPath, file);
|
|
||||||
// const rawQuiz = (await fs.readFile(filePath, "utf-8")).replace(
|
|
||||||
// /\r\n/g,
|
|
||||||
// "\n"
|
|
||||||
// );
|
|
||||||
// return localQuizMarkdownUtils.parseMarkdown(rawQuiz);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return await Promise.all(quizPromises);
|
|
||||||
// },
|
|
||||||
|
|
||||||
// async loadModulePagesFromPath(
|
|
||||||
// modulePath: string
|
|
||||||
// ): Promise<LocalCoursePage[]> {
|
|
||||||
// const pagesPath = path.join(modulePath, "pages");
|
|
||||||
// if (!(await directoryOrFileExists(pagesPath))) {
|
|
||||||
// console.log(`Pages folder does not exist in ${modulePath}, creating now`);
|
|
||||||
// await fs.mkdir(pagesPath);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const pageFiles = await fs.readdir(pagesPath);
|
|
||||||
// const pagePromises = pageFiles.map(async (file) => {
|
|
||||||
// const filePath = path.join(pagesPath, file);
|
|
||||||
// const rawPage = (await fs.readFile(filePath, "utf-8")).replace(
|
|
||||||
// /\r\n/g,
|
|
||||||
// "\n"
|
|
||||||
// );
|
|
||||||
// return localPageMarkdownUtils.parseMarkdown(rawPage);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return await Promise.all(pagePromises);
|
|
||||||
// },
|
|
||||||
};
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import { localAssignmentMarkdown } from "@/models/local/assignment/localAssignment";
|
|
||||||
import { LocalCourse, localCourseYamlUtils } from "@/models/local/localCourse";
|
|
||||||
import { LocalModule } from "@/models/local/localModules";
|
|
||||||
import { localPageMarkdownUtils } from "@/models/local/page/localCoursePage";
|
|
||||||
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
|
|
||||||
|
|
||||||
const directoryExists = async (directoryPath: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await fs.access(directoryPath);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSettings = async (course: LocalCourse, courseDirectory: string) => {
|
|
||||||
const settingsFilePath = path.join(courseDirectory, "settings.yml");
|
|
||||||
const settingsYaml = localCourseYamlUtils.settingsToYaml(course.settings);
|
|
||||||
await fs.writeFile(settingsFilePath, settingsYaml);
|
|
||||||
};
|
|
||||||
|
|
||||||
// const saveModules = async (
|
|
||||||
// course: LocalCourse,
|
|
||||||
// courseDirectory: string,
|
|
||||||
// previouslyStoredCourse?: LocalCourse
|
|
||||||
// ) => {
|
|
||||||
// for (const localModule of course.modules) {
|
|
||||||
// const moduleDirectory = path.join(courseDirectory, localModule.name);
|
|
||||||
// if (!(await directoryExists(moduleDirectory))) {
|
|
||||||
// await fs.mkdir(moduleDirectory, { recursive: true });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await saveQuizzes(course, localModule, previouslyStoredCourse);
|
|
||||||
// await saveAssignments(course, localModule, previouslyStoredCourse);
|
|
||||||
// await savePages(course, localModule, previouslyStoredCourse);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const moduleNames = course.modules.map((m) => m.name);
|
|
||||||
// const moduleDirectories = await fs.readdir(courseDirectory, {
|
|
||||||
// withFileTypes: true,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// for (const dirent of moduleDirectories) {
|
|
||||||
// if (dirent.isDirectory() && !moduleNames.includes(dirent.name)) {
|
|
||||||
// const moduleDirPath = path.join(courseDirectory, dirent.name);
|
|
||||||
// console.log(
|
|
||||||
// `Deleting extra module directory, it was probably renamed ${moduleDirPath}`
|
|
||||||
// );
|
|
||||||
// await fs.rmdir(moduleDirPath, { recursive: true });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const saveQuizzes = async (
|
|
||||||
// course: LocalCourse,
|
|
||||||
// module: LocalModule,
|
|
||||||
// previouslyStoredCourse?: LocalCourse
|
|
||||||
// ) => {
|
|
||||||
// const quizzesDirectory = path.join(
|
|
||||||
// basePath,
|
|
||||||
// course.settings.name,
|
|
||||||
// module.name,
|
|
||||||
// "quizzes"
|
|
||||||
// );
|
|
||||||
// if (!(await directoryExists(quizzesDirectory))) {
|
|
||||||
// await fs.mkdir(quizzesDirectory, { recursive: true });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (const quiz of module.quizzes) {
|
|
||||||
// const previousModule = previouslyStoredCourse?.modules.find(
|
|
||||||
// (m) => m.name === module.name
|
|
||||||
// );
|
|
||||||
// const previousQuiz = previousModule?.quizzes.find((q) => q === quiz);
|
|
||||||
|
|
||||||
// if (!previousQuiz) {
|
|
||||||
// const markdownPath = path.join(quizzesDirectory, `${quiz.name}.md`);
|
|
||||||
// const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
|
||||||
// console.log(`Saving quiz ${markdownPath}`);
|
|
||||||
// await fs.writeFile(markdownPath, quizMarkdown);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await removeOldQuizzes(quizzesDirectory, module);
|
|
||||||
// };
|
|
||||||
|
|
||||||
const saveAssignments = async (
|
|
||||||
course: LocalCourse,
|
|
||||||
module: LocalModule,
|
|
||||||
previouslyStoredCourse?: LocalCourse
|
|
||||||
) => {
|
|
||||||
const assignmentsDirectory = path.join(
|
|
||||||
basePath,
|
|
||||||
course.settings.name,
|
|
||||||
module.name,
|
|
||||||
"assignments"
|
|
||||||
);
|
|
||||||
if (!(await directoryExists(assignmentsDirectory))) {
|
|
||||||
await fs.mkdir(assignmentsDirectory, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const assignment of module.assignments) {
|
|
||||||
const previousModule = previouslyStoredCourse?.modules.find(
|
|
||||||
(m) => m.name === module.name
|
|
||||||
);
|
|
||||||
const previousAssignment = previousModule?.assignments.find(
|
|
||||||
(a) => a === assignment
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!previousAssignment) {
|
|
||||||
const assignmentMarkdown = localAssignmentMarkdown.toMarkdown(assignment);
|
|
||||||
const filePath = path.join(assignmentsDirectory, `${assignment.name}.md`);
|
|
||||||
console.log(`Saving assignment ${filePath}`);
|
|
||||||
await fs.writeFile(filePath, assignmentMarkdown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await removeOldAssignments(assignmentsDirectory, module);
|
|
||||||
};
|
|
||||||
|
|
||||||
const savePages = async (
|
|
||||||
course: LocalCourse,
|
|
||||||
module: LocalModule,
|
|
||||||
previouslyStoredCourse?: LocalCourse
|
|
||||||
) => {
|
|
||||||
const pagesDirectory = path.join(
|
|
||||||
basePath,
|
|
||||||
course.settings.name,
|
|
||||||
module.name,
|
|
||||||
"pages"
|
|
||||||
);
|
|
||||||
if (!(await directoryExists(pagesDirectory))) {
|
|
||||||
await fs.mkdir(pagesDirectory, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const page of module.pages) {
|
|
||||||
const previousModule = previouslyStoredCourse?.modules.find(
|
|
||||||
(m) => m.name === module.name
|
|
||||||
);
|
|
||||||
const previousPage = previousModule?.pages.find((p) => p === page);
|
|
||||||
|
|
||||||
if (!previousPage) {
|
|
||||||
const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
|
|
||||||
const filePath = path.join(pagesDirectory, `${page.name}.md`);
|
|
||||||
console.log(`Saving page ${filePath}`);
|
|
||||||
await fs.writeFile(filePath, pageMarkdown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await removeOldPages(pagesDirectory, module);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeOldQuizzes = async (
|
|
||||||
quizzesDirectory: string,
|
|
||||||
module: LocalModule
|
|
||||||
) => {
|
|
||||||
const existingFiles = await fs.readdir(quizzesDirectory);
|
|
||||||
const quizFilesToDelete = existingFiles.filter((file) => {
|
|
||||||
const quizMarkdownPath = path.join(
|
|
||||||
quizzesDirectory,
|
|
||||||
`${file.replace(".md", "")}.md`
|
|
||||||
);
|
|
||||||
return !module.quizzes.some(
|
|
||||||
(quiz) =>
|
|
||||||
path.join(quizzesDirectory, `${quiz.name}.md`) === quizMarkdownPath
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const file of quizFilesToDelete) {
|
|
||||||
console.log(
|
|
||||||
`Removing old quiz, it has probably been renamed ${path.join(
|
|
||||||
quizzesDirectory,
|
|
||||||
file
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
await fs.unlink(path.join(quizzesDirectory, file));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeOldAssignments = async (
|
|
||||||
assignmentsDirectory: string,
|
|
||||||
module: LocalModule
|
|
||||||
) => {
|
|
||||||
const existingFiles = await fs.readdir(assignmentsDirectory);
|
|
||||||
const assignmentFilesToDelete = existingFiles.filter((file) => {
|
|
||||||
const assignmentMarkdownPath = path.join(
|
|
||||||
assignmentsDirectory,
|
|
||||||
`${file.replace(".md", "")}.md`
|
|
||||||
);
|
|
||||||
return !module.assignments.some(
|
|
||||||
(assignment) =>
|
|
||||||
path.join(assignmentsDirectory, `${assignment.name}.md`) ===
|
|
||||||
assignmentMarkdownPath
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const file of assignmentFilesToDelete) {
|
|
||||||
console.log(
|
|
||||||
`Removing old assignment, it has probably been renamed ${path.join(
|
|
||||||
assignmentsDirectory,
|
|
||||||
file
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
await fs.unlink(path.join(assignmentsDirectory, file));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeOldPages = async (pagesDirectory: string, module: LocalModule) => {
|
|
||||||
const existingFiles = await fs.readdir(pagesDirectory);
|
|
||||||
const pageFilesToDelete = existingFiles.filter((file) => {
|
|
||||||
const pageMarkdownPath = path.join(
|
|
||||||
pagesDirectory,
|
|
||||||
`${file.replace(".md", "")}.md`
|
|
||||||
);
|
|
||||||
return !module.pages.some(
|
|
||||||
(page) =>
|
|
||||||
path.join(pagesDirectory, `${page.name}.md`) === pageMarkdownPath
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const file of pageFilesToDelete) {
|
|
||||||
console.log(
|
|
||||||
`Removing old page, it has probably been renamed ${path.join(
|
|
||||||
pagesDirectory,
|
|
||||||
file
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
await fs.unlink(path.join(pagesDirectory, file));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// export const courseMarkdownSaver = {
|
|
||||||
// async save(course: LocalCourse, previouslyStoredCourse?: LocalCourse) {
|
|
||||||
// const courseDirectory = path.join(basePath, course.settings.name);
|
|
||||||
// if (!(await directoryExists(courseDirectory))) {
|
|
||||||
// await fs.mkdir(courseDirectory, { recursive: true });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await saveSettings(course, courseDirectory);
|
|
||||||
// await saveModules(course, courseDirectory, previouslyStoredCourse);
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
30
nextjs/src/services/fileStorage/utils/lectureUtils.ts
Normal file
30
nextjs/src/services/fileStorage/utils/lectureUtils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { extractLabelValue } from "@/models/local/assignment/utils/markdownUtils";
|
||||||
|
import { Lecture } from "@/models/local/lecture";
|
||||||
|
|
||||||
|
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")[1].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"
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import { lectureToString } from "../fileStorage/utils/lectureUtils";
|
||||||
lectureToString,
|
import { parseLecture } from "../fileStorage/utils/lectureUtils";
|
||||||
parseLecture,
|
|
||||||
} from "../fileStorage/lectureFileStorageService";
|
|
||||||
import { Lecture } from "@/models/local/lecture";
|
import { Lecture } from "@/models/local/lecture";
|
||||||
|
|
||||||
describe("can parse and stringify lectures", () => {
|
describe("can parse and stringify lectures", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user