more refactor

This commit is contained in:
2025-07-23 09:54:11 -06:00
parent 3e371247d6
commit 1885431574
69 changed files with 158 additions and 142 deletions

View File

@@ -5,7 +5,7 @@ import { axiosClient } from "../axiosUtils";
import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
import { CanvasRubricCreationResponse } from "@/models/canvas/assignments/canvasRubricCreationResponse";
import { assignmentPoints } from "@/features/local/assignments/models/utils/assignmentPointsUtils";
import { getDateFromString } from "@/models/local/utils/timeUtils";
import { getDateFromString } from "@/features/local/utils/timeUtils";
import { getRubricCriterion } from "./canvasRubricUtils";
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";

View File

@@ -2,12 +2,15 @@ import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
import { axiosClient } from "../axiosUtils";
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
import { canvasAssignmentService } from "./canvasAssignmentService";
import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel";
import { escapeMatchingText } from "../utils/questionHtmlUtils";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { LocalQuizQuestion, QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import {
LocalQuizQuestion,
QuestionType,
} from "@/features/local/quizzes/models/localQuizQuestion";
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
export const getAnswers = (

View File

@@ -1,83 +0,0 @@
import { promises as fs } from "fs";
import path from "path";
import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils";
import { quizFileStorageService } from "../../features/local/quizzes/quizFileStorageService";
import { pageFileStorageService } from "../../features/local/pages/pageFileStorageService";
import { moduleFileStorageService } from "../../features/local/modules/moduleFileStorageService";
import { settingsFileStorageService } from "../../features/local/course/settingsFileStorageService";
import { getCoursePathByName } from "./globalSettingsFileStorageService";
import { assignmentsFileStorageService } from "@/features/local/assignments/assignmentsFileStorageService";
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 = await getCoursePathByName(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 });
},
async getDirectoryContents(
relativePath: string
): Promise<{ files: string[]; folders: string[] }> {
const fullPath = path.join(basePath, relativePath);
// Security: ensure fullPath is inside basePath
const resolvedBase = path.resolve(basePath);
const resolvedFull = path.resolve(fullPath);
if (!resolvedFull.startsWith(resolvedBase)) {
return { files: [], folders: [] };
}
if (!(await directoryOrFileExists(fullPath))) {
throw new Error(`Directory ${fullPath} does not exist`);
}
const contents = await fs.readdir(fullPath, { withFileTypes: true });
const files: string[] = [];
const folders: string[] = [];
for (const dirent of contents) {
if (dirent.isDirectory()) {
folders.push(dirent.name);
} else if (dirent.isFile()) {
files.push(dirent.name);
}
}
return { files, folders };
},
};

View File

@@ -1,50 +0,0 @@
import {
GlobalSettings,
zodGlobalSettings,
} from "@/models/local/globalSettings";
import {
globalSettingsToYaml,
parseGlobalSettingsYaml,
} from "@/models/local/globalSettingsUtils";
import { promises as fs } from "fs";
import path from "path";
import { basePath } from "./utils/fileSystemUtils";
const SETTINGS_FILE_PATH =
process.env.SETTINGS_FILE_PATH || "./globalSettings.yml";
export const getGlobalSettings = async (): Promise<GlobalSettings> => {
try {
await fs.access(SETTINGS_FILE_PATH);
} catch (err) {
console.log(err);
throw new Error(
`Global Settings file does not exist at path: ${SETTINGS_FILE_PATH}`
);
}
const globalSettingsString = process.env.GLOBAL_SETTINGS
? process.env.GLOBAL_SETTINGS
: await fs.readFile(SETTINGS_FILE_PATH, "utf-8");
const globalSettings = parseGlobalSettingsYaml(globalSettingsString);
return globalSettings;
};
export const getCoursePathByName = async (courseName: string) => {
const globalSettings = await getGlobalSettings();
const course = globalSettings.courses.find((c) => c.name === courseName);
if (!course) {
throw new Error(
`Course with name ${courseName} not found in global settings`
);
}
return path.join(basePath, course.path);
};
export const updateGlobalSettings = async (globalSettings: GlobalSettings) => {
const globalSettingsString = globalSettingsToYaml(
zodGlobalSettings.parse(globalSettings)
);
await fs.writeFile(SETTINGS_FILE_PATH, globalSettingsString, "utf-8");
};

View File

@@ -1,5 +1,5 @@
import { promises as fs } from "fs";
import { getGlobalSettings } from "../globalSettingsFileStorageService";
import { getGlobalSettings } from "../../../features/local/globalSettings/globalSettingsFileStorageService";
export const directoryOrFileExists = async (
directoryPath: string

View File

@@ -1,7 +1,7 @@
import { getWeekNumber } from "@/app/course/[courseName]/calendar/calendarMonthUtils";
import { extractLabelValue } from "@/features/local/assignments/models/utils/markdownUtils";
import { Lecture } from "@/features/local/lectures/lectureModel";
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
export function parseLecture(fileContent: string): Lecture {
try {

View File

@@ -1,4 +1,4 @@
import { procedure } from "../trpcSetup";
import { procedure } from "./trpcSetup";
const publicProcedure = procedure;

View File

@@ -2,12 +2,12 @@ import { createTrpcContext } from "../context";
import { createCallerFactory, router } from "../trpcSetup";
import { assignmentRouter } from "../../../features/local/assignments/assignmentRouter";
import { canvasFileRouter } from "./canvasFileRouter";
import { directoriesRouter } from "./directoriesRouter";
import { globalSettingsRouter } from "./globalSettingsRouter";
import { directoriesRouter } from "../../../features/local/utils/directoriesRouter";
import { globalSettingsRouter } from "../../../features/local/globalSettings/globalSettingsRouter";
import { lectureRouter } from "../../../features/local/lectures/lectureRouter";
import { pageRouter } from "../../../features/local/pages/pageRouter";
import { quizRouter } from "../../../features/local/quizzes/quizRouter";
import { settingsRouter } from "./settingsRouter";
import { settingsRouter } from "../../../features/local/course/settingsRouter";
import { moduleRouter } from "@/features/local/modules/moduleRouter";
export const trpcAppRouter = router({

View File

@@ -1,4 +1,4 @@
import publicProcedure from "../procedures/public";
import publicProcedure from "../publicProcedure";
import { z } from "zod";
import { router } from "../trpcSetup";
import {

View File

@@ -1,28 +0,0 @@
import z from "zod";
import publicProcedure from "../procedures/public";
import { router } from "../trpcSetup";
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
export const directoriesRouter = router({
getEmptyDirectories: publicProcedure.query(async () => {
return await fileStorageService.getEmptyDirectories();
}),
getDirectoryContents: publicProcedure
.input(
z.object({
relativePath: z.string(),
})
)
.query(async ({ input: { relativePath } }) => {
return await fileStorageService.getDirectoryContents(relativePath);
}),
directoryIsCourse: publicProcedure
.input(
z.object({
folderPath: z.string(),
})
)
.query(async ({ input: { folderPath } }) => {
return await fileStorageService.settings.folderIsCourse(folderPath);
}),
});

View File

@@ -1,23 +0,0 @@
import { zodGlobalSettings } from "@/models/local/globalSettings";
import { router } from "../trpcSetup";
import z from "zod";
import publicProcedure from "../procedures/public";
import {
getGlobalSettings,
updateGlobalSettings,
} from "@/services/fileStorage/globalSettingsFileStorageService";
export const globalSettingsRouter = router({
getGlobalSettings: publicProcedure.query(async () => {
return await getGlobalSettings();
}),
updateGlobalSettings: publicProcedure
.input(
z.object({
globalSettings: zodGlobalSettings,
})
)
.mutation(async ({ input: { globalSettings } }) => {
return await updateGlobalSettings(globalSettings);
}),
});

View File

@@ -1,170 +0,0 @@
import publicProcedure from "../procedures/public";
import { z } from "zod";
import { router } from "../trpcSetup";
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
import {
prepAssignmentForNewSemester,
prepLectureForNewSemester,
prepPageForNewSemester,
prepQuizForNewSemester,
} from "@/models/local/utils/semesterTransferUtils";
import {
getGlobalSettings,
updateGlobalSettings,
} from "@/services/fileStorage/globalSettingsFileStorageService";
import { getLectures, updateLecture } from "@/features/local/lectures/lectureFileStorageService";
import { zodLocalCourseSettings } from "@/features/local/course/localCourseSettings";
export const settingsRouter = router({
allCoursesSettings: publicProcedure.query(async () => {
return await fileStorageService.settings.getAllCoursesSettings();
}),
courseSettings: publicProcedure
.input(
z.object({
courseName: z.string(),
})
)
.query(async ({ input: { courseName } }) => {
const settingsList =
await fileStorageService.settings.getAllCoursesSettings();
const s = settingsList.find((s) => s.name === courseName);
if (!s) {
console.log(courseName, settingsList);
throw Error("Could not find settings for course " + courseName);
}
return s;
}),
createCourse: publicProcedure
.input(
z.object({
name: z.string(),
directory: z.string(),
settings: zodLocalCourseSettings,
settingsFromCourseToImport: zodLocalCourseSettings.optional(),
})
)
.mutation(
async ({
input: { settings, settingsFromCourseToImport, name, directory },
}) => {
console.log("creating in directory", directory);
await fileStorageService.settings.createCourseSettings(
settings,
directory
);
const globalSettings = await getGlobalSettings();
await updateGlobalSettings({
...globalSettings,
courses: [
...globalSettings.courses,
{
name,
path: directory,
},
],
});
if (settingsFromCourseToImport) {
const oldCourseName = settingsFromCourseToImport.name;
const newCourseName = settings.name;
const oldModules = await fileStorageService.modules.getModuleNames(
oldCourseName
);
await Promise.all(
oldModules.map(async (moduleName) => {
await fileStorageService.modules.createModule(
newCourseName,
moduleName
);
const [oldAssignments, oldQuizzes, oldPages, oldLecturesByWeek] =
await Promise.all([
fileStorageService.assignments.getAssignments(
oldCourseName,
moduleName
),
await fileStorageService.quizzes.getQuizzes(
oldCourseName,
moduleName
),
await fileStorageService.pages.getPages(
oldCourseName,
moduleName
),
await getLectures(oldCourseName),
]);
await Promise.all([
...oldAssignments.map(async (oldAssignment) => {
const newAssignment = prepAssignmentForNewSemester(
oldAssignment,
settingsFromCourseToImport.startDate,
settings.startDate
);
await fileStorageService.assignments.updateOrCreateAssignment(
{
courseName: newCourseName,
moduleName,
assignmentName: newAssignment.name,
assignment: newAssignment,
}
);
}),
...oldQuizzes.map(async (oldQuiz) => {
const newQuiz = prepQuizForNewSemester(
oldQuiz,
settingsFromCourseToImport.startDate,
settings.startDate
);
await fileStorageService.quizzes.updateQuiz({
courseName: newCourseName,
moduleName,
quizName: newQuiz.name,
quiz: newQuiz,
});
}),
...oldPages.map(async (oldPage) => {
const newPage = prepPageForNewSemester(
oldPage,
settingsFromCourseToImport.startDate,
settings.startDate
);
await fileStorageService.pages.updatePage({
courseName: newCourseName,
moduleName,
pageName: newPage.name,
page: newPage,
});
}),
...oldLecturesByWeek.flatMap(async (oldLectureByWeek) =>
oldLectureByWeek.lectures.map(async (oldLecture) => {
const newLecture = prepLectureForNewSemester(
oldLecture,
settingsFromCourseToImport.startDate,
settings.startDate
);
await updateLecture(newCourseName, settings, newLecture);
})
),
]);
})
);
}
}
),
updateSettings: publicProcedure
.input(
z.object({
settings: zodLocalCourseSettings,
})
)
.mutation(async ({ input: { settings } }) => {
await fileStorageService.settings.updateCourseSettings(
settings.name,
settings
);
}),
});

View File

@@ -1,7 +1,8 @@
import { createTRPCReact } from "@trpc/react-query";
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { AppRouter } from "./router/app";
import { createTRPCContext } from "@trpc/tanstack-react-query";
import { AppRouter } from "./router/appRouter";
export const trpc = createTRPCReact<AppRouter>();
export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<AppRouter>();
export const { TRPCProvider, useTRPC, useTRPCClient } =
createTRPCContext<AppRouter>();

View File

@@ -1,7 +1,10 @@
import { describe, it, expect, beforeEach } from "vitest";
import { promises as fs } from "fs";
import { fileStorageService } from "../fileStorage/fileStorageService";
import { LocalCourseSettings, DayOfWeek } from "@/features/local/course/localCourseSettings";
import { fileStorageService } from "../../features/local/utils/fileStorageService";
import {
LocalCourseSettings,
DayOfWeek,
} from "@/features/local/course/localCourseSettings";
describe("FileStorageTests", () => {
beforeEach(async () => {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach } from "vitest";
import { promises as fs } from "fs";
import { fileStorageService } from "../fileStorage/fileStorageService";
import { fileStorageService } from "../../features/local/utils/fileStorageService";
import { basePath } from "../fileStorage/utils/fileSystemUtils";
describe("FileStorageTests", () => {