mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 23:58:31 -06:00
workign on test folders
This commit is contained in:
1
nextjs/.env.test
Normal file
1
nextjs/.env.test
Normal file
@@ -0,0 +1 @@
|
|||||||
|
STORAGE_DIRECTORY="/tmp/canvasManagerStorage"
|
||||||
4
nextjs/.gitignore
vendored
4
nextjs/.gitignore
vendored
@@ -34,3 +34,7 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
|
||||||
|
storage/
|
||||||
|
temp/
|
||||||
4
nextjs/package-lock.json
generated
4
nextjs/package-lock.json
generated
@@ -12,7 +12,8 @@
|
|||||||
"next": "14.2.5",
|
"next": "14.2.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hot-toast": "^2.4.1"
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"yaml": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
@@ -7257,7 +7258,6 @@
|
|||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
||||||
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"next": "14.2.5",
|
"next": "14.2.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hot-toast": "^2.4.1"
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"yaml": "^2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { AssignmentSubmissionType } from "./assignmentSubmissionType";
|
import { AssignmentSubmissionType } from "./assignmentSubmissionType";
|
||||||
import { RubricItem } from "./rubricItem";
|
import { RubricItem } from "./rubricItem";
|
||||||
|
import { assignmentMarkdownParser } from "./utils/assignmentMarkdownParser";
|
||||||
|
import { assignmentMarkdownSerializer } from "./utils/assignmentMarkdownSerializer";
|
||||||
|
|
||||||
export interface LocalAssignment {
|
export interface LocalAssignment {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -11,3 +13,8 @@ export interface LocalAssignment {
|
|||||||
allowedFileUploadExtensions: string[];
|
allowedFileUploadExtensions: string[];
|
||||||
rubric: RubricItem[];
|
rubric: RubricItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const localAssignmentMarkdown = {
|
||||||
|
parseMarkdown: assignmentMarkdownParser.parseMarkdown,
|
||||||
|
toMarkdown: assignmentMarkdownSerializer.toMarkdown,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LocalAssignmentGroup } from "./assignmnet/localAssignmentGroup";
|
import { LocalAssignmentGroup } from "./assignmnet/localAssignmentGroup";
|
||||||
import { LocalModule } from "./localModules";
|
import { LocalModule } from "./localModules";
|
||||||
|
import { parse, stringify } from "yaml";
|
||||||
|
|
||||||
export interface LocalCourse {
|
export interface LocalCourse {
|
||||||
modules: LocalModule[];
|
modules: LocalModule[];
|
||||||
@@ -30,14 +31,11 @@ export enum DayOfWeek {
|
|||||||
Friday = "Friday",
|
Friday = "Friday",
|
||||||
Saturday = "Saturday",
|
Saturday = "Saturday",
|
||||||
}
|
}
|
||||||
|
export const localCourseYamlUtils = {
|
||||||
// export const LocalCourseSettingsUtils = {
|
parseSettingYaml: (settingsString: string): LocalCourseSettings => {
|
||||||
// toYaml(settings: LocalCourseSettings): string {
|
return parse(settingsString);
|
||||||
// return dump(settings, { noRefs: true });
|
},
|
||||||
// },
|
settingsToYaml: (settings: LocalCourseSettings) => {
|
||||||
|
return stringify(settings);
|
||||||
// parseYaml(rawText: string): LocalCourseSettings {
|
},
|
||||||
// const settings = load(rawText) as LocalCourseSettings;
|
};
|
||||||
// return createLocalCourseSettings(settings);
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { extractLabelValue } from "../assignmnet/utils/markdownUtils";
|
||||||
import { IModuleItem } from "../IModuleItem";
|
import { IModuleItem } from "../IModuleItem";
|
||||||
|
|
||||||
export interface LocalCoursePage extends IModuleItem {
|
export interface LocalCoursePage extends IModuleItem {
|
||||||
@@ -5,3 +6,33 @@ export interface LocalCoursePage extends IModuleItem {
|
|||||||
text: string;
|
text: string;
|
||||||
dueAt: string;
|
dueAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const localPageMarkdownUtils = {
|
||||||
|
toMarkdown: (page: LocalCoursePage) => {
|
||||||
|
const printableDueDate = new Date(page.dueAt)
|
||||||
|
.toISOString()
|
||||||
|
.replace("\u202F", " ");
|
||||||
|
const settingsMarkdown = `Name: ${page.name}\nDueDateForOrdering: ${printableDueDate}\n---\n`;
|
||||||
|
return settingsMarkdown + page.text;
|
||||||
|
},
|
||||||
|
|
||||||
|
parseMarkdown: (pageMarkdown: string) => {
|
||||||
|
const rawSettings = pageMarkdown.split("---")[0];
|
||||||
|
const name = extractLabelValue(rawSettings, "Name");
|
||||||
|
const rawDate = extractLabelValue(rawSettings, "DueDateForOrdering");
|
||||||
|
|
||||||
|
const parsedDate = new Date(rawDate);
|
||||||
|
if (isNaN(parsedDate.getTime())) {
|
||||||
|
throw new Error(`could not parse due date: ${rawDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = pageMarkdown.split("---\n")[1];
|
||||||
|
|
||||||
|
const page: LocalCoursePage = {
|
||||||
|
name,
|
||||||
|
dueAt: parsedDate.toISOString(),
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
return page;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { extractLabelValue } from "../assignmnet/utils/markdownUtils";
|
|
||||||
import { LocalCoursePage } from "./localCoursePage";
|
|
||||||
|
|
||||||
export const pageMarkdownUtils = {
|
|
||||||
toMarkdown: (page: LocalCoursePage) => {
|
|
||||||
const printableDueDate = new Date(page.dueAt)
|
|
||||||
.toISOString()
|
|
||||||
.replace("\u202F", " ");
|
|
||||||
const settingsMarkdown = `Name: ${page.name}\nDueDateForOrdering: ${printableDueDate}\n---\n`;
|
|
||||||
return settingsMarkdown + page.text;
|
|
||||||
},
|
|
||||||
|
|
||||||
parseMarkdown: (pageMarkdown: string) => {
|
|
||||||
const rawSettings = pageMarkdown.split("---")[0];
|
|
||||||
const name = extractLabelValue(rawSettings, "Name");
|
|
||||||
const rawDate = extractLabelValue(rawSettings, "DueDateForOrdering");
|
|
||||||
|
|
||||||
const parsedDate = new Date(rawDate);
|
|
||||||
if (isNaN(parsedDate.getTime())) {
|
|
||||||
throw new Error(`could not parse due date: ${rawDate}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = pageMarkdown.split("---\n")[1];
|
|
||||||
|
|
||||||
const page: LocalCoursePage = {
|
|
||||||
name,
|
|
||||||
dueAt: parsedDate.toISOString(),
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
return page;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LocalQuizQuestion } from "./localQuizQuestion";
|
import { LocalQuizQuestion } from "./localQuizQuestion";
|
||||||
|
import { quizMarkdownUtils } from "./utils/quizMarkdownUtils";
|
||||||
|
|
||||||
export interface LocalQuiz {
|
export interface LocalQuiz {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,3 +14,8 @@ export interface LocalQuiz {
|
|||||||
allowedAttempts: number;
|
allowedAttempts: number;
|
||||||
questions: LocalQuizQuestion[];
|
questions: LocalQuizQuestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const localQuizMarkdownUtils = {
|
||||||
|
parseMarkdown: quizMarkdownUtils.parseMarkdown,
|
||||||
|
toMarkdown: quizMarkdownUtils.toMarkdown,
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
export const getDateFromString = (value: string) => {
|
export const getDateFromString = (value: string) => {
|
||||||
// may need to check for other formats
|
// may need to check for other formats
|
||||||
const validDateRegex =
|
const validDateRegex =
|
||||||
/([1-9][1-9]|[0-2])\/(0[1-9]|[1-2][0-9]|3[01])\/\d{4} (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/;
|
/\d{2}\/\d{2}\/\d{4} [0-2][0-9]|[0-5][0-9]|[0-2][0-9]:[0-5][0-9]:[0-5][0-9]/;
|
||||||
if (!validDateRegex.test(value)) {
|
if (!validDateRegex.test(value)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
55
nextjs/src/services/fileStorage/fileStorageService.ts
Normal file
55
nextjs/src/services/fileStorage/fileStorageService.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { LocalCourse } from "@/models/local/localCourse";
|
||||||
|
import { courseMarkdownLoader } from "./utils/couresMarkdownLoader";
|
||||||
|
import { courseMarkdownSaver } from "./utils/courseMarkdownSaver";
|
||||||
|
|
||||||
|
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
|
||||||
|
|
||||||
|
export const fileStorageService = {
|
||||||
|
async saveCourseAsync(
|
||||||
|
course: LocalCourse,
|
||||||
|
previouslyStoredCourse?: LocalCourse
|
||||||
|
) {
|
||||||
|
await courseMarkdownSaver.save(course, previouslyStoredCourse);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSavedCourses(): Promise<LocalCourse[]> {
|
||||||
|
console.log("loading pages from file system");
|
||||||
|
return (await courseMarkdownLoader.loadSavedCourses()) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getEmptyDirectories(): Promise<string[]> {
|
||||||
|
if (!(await this.directoryExists(basePath))) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot get empty directories, ${basePath} does not exist`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const directories = await fs.readdir(basePath, { withFileTypes: true });
|
||||||
|
const emptyDirectories = directories
|
||||||
|
.filter((dirent) => dirent.isDirectory())
|
||||||
|
.map((dirent) => path.join(basePath, dirent.name))
|
||||||
|
.filter(async (dir) => !(await this.hasFileSystemEntries(dir)));
|
||||||
|
|
||||||
|
return emptyDirectories;
|
||||||
|
},
|
||||||
|
|
||||||
|
async directoryExists(directoryPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(directoryPath);
|
||||||
|
return stat.isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async hasFileSystemEntries(directoryPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(directoryPath);
|
||||||
|
return entries.length > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
181
nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts
Normal file
181
nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { LocalAssignment, localAssignmentMarkdown } from "@/models/local/assignmnet/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";
|
||||||
|
|
||||||
|
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 this.fileExists(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 this.directoryExists(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 this.fileExists(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 this.directoryExists(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 this.directoryExists(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 this.directoryExists(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);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async directoryExists(directoryPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(directoryPath);
|
||||||
|
return stat.isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
245
nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts
Normal file
245
nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { localAssignmentMarkdown } from "@/models/local/assignmnet/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 {
|
||||||
|
const stat = await fs.stat(directoryPath);
|
||||||
|
return stat.isDirectory();
|
||||||
|
} 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { LocalCourse } from "@/models/local/localCourse";
|
import { LocalCourse } from "@/models/local/localCourse";
|
||||||
import { CourseDifferences } from "../fileStorage/courseDifferences";
|
import { CourseDifferences } from "../fileStorage/utils/courseDifferences";
|
||||||
import { AssignmentSubmissionType } from "@/models/local/assignmnet/assignmentSubmissionType";
|
import { AssignmentSubmissionType } from "@/models/local/assignmnet/assignmentSubmissionType";
|
||||||
|
|
||||||
describe("CourseDifferencesChangesTests", () => {
|
describe("CourseDifferencesChangesTests", () => {
|
||||||
@@ -124,7 +124,9 @@ describe("CourseDifferencesChangesTests", () => {
|
|||||||
|
|
||||||
expect(differences.modules).not.toBeNull();
|
expect(differences.modules).not.toBeNull();
|
||||||
expect(differences.modules).toHaveLength(1);
|
expect(differences.modules).toHaveLength(1);
|
||||||
expect(differences.modules?.[0].assignments?.[0].description).toBe("new description");
|
expect(differences.modules?.[0].assignments?.[0].description).toBe(
|
||||||
|
"new description"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can properly ignore unchanged modules", () => {
|
it("can properly ignore unchanged modules", () => {
|
||||||
@@ -241,7 +243,9 @@ describe("CourseDifferencesChangesTests", () => {
|
|||||||
|
|
||||||
expect(differences.modules).toHaveLength(1);
|
expect(differences.modules).toHaveLength(1);
|
||||||
expect(differences.modules?.[0].assignments).toHaveLength(1);
|
expect(differences.modules?.[0].assignments).toHaveLength(1);
|
||||||
expect(differences.modules?.[0].assignments?.[0].name).toBe("test assignment 2 with a new name");
|
expect(differences.modules?.[0].assignments?.[0].name).toBe(
|
||||||
|
"test assignment 2 with a new name"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("identical quizzes ignored", () => {
|
it("identical quizzes ignored", () => {
|
||||||
@@ -349,7 +353,9 @@ describe("CourseDifferencesChangesTests", () => {
|
|||||||
|
|
||||||
expect(differences.modules).toHaveLength(1);
|
expect(differences.modules).toHaveLength(1);
|
||||||
expect(differences.modules?.[0].quizzes).toHaveLength(1);
|
expect(differences.modules?.[0].quizzes).toHaveLength(1);
|
||||||
expect(differences.modules?.[0].quizzes?.[0].lockAt).toBe("12/31/9999 23:59:59");
|
expect(differences.modules?.[0].quizzes?.[0].lockAt).toBe(
|
||||||
|
"12/31/9999 23:59:59"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can detect only different quiz when other quizzes stay", () => {
|
it("can detect only different quiz when other quizzes stay", () => {
|
||||||
@@ -515,7 +521,9 @@ describe("CourseDifferencesChangesTests", () => {
|
|||||||
|
|
||||||
expect(differences.modules).toHaveLength(1);
|
expect(differences.modules).toHaveLength(1);
|
||||||
expect(differences.modules?.[0].pages).toHaveLength(1);
|
expect(differences.modules?.[0].pages).toHaveLength(1);
|
||||||
expect(differences.modules?.[0].pages?.[0].text).toBe("test description changed");
|
expect(differences.modules?.[0].pages?.[0].text).toBe(
|
||||||
|
"test description changed"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("different page detected but not same page", () => {
|
it("different page detected but not same page", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { LocalCourse } from "@/models/local/localCourse";
|
import { LocalCourse } from "@/models/local/localCourse";
|
||||||
import { CourseDifferences } from "../fileStorage/courseDifferences";
|
import { CourseDifferences } from "../fileStorage/utils/courseDifferences";
|
||||||
|
|
||||||
describe("CourseDifferencesDeletionsTests", () => {
|
describe("CourseDifferencesDeletionsTests", () => {
|
||||||
it("same module does not get deleted", () => {
|
it("same module does not get deleted", () => {
|
||||||
@@ -37,7 +37,10 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse);
|
const differences = CourseDifferences.getDeletedChanges(
|
||||||
|
newCourse,
|
||||||
|
oldCourse
|
||||||
|
);
|
||||||
|
|
||||||
expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(0);
|
expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(0);
|
||||||
});
|
});
|
||||||
@@ -76,7 +79,10 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse);
|
const differences = CourseDifferences.getDeletedChanges(
|
||||||
|
newCourse,
|
||||||
|
oldCourse
|
||||||
|
);
|
||||||
|
|
||||||
expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(1);
|
expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(1);
|
||||||
expect(differences.namesOfModulesToDeleteCompletely[0]).toBe("test module");
|
expect(differences.namesOfModulesToDeleteCompletely[0]).toBe("test module");
|
||||||
@@ -105,7 +111,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quizzes: [],
|
quizzes: [],
|
||||||
@@ -125,7 +131,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quizzes: [],
|
quizzes: [],
|
||||||
@@ -134,7 +140,10 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse);
|
const differences = CourseDifferences.getDeletedChanges(
|
||||||
|
newCourse,
|
||||||
|
oldCourse
|
||||||
|
);
|
||||||
|
|
||||||
expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(0);
|
expect(differences.namesOfModulesToDeleteCompletely).toHaveLength(0);
|
||||||
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
||||||
@@ -167,7 +176,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quizzes: [],
|
quizzes: [],
|
||||||
@@ -187,7 +196,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quizzes: [],
|
quizzes: [],
|
||||||
@@ -196,7 +205,10 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse);
|
const differences = CourseDifferences.getDeletedChanges(
|
||||||
|
newCourse,
|
||||||
|
oldCourse
|
||||||
|
);
|
||||||
|
|
||||||
expect(differences.deleteContentsOfModule).toHaveLength(0);
|
expect(differences.deleteContentsOfModule).toHaveLength(0);
|
||||||
});
|
});
|
||||||
@@ -224,7 +236,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test assignment 2",
|
name: "test assignment 2",
|
||||||
@@ -232,7 +244,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quizzes: [],
|
quizzes: [],
|
||||||
@@ -252,7 +264,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test assignment 2 changed",
|
name: "test assignment 2 changed",
|
||||||
@@ -260,7 +272,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
dueAt: "09/07/2024 23:59:00",
|
dueAt: "09/07/2024 23:59:00",
|
||||||
submissionTypes: [],
|
submissionTypes: [],
|
||||||
allowedFileUploadExtensions: [],
|
allowedFileUploadExtensions: [],
|
||||||
rubric: []
|
rubric: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quizzes: [],
|
quizzes: [],
|
||||||
@@ -269,7 +281,10 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse);
|
const differences = CourseDifferences.getDeletedChanges(
|
||||||
|
newCourse,
|
||||||
|
oldCourse
|
||||||
|
);
|
||||||
|
|
||||||
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
||||||
expect(differences.deleteContentsOfModule[0].assignments).toHaveLength(1);
|
expect(differences.deleteContentsOfModule[0].assignments).toHaveLength(1);
|
||||||
@@ -304,7 +319,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
showCorrectAnswers: false,
|
showCorrectAnswers: false,
|
||||||
oneQuestionAtATime: false,
|
oneQuestionAtATime: false,
|
||||||
allowedAttempts: 0,
|
allowedAttempts: 0,
|
||||||
questions: []
|
questions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Quiz 2",
|
name: "Test Quiz 2",
|
||||||
@@ -314,7 +329,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
showCorrectAnswers: false,
|
showCorrectAnswers: false,
|
||||||
oneQuestionAtATime: false,
|
oneQuestionAtATime: false,
|
||||||
allowedAttempts: 0,
|
allowedAttempts: 0,
|
||||||
questions: []
|
questions: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pages: [],
|
pages: [],
|
||||||
@@ -336,7 +351,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
showCorrectAnswers: false,
|
showCorrectAnswers: false,
|
||||||
oneQuestionAtATime: false,
|
oneQuestionAtATime: false,
|
||||||
allowedAttempts: 0,
|
allowedAttempts: 0,
|
||||||
questions: []
|
questions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Quiz 3",
|
name: "Test Quiz 3",
|
||||||
@@ -346,7 +361,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
showCorrectAnswers: false,
|
showCorrectAnswers: false,
|
||||||
oneQuestionAtATime: false,
|
oneQuestionAtATime: false,
|
||||||
allowedAttempts: 0,
|
allowedAttempts: 0,
|
||||||
questions: []
|
questions: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pages: [],
|
pages: [],
|
||||||
@@ -354,7 +369,10 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse);
|
const differences = CourseDifferences.getDeletedChanges(
|
||||||
|
newCourse,
|
||||||
|
oldCourse
|
||||||
|
);
|
||||||
|
|
||||||
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
||||||
expect(differences.deleteContentsOfModule[0].quizzes).toHaveLength(1);
|
expect(differences.deleteContentsOfModule[0].quizzes).toHaveLength(1);
|
||||||
@@ -390,7 +408,7 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
{
|
{
|
||||||
name: "Test Page 2",
|
name: "Test Page 2",
|
||||||
text: "test contents",
|
text: "test contents",
|
||||||
dueAt: "09/07/2024 23:59:00"
|
dueAt: "09/07/2024 23:59:00",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -407,19 +425,22 @@ describe("CourseDifferencesDeletionsTests", () => {
|
|||||||
{
|
{
|
||||||
name: "Test Page",
|
name: "Test Page",
|
||||||
text: "test contents",
|
text: "test contents",
|
||||||
dueAt: "09/07/2024 23:59:00"
|
dueAt: "09/07/2024 23:59:00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Page 3",
|
name: "Test Page 3",
|
||||||
text: "test contents",
|
text: "test contents",
|
||||||
dueAt: "09/07/2024 23:59:00"
|
dueAt: "09/07/2024 23:59:00",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const differences = CourseDifferences.getDeletedChanges(newCourse, oldCourse);
|
const differences = CourseDifferences.getDeletedChanges(
|
||||||
|
newCourse,
|
||||||
|
oldCourse
|
||||||
|
);
|
||||||
|
|
||||||
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
expect(differences.deleteContentsOfModule).toHaveLength(1);
|
||||||
expect(differences.deleteContentsOfModule[0].pages).toHaveLength(1);
|
expect(differences.deleteContentsOfModule[0].pages).toHaveLength(1);
|
||||||
|
|||||||
298
nextjs/src/services/tests/fileStorage.test.ts
Normal file
298
nextjs/src/services/tests/fileStorage.test.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import fs from "fs";
|
||||||
|
import { DayOfWeek, LocalCourse } from "@/models/local/localCourse";
|
||||||
|
import { AssignmentSubmissionType } from "@/models/local/assignmnet/assignmentSubmissionType";
|
||||||
|
import { QuestionType } from "@/models/local/quiz/localQuizQuestion";
|
||||||
|
import { fileStorageService } from "../fileStorage/fileStorageService";
|
||||||
|
|
||||||
|
describe("FileStorageTests", () => {
|
||||||
|
let storageDirectory: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const tempDirectory = path.resolve("./temp");
|
||||||
|
storageDirectory = path.join(tempDirectory, "fileStorageTests");
|
||||||
|
console.log(storageDirectory);
|
||||||
|
|
||||||
|
if (fs.existsSync(storageDirectory)) {
|
||||||
|
fs.rmdirSync(storageDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(storageDirectory, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty course can be saved and loaded", async () => {
|
||||||
|
const testCourse: LocalCourse = {
|
||||||
|
settings: {
|
||||||
|
name: "test empty course",
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: [],
|
||||||
|
startDate: "09/07/2024 23:59:00",
|
||||||
|
endDate: "09/07/2024 23:59:00",
|
||||||
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
|
},
|
||||||
|
modules: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
|
const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
|
const loadedCourse = loadedCourses.find(
|
||||||
|
(c) => c.settings.name === testCourse.settings.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadedCourse).toEqual(testCourse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("course settings can be saved and loaded", async () => {
|
||||||
|
const testCourse: LocalCourse = {
|
||||||
|
settings: {
|
||||||
|
assignmentGroups: [],
|
||||||
|
name: "Test Course with settings",
|
||||||
|
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
||||||
|
startDate: "09/07/2024 23:59:00",
|
||||||
|
endDate: "09/07/2024 23:59:00",
|
||||||
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
|
},
|
||||||
|
modules: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
|
const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
|
const loadedCourse = loadedCourses.find(
|
||||||
|
(c) => c.settings.name === testCourse.settings.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadedCourse?.settings).toEqual(testCourse.settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty course modules can be saved and loaded", async () => {
|
||||||
|
const testCourse: LocalCourse = {
|
||||||
|
settings: {
|
||||||
|
name: "Test Course with modules",
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: [],
|
||||||
|
startDate: "09/07/2024 23:59:00",
|
||||||
|
endDate: "09/07/2024 23:59:00",
|
||||||
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: "test module 1",
|
||||||
|
assignments: [],
|
||||||
|
quizzes: [],
|
||||||
|
pages: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
|
const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
|
const loadedCourse = loadedCourses.find(
|
||||||
|
(c) => c.settings.name === testCourse.settings.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadedCourse?.modules).toEqual(testCourse.modules);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("course modules with assignments can be saved and loaded", async () => {
|
||||||
|
const testCourse: LocalCourse = {
|
||||||
|
settings: {
|
||||||
|
name: "Test Course with modules and assignments",
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: [],
|
||||||
|
startDate: "09/07/2024 23:59:00",
|
||||||
|
endDate: "09/07/2024 23:59:00",
|
||||||
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: "test module 1 with assignments",
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
name: "test assignment",
|
||||||
|
description: "here is the description",
|
||||||
|
dueAt: "09/07/2024 23:59:00",
|
||||||
|
lockAt: "09/07/2024 23:59:00",
|
||||||
|
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||||
|
localAssignmentGroupName: "Final Project",
|
||||||
|
rubric: [
|
||||||
|
{ points: 4, label: "do task 1" },
|
||||||
|
{ points: 2, label: "do task 2" },
|
||||||
|
],
|
||||||
|
allowedFileUploadExtensions: []
|
||||||
|
},
|
||||||
|
],
|
||||||
|
quizzes: [],
|
||||||
|
pages: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
|
const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
|
const loadedCourse = loadedCourses.find(
|
||||||
|
(c) => c.settings.name === testCourse.settings.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadedCourse?.modules[0].assignments).toEqual(
|
||||||
|
testCourse.modules[0].assignments
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("course modules with quizzes can be saved and loaded", async () => {
|
||||||
|
const testCourse: LocalCourse = {
|
||||||
|
settings: {
|
||||||
|
name: "Test Course with modules and quiz",
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: [],
|
||||||
|
startDate: "09/07/2024 23:59:00",
|
||||||
|
endDate: "09/07/2024 23:59:00",
|
||||||
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: "test module 1 with quiz",
|
||||||
|
assignments: [],
|
||||||
|
quizzes: [
|
||||||
|
{
|
||||||
|
name: "Test Quiz",
|
||||||
|
description: "quiz description",
|
||||||
|
lockAt: "09/07/2024 12:05:00",
|
||||||
|
dueAt: "09/07/2024 12:05:00",
|
||||||
|
shuffleAnswers: true,
|
||||||
|
oneQuestionAtATime: true,
|
||||||
|
localAssignmentGroupName: "Assignments",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
text: "test essay",
|
||||||
|
questionType: QuestionType.ESSAY,
|
||||||
|
points: 1,
|
||||||
|
answers: [],
|
||||||
|
matchDistractors: []
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showCorrectAnswers: false,
|
||||||
|
allowedAttempts: 0
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pages: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
|
const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
|
const loadedCourse = loadedCourses.find(
|
||||||
|
(c) => c.settings.name === testCourse.settings.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadedCourse?.modules[0].quizzes).toEqual(
|
||||||
|
testCourse.modules[0].quizzes
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("markdown storage fully populated does not lose data", async () => {
|
||||||
|
const testCourse: LocalCourse = {
|
||||||
|
settings: {
|
||||||
|
name: "Test Course with lots of data",
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
||||||
|
startDate: "09/07/2024 23:59:00",
|
||||||
|
endDate: "09/07/2024 23:59:00",
|
||||||
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: "new test module",
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
name: "test assignment",
|
||||||
|
description: "here is the description",
|
||||||
|
dueAt: "09/07/2024 23:59:00",
|
||||||
|
lockAt: "09/07/2024 23:59:00",
|
||||||
|
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||||
|
localAssignmentGroupName: "Final Project",
|
||||||
|
rubric: [
|
||||||
|
{ points: 4, label: "do task 1" },
|
||||||
|
{ points: 2, label: "do task 2" },
|
||||||
|
],
|
||||||
|
allowedFileUploadExtensions: []
|
||||||
|
},
|
||||||
|
],
|
||||||
|
quizzes: [
|
||||||
|
{
|
||||||
|
name: "Test Quiz",
|
||||||
|
description: "quiz description",
|
||||||
|
lockAt: "09/07/2024 23:59:00",
|
||||||
|
dueAt: "09/07/2024 23:59:00",
|
||||||
|
shuffleAnswers: true,
|
||||||
|
oneQuestionAtATime: false,
|
||||||
|
localAssignmentGroupName: "someId",
|
||||||
|
allowedAttempts: -1,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
text: "test short answer",
|
||||||
|
questionType: QuestionType.SHORT_ANSWER,
|
||||||
|
points: 1,
|
||||||
|
answers: [],
|
||||||
|
matchDistractors: []
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showCorrectAnswers: false
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pages: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
|
const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
|
const loadedCourse = loadedCourses.find(
|
||||||
|
(c) => c.settings.name === testCourse.settings.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadedCourse).toEqual(testCourse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("markdown storage can persist pages", async () => {
|
||||||
|
const testCourse: LocalCourse = {
|
||||||
|
settings: {
|
||||||
|
name: "Test Course with page",
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
||||||
|
startDate: "09/07/2024 23:59:00",
|
||||||
|
endDate: "09/07/2024 23:59:00",
|
||||||
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: "page test module",
|
||||||
|
assignments: [],
|
||||||
|
quizzes: [],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
name: "test page persistence",
|
||||||
|
dueAt: "09/07/2024 23:59:00",
|
||||||
|
text: "this is some\n## markdown\n",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
|
const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
|
const loadedCourse = loadedCourses.find(
|
||||||
|
(c) => c.settings.name === testCourse.settings.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadedCourse).toEqual(testCourse);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user