mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
can add new courses, kinda janky
This commit is contained in:
@@ -7,3 +7,5 @@ courses:
|
|||||||
name: Web Intro
|
name: Web Intro
|
||||||
- path: ./1430/2025-fall-alex/modules/
|
- path: ./1430/2025-fall-alex/modules/
|
||||||
name: UX
|
name: UX
|
||||||
|
- path: ./1420/2025-fall-alex/labModules
|
||||||
|
name: "1425"
|
||||||
|
|||||||
@@ -2,15 +2,24 @@
|
|||||||
import ButtonSelect from "@/components/ButtonSelect";
|
import ButtonSelect from "@/components/ButtonSelect";
|
||||||
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
|
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
|
||||||
import SelectInput from "@/components/form/SelectInput";
|
import SelectInput from "@/components/form/SelectInput";
|
||||||
|
import { StoragePathSelector } from "@/components/form/StoragePathSelector";
|
||||||
|
import TextInput from "@/components/form/TextInput";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks";
|
import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks";
|
||||||
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
|
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
|
||||||
|
import {
|
||||||
|
useGlobalSettingsQuery,
|
||||||
|
useUpdateGlobalSettingsMutation,
|
||||||
|
} from "@/hooks/localCourse/globalSettingsHooks";
|
||||||
import {
|
import {
|
||||||
useCreateLocalCourseMutation,
|
useCreateLocalCourseMutation,
|
||||||
useLocalCoursesSettingsQuery,
|
useLocalCoursesSettingsQuery,
|
||||||
} from "@/hooks/localCourse/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { useEmptyDirectoriesQuery } from "@/hooks/localCourse/storageDirectoryHooks";
|
import {
|
||||||
|
useDirectoryIsCourseQuery,
|
||||||
|
useEmptyDirectoriesQuery,
|
||||||
|
} from "@/hooks/localCourse/storageDirectoryHooks";
|
||||||
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
||||||
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
||||||
import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
|
import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
|
||||||
@@ -20,7 +29,7 @@ import {
|
|||||||
} from "@/models/local/localCourseSettings";
|
} from "@/models/local/localCourseSettings";
|
||||||
import { getCourseUrl } from "@/services/urlUtils";
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||||
|
|
||||||
const sampleCompose = `services:
|
const sampleCompose = `services:
|
||||||
canvas_manager:
|
canvas_manager:
|
||||||
@@ -55,6 +64,7 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
const [courseToImport, setCourseToImport] = useState<
|
const [courseToImport, setCourseToImport] = useState<
|
||||||
LocalCourseSettings | undefined
|
LocalCourseSettings | undefined
|
||||||
>();
|
>();
|
||||||
|
const [name, setName] = useState("");
|
||||||
const createCourse = useCreateLocalCourseMutation();
|
const createCourse = useCreateLocalCourseMutation();
|
||||||
|
|
||||||
const formIsComplete =
|
const formIsComplete =
|
||||||
@@ -82,6 +92,8 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
setSelectedDaysOfWeek={setSelectedDaysOfWeek}
|
setSelectedDaysOfWeek={setSelectedDaysOfWeek}
|
||||||
courseToImport={courseToImport}
|
courseToImport={courseToImport}
|
||||||
setCourseToImport={setCourseToImport}
|
setCourseToImport={setCourseToImport}
|
||||||
|
name={name}
|
||||||
|
setName={setName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SuspenseAndErrorHandling>
|
</SuspenseAndErrorHandling>
|
||||||
@@ -90,6 +102,7 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
disabled={!formIsComplete || createCourse.isPending}
|
disabled={!formIsComplete || createCourse.isPending}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (formIsComplete) {
|
if (formIsComplete) {
|
||||||
|
console.log("Creating course with settings:", selectedDirectory);
|
||||||
const newSettings: LocalCourseSettings = courseToImport
|
const newSettings: LocalCourseSettings = courseToImport
|
||||||
? {
|
? {
|
||||||
...courseToImport,
|
...courseToImport,
|
||||||
@@ -128,8 +141,10 @@ export default function AddNewCourseToGlobalSettingsForm() {
|
|||||||
await createCourse.mutateAsync({
|
await createCourse.mutateAsync({
|
||||||
settings: newSettings,
|
settings: newSettings,
|
||||||
settingsFromCourseToImport: courseToImport,
|
settingsFromCourseToImport: courseToImport,
|
||||||
|
name,
|
||||||
|
directory: selectedDirectory,
|
||||||
});
|
});
|
||||||
router.push(getCourseUrl(selectedDirectory));
|
router.push(getCourseUrl(name));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -156,26 +171,29 @@ function OtherSettings({
|
|||||||
setSelectedDaysOfWeek,
|
setSelectedDaysOfWeek,
|
||||||
courseToImport,
|
courseToImport,
|
||||||
setCourseToImport,
|
setCourseToImport,
|
||||||
|
name,
|
||||||
|
setName,
|
||||||
}: {
|
}: {
|
||||||
selectedTerm: CanvasEnrollmentTermModel;
|
selectedTerm: CanvasEnrollmentTermModel;
|
||||||
selectedCanvasCourse: CanvasCourseModel | undefined;
|
selectedCanvasCourse: CanvasCourseModel | undefined;
|
||||||
setSelectedCanvasCourse: React.Dispatch<
|
setSelectedCanvasCourse: Dispatch<
|
||||||
React.SetStateAction<CanvasCourseModel | undefined>
|
SetStateAction<CanvasCourseModel | undefined>
|
||||||
>;
|
>;
|
||||||
selectedDirectory: string | undefined;
|
selectedDirectory: string | undefined;
|
||||||
setSelectedDirectory: React.Dispatch<
|
setSelectedDirectory: Dispatch<SetStateAction<string | undefined>>;
|
||||||
React.SetStateAction<string | undefined>
|
|
||||||
>;
|
|
||||||
selectedDaysOfWeek: DayOfWeek[];
|
selectedDaysOfWeek: DayOfWeek[];
|
||||||
setSelectedDaysOfWeek: React.Dispatch<React.SetStateAction<DayOfWeek[]>>;
|
setSelectedDaysOfWeek: Dispatch<SetStateAction<DayOfWeek[]>>;
|
||||||
courseToImport: LocalCourseSettings | undefined;
|
courseToImport: LocalCourseSettings | undefined;
|
||||||
setCourseToImport: React.Dispatch<
|
setCourseToImport: Dispatch<SetStateAction<LocalCourseSettings | undefined>>;
|
||||||
React.SetStateAction<LocalCourseSettings | undefined>
|
name: string;
|
||||||
>;
|
setName: Dispatch<SetStateAction<string>>;
|
||||||
}) {
|
}) {
|
||||||
const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id);
|
const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id);
|
||||||
const { data: allSettings } = useLocalCoursesSettingsQuery();
|
const { data: allSettings } = useLocalCoursesSettingsQuery();
|
||||||
const { data: emptyDirectories } = useEmptyDirectoriesQuery();
|
const [directory, setDirectory] = useState("./");
|
||||||
|
// const directoryIsCourseQuery = useDirectoryIsCourseQuery(
|
||||||
|
// selectedDirectory ?? "./"
|
||||||
|
// );
|
||||||
|
|
||||||
const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? [];
|
const populatedCanvasCourseIds = allSettings?.map((s) => s.canvasId) ?? [];
|
||||||
const availableCourses =
|
const availableCourses =
|
||||||
@@ -194,18 +212,13 @@ function OtherSettings({
|
|||||||
getOptionName={(c) => c?.name ?? ""}
|
getOptionName={(c) => c?.name ?? ""}
|
||||||
center={true}
|
center={true}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
|
||||||
value={selectedDirectory}
|
<StoragePathSelector
|
||||||
setValue={setSelectedDirectory}
|
value={directory}
|
||||||
|
setValue={setDirectory}
|
||||||
|
setLastTypedValue={setSelectedDirectory}
|
||||||
label={"Storage Folder"}
|
label={"Storage Folder"}
|
||||||
options={emptyDirectories ?? []}
|
|
||||||
getOptionName={(d) => d}
|
|
||||||
emptyOptionText="--- add a new folder to your docker compose to add more folders ---"
|
|
||||||
/>
|
/>
|
||||||
<div className="px-5">
|
|
||||||
New folders will not be created automatically, you are expected to mount
|
|
||||||
a docker volume for each courses.
|
|
||||||
</div>
|
|
||||||
<br />
|
<br />
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<DayOfWeekInput
|
<DayOfWeekInput
|
||||||
@@ -228,6 +241,7 @@ function OtherSettings({
|
|||||||
options={allSettings}
|
options={allSettings}
|
||||||
getOptionName={(c) => c.name}
|
getOptionName={(c) => c.name}
|
||||||
/>
|
/>
|
||||||
|
<TextInput value={name} setValue={setName} label={"Display Name"} />
|
||||||
<div className="px-5">
|
<div className="px-5">
|
||||||
Assignments, Quizzes, Pages, and Lectures will have their due dates
|
Assignments, Quizzes, Pages, and Lectures will have their due dates
|
||||||
moved based on how far they are from the start of the semester.
|
moved based on how far they are from the start of the semester.
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ export function StoragePathSelector({
|
|||||||
setValue,
|
setValue,
|
||||||
label,
|
label,
|
||||||
className,
|
className,
|
||||||
|
setLastTypedValue,
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
setValue: (newValue: string) => void;
|
setValue: (newValue: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
setLastTypedValue?: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [path, setPath] = useState(value);
|
const [path, setPath] = useState(value);
|
||||||
const { data: directoryContents } = useDirectoryContentsQuery(value);
|
const { data: directoryContents } = useDirectoryContentsQuery(value);
|
||||||
@@ -25,6 +27,10 @@ export function StoragePathSelector({
|
|||||||
setPath(value);
|
setPath(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setLastTypedValue) setLastTypedValue(path);
|
||||||
|
}, [path, setLastTypedValue]);
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (!isFocused || filteredFolders.length === 0) return;
|
if (!isFocused || filteredFolders.length === 0) return;
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
|
import { useGlobalSettingsQuery } from "./globalSettingsHooks";
|
||||||
|
|
||||||
export const useLocalCoursesSettingsQuery = () => {
|
export const useLocalCoursesSettingsQuery = () => {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
@@ -23,6 +24,7 @@ export const useLocalCourseSettingsQuery = () => {
|
|||||||
export const useCreateLocalCourseMutation = () => {
|
export const useCreateLocalCourseMutation = () => {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
trpc.settings.createCourse.mutationOptions({
|
trpc.settings.createCourse.mutationOptions({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -32,6 +34,9 @@ export const useCreateLocalCourseMutation = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: trpc.directories.getEmptyDirectories.queryKey(),
|
queryKey: trpc.directories.getEmptyDirectories.queryKey(),
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: trpc.globalSettings.getGlobalSettings.queryKey(),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { GlobalSettings } from "@/models/local/globalSettings";
|
import {
|
||||||
|
GlobalSettings,
|
||||||
|
zodGlobalSettings,
|
||||||
|
} from "@/models/local/globalSettings";
|
||||||
import {
|
import {
|
||||||
globalSettingsToYaml,
|
globalSettingsToYaml,
|
||||||
parseGlobalSettingsYaml,
|
parseGlobalSettingsYaml,
|
||||||
@@ -39,6 +42,15 @@ export const getCoursePathByName = async (courseName: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateGlobalSettings = async (globalSettings: GlobalSettings) => {
|
export const updateGlobalSettings = async (globalSettings: GlobalSettings) => {
|
||||||
const globalSettingsString = globalSettingsToYaml(globalSettings);
|
const globalSettingsString = globalSettingsToYaml(
|
||||||
|
zodGlobalSettings.parse(globalSettings)
|
||||||
|
);
|
||||||
await fs.writeFile(SETTINGS_FILE_PATH, globalSettingsString, "utf-8");
|
await fs.writeFile(SETTINGS_FILE_PATH, globalSettingsString, "utf-8");
|
||||||
|
|
||||||
|
// await Promise.all(
|
||||||
|
// globalSettings.courses.map(async (course) => {
|
||||||
|
// const coursePath = await getCoursePathByName(course.name);
|
||||||
|
// await fs.mkdir(coursePath, { recursive: true });
|
||||||
|
// })
|
||||||
|
// );
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,6 +86,26 @@ export const settingsFileStorageService = {
|
|||||||
console.log(`Saving settings ${settingsPath}`);
|
console.log(`Saving settings ${settingsPath}`);
|
||||||
await fs.writeFile(settingsPath, settingsMarkdown);
|
await fs.writeFile(settingsPath, settingsMarkdown);
|
||||||
},
|
},
|
||||||
|
async createCourseSettings(settings: LocalCourseSettings, directory: string) {
|
||||||
|
const courseDirectory = path.join(basePath, directory);
|
||||||
|
|
||||||
|
if (await directoryOrFileExists(courseDirectory)) {
|
||||||
|
throw new Error(
|
||||||
|
`Course path "${courseDirectory}" already exists. Create course in a new folder.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(courseDirectory, { recursive: true });
|
||||||
|
const settingsPath = path.join(courseDirectory, "settings.yml");
|
||||||
|
|
||||||
|
const { name: _, ...settingsWithoutName } = settings;
|
||||||
|
|
||||||
|
const settingsMarkdown =
|
||||||
|
localCourseYamlUtils.settingsToYaml(settingsWithoutName);
|
||||||
|
|
||||||
|
console.log(`Saving settings ${settingsPath}`);
|
||||||
|
await fs.writeFile(settingsPath, settingsMarkdown);
|
||||||
|
},
|
||||||
async folderIsCourse(folderPath: string) {
|
async folderIsCourse(folderPath: string) {
|
||||||
const settingsPath = path.join(basePath, folderPath, "settings.yml");
|
const settingsPath = path.join(basePath, folderPath, "settings.yml");
|
||||||
if (!(await directoryOrFileExists(settingsPath))) {
|
if (!(await directoryOrFileExists(settingsPath))) {
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ import {
|
|||||||
prepPageForNewSemester,
|
prepPageForNewSemester,
|
||||||
prepQuizForNewSemester,
|
prepQuizForNewSemester,
|
||||||
} from "@/models/local/utils/semesterTransferUtils";
|
} from "@/models/local/utils/semesterTransferUtils";
|
||||||
|
import {
|
||||||
|
getGlobalSettings,
|
||||||
|
updateGlobalSettings,
|
||||||
|
} from "@/services/fileStorage/globalSettingsFileStorageService";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { basePath } from "@/services/fileStorage/utils/fileSystemUtils";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export const settingsRouter = router({
|
export const settingsRouter = router({
|
||||||
allCoursesSettings: publicProcedure.query(async () => {
|
allCoursesSettings: publicProcedure.query(async () => {
|
||||||
@@ -37,16 +44,35 @@ export const settingsRouter = router({
|
|||||||
createCourse: publicProcedure
|
createCourse: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
directory: z.string(),
|
||||||
settings: zodLocalCourseSettings,
|
settings: zodLocalCourseSettings,
|
||||||
settingsFromCourseToImport: zodLocalCourseSettings.optional(),
|
settingsFromCourseToImport: zodLocalCourseSettings.optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input: { settings, settingsFromCourseToImport } }) => {
|
.mutation(
|
||||||
await fileStorageService.settings.updateCourseSettings(
|
async ({
|
||||||
settings.name,
|
input: { settings, settingsFromCourseToImport, name, directory },
|
||||||
settings
|
}) => {
|
||||||
|
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) {
|
if (settingsFromCourseToImport) {
|
||||||
const oldCourseName = settingsFromCourseToImport.name;
|
const oldCourseName = settingsFromCourseToImport.name;
|
||||||
const newCourseName = settings.name;
|
const newCourseName = settings.name;
|
||||||
@@ -82,14 +108,16 @@ export const settingsRouter = router({
|
|||||||
const newAssignment = prepAssignmentForNewSemester(
|
const newAssignment = prepAssignmentForNewSemester(
|
||||||
oldAssignment,
|
oldAssignment,
|
||||||
settingsFromCourseToImport.startDate,
|
settingsFromCourseToImport.startDate,
|
||||||
settings.startDate,
|
settings.startDate
|
||||||
);
|
);
|
||||||
await fileStorageService.assignments.updateOrCreateAssignment({
|
await fileStorageService.assignments.updateOrCreateAssignment(
|
||||||
|
{
|
||||||
courseName: newCourseName,
|
courseName: newCourseName,
|
||||||
moduleName,
|
moduleName,
|
||||||
assignmentName: newAssignment.name,
|
assignmentName: newAssignment.name,
|
||||||
assignment: newAssignment,
|
assignment: newAssignment,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
...oldQuizzes.map(async (oldQuiz) => {
|
...oldQuizzes.map(async (oldQuiz) => {
|
||||||
const newQuiz = prepQuizForNewSemester(
|
const newQuiz = prepQuizForNewSemester(
|
||||||
@@ -131,7 +159,8 @@ export const settingsRouter = router({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
updateSettings: publicProcedure
|
updateSettings: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user