mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
more refactor
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import publicProcedure from "../../../services/serverFunctions/procedures/public";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { zodLocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
||||
|
||||
export const assignmentRouter = router({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { assignmentMarkdownSerializer } from "@/features/local/assignments/model
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { courseItemFileStorageService } from "@/features/local/course/courseItemFileStorageService";
|
||||
import { getCoursePathByName } from "@/services/fileStorage/globalSettingsFileStorageService";
|
||||
import { getCoursePathByName } from "@/features/local/globalSettings/globalSettingsFileStorageService";
|
||||
import { directoryOrFileExists } from "@/services/fileStorage/utils/fileSystemUtils";
|
||||
|
||||
const getAssignmentNames = async (courseName: string, moduleName: string) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
verifyDateOrThrow,
|
||||
verifyDateStringOrUndefined,
|
||||
} from "../../../../../models/local/utils/timeUtils";
|
||||
} from "../../../utils/timeUtils";
|
||||
import { AssignmentSubmissionType } from "../assignmentSubmissionType";
|
||||
import { LocalAssignment } from "../localAssignment";
|
||||
import { RubricItem } from "../rubricItem";
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CourseItemType,
|
||||
typeToFolder,
|
||||
} from "@/features/local/course/courseItemTypes";
|
||||
import { getCoursePathByName } from "../../../services/fileStorage/globalSettingsFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
localPageMarkdownUtils,
|
||||
LocalCoursePage,
|
||||
|
||||
@@ -8,12 +8,12 @@ import { AssignmentSubmissionType } from "@/features/local/assignments/models/as
|
||||
import {
|
||||
getCoursePathByName,
|
||||
getGlobalSettings,
|
||||
} from "../../../services/fileStorage/globalSettingsFileStorageService";
|
||||
import { GlobalSettingsCourse } from "@/models/local/globalSettings";
|
||||
} from "../globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
LocalCourseSettings,
|
||||
localCourseYamlUtils,
|
||||
} from "@/features/local/course/localCourseSettings";
|
||||
import { GlobalSettingsCourse } from "../globalSettings/globalSettingsModels";
|
||||
|
||||
const getCourseSettings = async (
|
||||
course: GlobalSettingsCourse
|
||||
|
||||
173
src/features/local/course/settingsRouter.ts
Normal file
173
src/features/local/course/settingsRouter.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import {
|
||||
prepAssignmentForNewSemester,
|
||||
prepLectureForNewSemester,
|
||||
prepPageForNewSemester,
|
||||
prepQuizForNewSemester,
|
||||
} from "@/features/local/utils/semesterTransferUtils";
|
||||
import {
|
||||
getGlobalSettings,
|
||||
updateGlobalSettings,
|
||||
} from "@/features/local/globalSettings/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
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { basePath } from "../../../services/fileStorage/utils/fileSystemUtils";
|
||||
import {
|
||||
GlobalSettings,
|
||||
zodGlobalSettings,
|
||||
} from "@/features/local/globalSettings/globalSettingsModels";
|
||||
import {
|
||||
parseGlobalSettingsYaml,
|
||||
globalSettingsToYaml,
|
||||
} from "@/features/local/globalSettings/globalSettingsUtils";
|
||||
|
||||
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");
|
||||
};
|
||||
28
src/features/local/globalSettings/globalSettingsHooks.ts
Normal file
28
src/features/local/globalSettings/globalSettingsHooks.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useTRPC } from "@/services/serverFunctions/trpcClient";
|
||||
import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
export const useGlobalSettingsQuery = () => {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.globalSettings.getGlobalSettings.queryOptions());
|
||||
};
|
||||
|
||||
export const useUpdateGlobalSettingsMutation = () => {
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
trpc.globalSettings.updateGlobalSettings.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: trpc.globalSettings.getGlobalSettings.queryKey(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: trpc.settings.allCoursesSettings.queryKey(),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
14
src/features/local/globalSettings/globalSettingsModels.ts
Normal file
14
src/features/local/globalSettings/globalSettingsModels.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import z from "zod";
|
||||
|
||||
export const zodGlobalSettingsCourse = z.object({
|
||||
path: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const zodGlobalSettings = z.object({
|
||||
courses: z.array(zodGlobalSettingsCourse),
|
||||
});
|
||||
|
||||
|
||||
export type GlobalSettings = z.infer<typeof zodGlobalSettings>;
|
||||
export type GlobalSettingsCourse = z.infer<typeof zodGlobalSettingsCourse>;
|
||||
23
src/features/local/globalSettings/globalSettingsRouter.ts
Normal file
23
src/features/local/globalSettings/globalSettingsRouter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import z from "zod";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import {
|
||||
getGlobalSettings,
|
||||
updateGlobalSettings,
|
||||
} from "@/features/local/globalSettings/globalSettingsFileStorageService";
|
||||
import { zodGlobalSettings } from "./globalSettingsModels";
|
||||
|
||||
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);
|
||||
}),
|
||||
});
|
||||
16
src/features/local/globalSettings/globalSettingsUtils.ts
Normal file
16
src/features/local/globalSettings/globalSettingsUtils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GlobalSettings, zodGlobalSettings } from "./globalSettingsModels";
|
||||
import { parse, stringify } from "yaml";
|
||||
|
||||
export const globalSettingsToYaml = (settings: GlobalSettings) => {
|
||||
return stringify(settings);
|
||||
};
|
||||
|
||||
export const parseGlobalSettingsYaml = (yaml: string): GlobalSettings => {
|
||||
const parsed = parse(yaml);
|
||||
try {
|
||||
return zodGlobalSettings.parse(parsed);
|
||||
} catch (e) {
|
||||
console.error("Error parsing global settings YAML:", e);
|
||||
throw new Error(`Error parsing global settings, got ${yaml}, ${e}`);
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,18 @@
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { Lecture } from "@/features/local/lectures/lectureModel";
|
||||
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
|
||||
import { getCoursePathByName } from "@/services/fileStorage/globalSettingsFileStorageService";
|
||||
import { getDateFromStringOrThrow } from "@/features/local/utils/timeUtils";
|
||||
import { getCoursePathByName } from "@/features/local/globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
lectureFolderName,
|
||||
parseLecture,
|
||||
getLectureWeekName,
|
||||
lectureToString,
|
||||
} from "@/services/fileStorage/utils/lectureUtils";
|
||||
import { LocalCourseSettings, getDayOfWeek } from "../course/localCourseSettings";
|
||||
import {
|
||||
LocalCourseSettings,
|
||||
getDayOfWeek,
|
||||
} from "../course/localCourseSettings";
|
||||
|
||||
export async function getLectures(courseName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import publicProcedure from "../../../services/serverFunctions/procedures/public";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { zodLecture } from "@/features/local/lectures/lectureModel";
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CalendarItemsInterface } from "@/app/course/[courseName]/context/calend
|
||||
import {
|
||||
getDateOnlyMarkdownString,
|
||||
getDateFromStringOrThrow,
|
||||
} from "@/models/local/utils/timeUtils";
|
||||
} from "@/features/local/utils/timeUtils";
|
||||
import {
|
||||
useSuspenseQuery,
|
||||
useMutation,
|
||||
@@ -92,10 +92,7 @@ export const useCoursePagesByModuleByDateQuery = () => {
|
||||
}
|
||||
);
|
||||
const pagesByModuleByDate = pagesAndModules.reduce(
|
||||
(
|
||||
previous,
|
||||
{ page, moduleName }
|
||||
) => {
|
||||
(previous, { page, moduleName }) => {
|
||||
const dueDay = getDateOnlyMarkdownString(
|
||||
getDateFromStringOrThrow(page.dueAt, "due at for page in items context")
|
||||
);
|
||||
@@ -129,9 +126,7 @@ export const useCourseAssignmentsByModuleByDateQuery = () => {
|
||||
trpc.assignment.getAllAssignments.queryOptions({ courseName, moduleName })
|
||||
),
|
||||
});
|
||||
const assignments = assignmentsResults.map(
|
||||
(result) => result.data
|
||||
);
|
||||
const assignments = assignmentsResults.map((result) => result.data);
|
||||
const assignmentsAndModules = moduleNames.flatMap(
|
||||
(moduleName: string, index: number) => {
|
||||
return assignments[index].map((assignment) => ({
|
||||
@@ -141,10 +136,7 @@ export const useCourseAssignmentsByModuleByDateQuery = () => {
|
||||
}
|
||||
);
|
||||
const assignmentsByModuleByDate = assignmentsAndModules.reduce(
|
||||
(
|
||||
previous,
|
||||
{ assignment, moduleName }
|
||||
) => {
|
||||
(previous, { assignment, moduleName }) => {
|
||||
const dueDay = getDateOnlyMarkdownString(
|
||||
getDateFromStringOrThrow(
|
||||
assignment.dueAt,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||
import { LocalAssignment } from "../assignments/models/localAssignment";
|
||||
import { IModuleItem } from "./IModuleItem";
|
||||
import { getDateFromString } from "../../../models/local/utils/timeUtils";
|
||||
import { getDateFromString } from "../utils/timeUtils";
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
|
||||
export interface LocalModule {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { promises as fs } from "fs";
|
||||
import { lectureFolderName } from "../../../services/fileStorage/utils/lectureUtils";
|
||||
import { getCoursePathByName } from "../../../services/fileStorage/globalSettingsFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
|
||||
export const moduleFileStorageService = {
|
||||
async getModuleNames(courseName: string) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { router } from "@/services/serverFunctions/trpcSetup";
|
||||
import publicProcedure from "@/services/serverFunctions/procedures/public";
|
||||
import publicProcedure from "@/services/serverFunctions/publicProcedure";
|
||||
|
||||
export const moduleRouter = router({
|
||||
getModuleNames: publicProcedure
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
||||
import { verifyDateOrThrow } from "@/models/local/utils/timeUtils";
|
||||
import { verifyDateOrThrow } from "@/features/local/utils/timeUtils";
|
||||
import { z } from "zod";
|
||||
import { extractLabelValue } from "../assignments/models/utils/markdownUtils";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { courseItemFileStorageService } from "../course/courseItemFileStorageService";
|
||||
import { getCoursePathByName } from "../../../services/fileStorage/globalSettingsFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import {
|
||||
LocalCoursePage,
|
||||
localPageMarkdownUtils,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import publicProcedure from "../../../services/serverFunctions/procedures/public";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { zodLocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||
|
||||
export const pageRouter = router({
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { verifyDateOrThrow, verifyDateStringOrUndefined } from "@/models/local/utils/timeUtils";
|
||||
import {
|
||||
verifyDateOrThrow,
|
||||
verifyDateStringOrUndefined,
|
||||
} from "@/features/local/utils/timeUtils";
|
||||
import { LocalQuiz } from "../localQuiz";
|
||||
import { quizQuestionMarkdownUtils } from "./quizQuestionMarkdownUtils";
|
||||
|
||||
@@ -38,7 +41,6 @@ const parseNumberOrThrow = (value: string, label: string): number => {
|
||||
return parsed;
|
||||
};
|
||||
const getQuizWithOnlySettings = (settings: string, name: string): LocalQuiz => {
|
||||
|
||||
const rawShuffleAnswers = extractLabelValue(settings, "ShuffleAnswers");
|
||||
const shuffleAnswers = parseBooleanOrThrow(
|
||||
rawShuffleAnswers,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { courseItemFileStorageService } from "../course/courseItemFileStorageService";
|
||||
import { getCoursePathByName } from "../../../services/fileStorage/globalSettingsFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import publicProcedure from "../../../services/serverFunctions/procedures/public";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { z } from "zod";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
import { zodLocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
|
||||
export const quizRouter = router({
|
||||
|
||||
28
src/features/local/utils/directoriesRouter.ts
Normal file
28
src/features/local/utils/directoriesRouter.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import z from "zod";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/features/local/utils/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);
|
||||
}),
|
||||
});
|
||||
86
src/features/local/utils/fileStorageService.ts
Normal file
86
src/features/local/utils/fileStorageService.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
basePath,
|
||||
directoryOrFileExists,
|
||||
} from "../../../services/fileStorage/utils/fileSystemUtils";
|
||||
import { quizFileStorageService } from "../quizzes/quizFileStorageService";
|
||||
import { pageFileStorageService } from "../pages/pageFileStorageService";
|
||||
import { moduleFileStorageService } from "../modules/moduleFileStorageService";
|
||||
import { settingsFileStorageService } from "../course/settingsFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/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 };
|
||||
},
|
||||
};
|
||||
11
src/features/local/utils/lectureUtils.ts
Normal file
11
src/features/local/utils/lectureUtils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Lecture } from "../lectures/lectureModel";
|
||||
import { getDateOnlyMarkdownString } from "./timeUtils";
|
||||
|
||||
export function getLectureForDay(
|
||||
weeks: { weekName: string; lectures: Lecture[] }[],
|
||||
dayAsDate: Date
|
||||
) {
|
||||
return weeks
|
||||
.flatMap((w) => w.lectures)
|
||||
.find((l) => l.date == getDateOnlyMarkdownString(dayAsDate));
|
||||
}
|
||||
124
src/features/local/utils/semesterTransferUtils.ts
Normal file
124
src/features/local/utils/semesterTransferUtils.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||
import { LocalAssignment } from "../assignments/models/localAssignment";
|
||||
import { Lecture } from "../lectures/lectureModel";
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { getDateFromStringOrThrow, dateToMarkdownString } from "./timeUtils";
|
||||
|
||||
export const prepAssignmentForNewSemester = (
|
||||
assignment: LocalAssignment,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalAssignment => {
|
||||
const descriptionWithoutGithubClassroom = replaceClassroomUrl(
|
||||
assignment.description
|
||||
);
|
||||
return {
|
||||
...assignment,
|
||||
description: descriptionWithoutGithubClassroom,
|
||||
dueAt:
|
||||
newDateOffset(
|
||||
assignment.dueAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
) ?? assignment.dueAt,
|
||||
lockAt: newDateOffset(
|
||||
assignment.lockAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
),
|
||||
githubClassroomAssignmentLink: undefined,
|
||||
githubClassroomAssignmentShareLink: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const prepQuizForNewSemester = (
|
||||
quiz: LocalQuiz,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalQuiz => {
|
||||
const descriptionWithoutGithubClassroom = replaceClassroomUrl(
|
||||
quiz.description
|
||||
);
|
||||
return {
|
||||
...quiz,
|
||||
description: descriptionWithoutGithubClassroom,
|
||||
dueAt:
|
||||
newDateOffset(quiz.dueAt, oldSemesterStartDate, newSemesterStartDate) ??
|
||||
quiz.dueAt,
|
||||
lockAt: newDateOffset(
|
||||
quiz.lockAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const prepPageForNewSemester = (
|
||||
page: LocalCoursePage,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalCoursePage => {
|
||||
const updatedText = replaceClassroomUrl(page.text);
|
||||
return {
|
||||
...page,
|
||||
text: updatedText,
|
||||
dueAt:
|
||||
newDateOffset(page.dueAt, oldSemesterStartDate, newSemesterStartDate) ??
|
||||
page.dueAt,
|
||||
};
|
||||
};
|
||||
export const prepLectureForNewSemester = (
|
||||
lecture: Lecture,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): Lecture => {
|
||||
const updatedText = replaceClassroomUrl(lecture.content);
|
||||
const newDate = newDateOffset(
|
||||
lecture.date,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
const newDateOnly = newDate?.split(" ")[0];
|
||||
return {
|
||||
...lecture,
|
||||
content: updatedText,
|
||||
date: newDateOnly ?? lecture.date,
|
||||
};
|
||||
};
|
||||
|
||||
const replaceClassroomUrl = (value: string) => {
|
||||
const classroomPattern =
|
||||
/https:\/\/classroom\.github\.com\/[a-zA-Z0-9\/._-]+/g;
|
||||
const withoutGithubClassroom = value.replace(
|
||||
classroomPattern,
|
||||
"insert_github_classroom_url"
|
||||
);
|
||||
return withoutGithubClassroom;
|
||||
};
|
||||
|
||||
const newDateOffset = (
|
||||
dateString: string | undefined,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
) => {
|
||||
if (!dateString) return dateString;
|
||||
const oldStart = getDateFromStringOrThrow(
|
||||
oldSemesterStartDate,
|
||||
"semester start date in new semester offset"
|
||||
);
|
||||
const newStart = getDateFromStringOrThrow(
|
||||
newSemesterStartDate,
|
||||
"new semester start date in new semester offset"
|
||||
);
|
||||
const date = getDateFromStringOrThrow(
|
||||
dateString,
|
||||
"date in new semester offset"
|
||||
);
|
||||
const offset = date.getTime() - oldStart.getTime();
|
||||
|
||||
const newUnixTime = offset + newStart.getTime();
|
||||
|
||||
const newDate = new Date(newUnixTime);
|
||||
|
||||
return dateToMarkdownString(newDate);
|
||||
};
|
||||
50
src/features/local/utils/settingsUtils.tsx
Normal file
50
src/features/local/utils/settingsUtils.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
getDateFromStringOrThrow,
|
||||
getDateOnlyMarkdownString,
|
||||
} from "./timeUtils";
|
||||
|
||||
export const parseHolidays = (
|
||||
inputText: string
|
||||
): {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[] => {
|
||||
let holidays: {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[] = [];
|
||||
|
||||
const lines = inputText.split("\n").filter((line) => line.trim() !== "");
|
||||
let currentHoliday: string | null = null;
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.includes(":")) {
|
||||
const holidayName = line.split(":")[0].trim();
|
||||
currentHoliday = holidayName;
|
||||
holidays = [...holidays, { name: holidayName, days: [] }];
|
||||
} else if (currentHoliday && line.startsWith("-")) {
|
||||
const date = line.replace("-", "").trim();
|
||||
const dateObject = getDateFromStringOrThrow(date, "parsing holiday text");
|
||||
|
||||
const holiday = holidays.find((h) => h.name == currentHoliday);
|
||||
holiday?.days.push(getDateOnlyMarkdownString(dateObject));
|
||||
}
|
||||
});
|
||||
|
||||
return holidays;
|
||||
};
|
||||
|
||||
export const holidaysToString = (
|
||||
holidays: {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[]
|
||||
) => {
|
||||
const entries = holidays.map((holiday) => {
|
||||
const title = holiday.name + ":\n";
|
||||
const days = holiday.days.map((d) => `- ${d}\n`);
|
||||
return title + days.join("");
|
||||
});
|
||||
|
||||
return entries.join("");
|
||||
};
|
||||
25
src/features/local/utils/storageDirectoryHooks.ts
Normal file
25
src/features/local/utils/storageDirectoryHooks.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useTRPC } from "@/services/serverFunctions/trpcClient";
|
||||
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
|
||||
export const directoryKeys = {
|
||||
emptyFolders: ["empty folders"] as const,
|
||||
};
|
||||
|
||||
export const useEmptyDirectoriesQuery = () => {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.directories.getEmptyDirectories.queryOptions());
|
||||
};
|
||||
|
||||
export const useDirectoryContentsQuery = (relativePath: string) => {
|
||||
const trpc = useTRPC();
|
||||
return useQuery(
|
||||
trpc.directories.getDirectoryContents.queryOptions({ relativePath })
|
||||
);
|
||||
};
|
||||
|
||||
export const useDirectoryIsCourseQuery = (folderPath: string) => {
|
||||
const trpc = useTRPC();
|
||||
return useQuery(
|
||||
trpc.directories.directoryIsCourse.queryOptions({ folderPath })
|
||||
);
|
||||
};
|
||||
138
src/features/local/utils/timeUtils.ts
Normal file
138
src/features/local/utils/timeUtils.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
||||
|
||||
const _getDateFromAMPM = (
|
||||
datePart: string,
|
||||
timePart: string,
|
||||
amPmPart: string
|
||||
): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
||||
|
||||
let adjustedHours = hours;
|
||||
if (amPmPart) {
|
||||
const upperMeridian = amPmPart.toUpperCase();
|
||||
if (upperMeridian === "PM" && hours < 12) {
|
||||
adjustedHours += 12;
|
||||
} else if (upperMeridian === "AM" && hours === 12) {
|
||||
adjustedHours = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(year, month - 1, day, adjustedHours, minutes, seconds);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromMilitary = (
|
||||
datePart: string,
|
||||
timePart: string
|
||||
): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
||||
|
||||
const date = new Date(year, month - 1, day, hours, minutes, seconds);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromISO = (value: string): Date | undefined => {
|
||||
const date = new Date(value);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromDateOnly = (datePart: string): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const date = new Date(year, month - 1, day);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
export const getDateFromString = (value: string): Date | undefined => {
|
||||
const ampmDateRegex =
|
||||
/^\d{1,2}\/\d{1,2}\/\d{4},? \d{1,2}:\d{2}:\d{2}\s{1}[APap][Mm]$/; //"M/D/YYYY h:mm:ss AM/PM" or "M/D/YYYY, h:mm:ss AM/PM"
|
||||
const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/; //"MM/DD/YYYY HH:mm:ss"
|
||||
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}((.\d+)|(Z))$/; //"2024-08-26T00:00:00.0000000"
|
||||
const dateOnlyRegex = /^\d{1,2}\/\d{1,2}\/\d{4}$/; // "M/D/YYYY" or "MM/DD/YYYY"
|
||||
|
||||
if (isoDateRegex.test(value)) {
|
||||
return _getDateFromISO(value);
|
||||
} else if (ampmDateRegex.test(value)) {
|
||||
const [datePart, timePart, amPmPart] = value.split(/,?[\s\u202F]+/);
|
||||
return _getDateFromAMPM(datePart, timePart, amPmPart);
|
||||
} else if (militaryDateRegex.test(value)) {
|
||||
const [datePart, timePart] = value.split(" ");
|
||||
return _getDateFromMilitary(datePart, timePart);
|
||||
}
|
||||
if (dateOnlyRegex.test(value)) {
|
||||
return _getDateFromDateOnly(value);
|
||||
} else {
|
||||
if (value) console.log("invalid date format", value);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDateFromStringOrThrow = (
|
||||
value: string,
|
||||
labelForError: string
|
||||
): Date => {
|
||||
const d = getDateFromString(value);
|
||||
if (!d) throw Error(`Invalid date format for ${labelForError}, ${value}`);
|
||||
return d;
|
||||
};
|
||||
|
||||
export const verifyDateStringOrUndefined = (
|
||||
value: string
|
||||
): string | undefined => {
|
||||
const date = getDateFromString(value);
|
||||
return date ? dateToMarkdownString(date) : undefined;
|
||||
};
|
||||
|
||||
export const verifyDateOrThrow = (
|
||||
value: string,
|
||||
labelForError: string
|
||||
): string => {
|
||||
const myDate = getDateFromString(value);
|
||||
if (!myDate) throw new Error(`Invalid format for ${labelForError}: ${value}`);
|
||||
return dateToMarkdownString(myDate);
|
||||
};
|
||||
|
||||
export const dateToMarkdownString = (date: Date) => {
|
||||
const stringDay = String(date.getDate()).padStart(2, "0");
|
||||
const stringMonth = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based
|
||||
const stringYear = date.getFullYear();
|
||||
const stringHours = String(date.getHours()).padStart(2, "0");
|
||||
const stringMinutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const stringSeconds = String(date.getSeconds()).padStart(2, "0");
|
||||
|
||||
return `${stringMonth}/${stringDay}/${stringYear} ${stringHours}:${stringMinutes}:${stringSeconds}`;
|
||||
};
|
||||
|
||||
export const getDateOnlyMarkdownString = (date: Date) => {
|
||||
return dateToMarkdownString(date).split(" ")[0];
|
||||
};
|
||||
|
||||
export function getTermName(startDate: string) {
|
||||
const [year, month, ..._rest] = startDate.split("-");
|
||||
if (month < "04") return "Spring " + year;
|
||||
if (month < "07") return "Summer " + year;
|
||||
return "Fall " + year;
|
||||
}
|
||||
|
||||
export function getDateKey(dateString: string) {
|
||||
return dateString.split("T")[0];
|
||||
}
|
||||
export function groupByStartDate(courses: LocalCourseSettings[]): {
|
||||
[key: string]: LocalCourseSettings[];
|
||||
} {
|
||||
return courses.reduce(
|
||||
(acc, course) => {
|
||||
const { startDate } = course;
|
||||
const key = getDateKey(startDate);
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(course);
|
||||
return acc;
|
||||
},
|
||||
{} as {
|
||||
[key: string]: LocalCourseSettings[];
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user