mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
more refactor
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
@@ -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");
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { procedure } from "../trpcSetup";
|
||||
import { procedure } from "./trpcSetup";
|
||||
|
||||
const publicProcedure = procedure;
|
||||
|
||||
@@ -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({
|
||||
@@ -1,4 +1,4 @@
|
||||
import publicProcedure from "../procedures/public";
|
||||
import publicProcedure from "../publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../trpcSetup";
|
||||
import {
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
});
|
||||
@@ -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);
|
||||
}),
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -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>();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user