diff --git a/nextjs/src/components/form/SelectInput.tsx b/nextjs/src/components/form/SelectInput.tsx
index a3beabf..ec4d17b 100644
--- a/nextjs/src/components/form/SelectInput.tsx
+++ b/nextjs/src/components/form/SelectInput.tsx
@@ -4,12 +4,14 @@ export default function SelectInput({
label,
options,
getOptionName,
+ emptyOptionText,
}: {
value: T | undefined;
setValue: (newValue: T | undefined) => void;
label: string;
options: T[];
getOptionName: (item: T) => string;
+ emptyOptionText?: string;
}) {
return (
@@ -25,6 +27,7 @@ export default function SelectInput({
}}
>
+ {emptyOptionText && {emptyOptionText} }
{options.map((o) => (
{getOptionName(o)}
))}
diff --git a/nextjs/src/hooks/canvas/canvasHooks.ts b/nextjs/src/hooks/canvas/canvasHooks.ts
index 3261540..ced0116 100644
--- a/nextjs/src/hooks/canvas/canvasHooks.ts
+++ b/nextjs/src/hooks/canvas/canvasHooks.ts
@@ -17,13 +17,14 @@ export const useCanvasTermsQuery = (queryDate: Date) => {
return useSuspenseQuery({
queryKey: canvasKeys.allAroundDate(queryDate),
queryFn: () => {
- const currentTerms = terms
- .filter((t) => {
- if (!t.end_at) return false;
+ const finiteTerms = terms.filter((t) => {
+ if (!t.end_at) return false;
- const endDate = new Date(t.end_at);
- return endDate > queryDate;
- })
+ const endDate = new Date(t.end_at);
+ return endDate > queryDate;
+ });
+ console.log("finite terms", finiteTerms, terms);
+ const currentTerms = finiteTerms
.sort(
(a, b) =>
new Date(a.start_at ?? "").getTime() -
@@ -34,4 +35,4 @@ export const useCanvasTermsQuery = (queryDate: Date) => {
return currentTerms;
},
});
-};
\ No newline at end of file
+};
diff --git a/nextjs/src/services/canvas/canvasService.ts b/nextjs/src/services/canvas/canvasService.ts
index 7e2e99b..4a19d45 100644
--- a/nextjs/src/services/canvas/canvasService.ts
+++ b/nextjs/src/services/canvas/canvasService.ts
@@ -1,24 +1,25 @@
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
-import { canvasApi } from "./canvasServiceUtils";
+import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel";
import { axiosClient } from "../axiosUtils";
const getAllTerms = async () => {
- const url = `${canvasApi}/accounts/10/terms`;
- const { data } = await axiosClient.get<{
- enrollment_terms: CanvasEnrollmentTermModel[];
- }>(url);
- const terms = data.enrollment_terms;
+ const url = `${canvasApi}/accounts/10/terms?per_page=100`;
+ const data = await paginatedRequest<
+ {
+ enrollment_terms: CanvasEnrollmentTermModel[];
+ }[]
+ >({ url });
+ const terms = data.flatMap((t) => t.enrollment_terms);
return terms;
};
export const canvasService = {
getAllTerms,
async getCourses(termId: number) {
- const url = `${canvasApi}/courses`;
- const response = await axiosClient.get(url);
- const allCourses = response.data;
+ const url = `${canvasApi}/courses?per_page=100`;
+ const allCourses = await paginatedRequest({ url });
const coursesInTerm = allCourses
.flatMap((l) => l)
.filter((c) => c.enrollment_term_id === termId);
diff --git a/nextjs/src/services/canvas/canvasServiceUtils.ts b/nextjs/src/services/canvas/canvasServiceUtils.ts
index 6845126..8aded91 100644
--- a/nextjs/src/services/canvas/canvasServiceUtils.ts
+++ b/nextjs/src/services/canvas/canvasServiceUtils.ts
@@ -44,19 +44,14 @@ export async function paginatedRequest(request: {
url.toString()
);
- // if (!Array.isArray(firstData)) {
- // return firstData;
- // }
-
- var returnData = [...firstData];
+ var returnData = Array.isArray(firstData) ? [...firstData] : [firstData]; // terms come across as nested objects {enrolmentTerms: terms[]}
var nextUrl = getNextUrl(firstHeaders);
- // console.log("got first request", nextUrl, firstHeaders);
while (nextUrl) {
requestCount += 1;
const { data, headers } = await axiosClient.get(nextUrl);
if (data) {
- returnData = returnData.concat(data);
+ returnData = returnData.concat(Array.isArray(data) ? [...data] : [data]);
}
nextUrl = getNextUrl(headers);
}
diff --git a/nextjs/src/services/fileStorage/fileStorageService.ts b/nextjs/src/services/fileStorage/fileStorageService.ts
index 09e9f41..67c8b26 100644
--- a/nextjs/src/services/fileStorage/fileStorageService.ts
+++ b/nextjs/src/services/fileStorage/fileStorageService.ts
@@ -1,230 +1,19 @@
import { promises as fs } from "fs";
import path from "path";
-import {
- LocalCourseSettings,
- localCourseYamlUtils,
-} from "@/models/local/localCourse";
import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils";
-import {
- LocalQuiz,
- localQuizMarkdownUtils,
-} from "@/models/local/quiz/localQuiz";
-import {
- LocalCoursePage,
- localPageMarkdownUtils,
-} from "@/models/local/page/localCoursePage";
-import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import { assignmentsFileStorageService } from "./assignmentsFileStorageService";
-
-const quizzes = {
- async getQuizNames(courseName: string, moduleName: string) {
- const filePath = path.join(basePath, courseName, moduleName, "quizzes");
- if (!(await directoryOrFileExists(filePath))) {
- console.log(
- `Error loading course by name, quiz folder does not exist in ${filePath}`
- );
- await fs.mkdir(filePath);
- }
-
- const files = await fs.readdir(filePath);
- return files.map((f) => f.replace(/\.md$/, ""));
- },
- async getQuiz(courseName: string, moduleName: string, quizName: string) {
- const filePath = path.join(
- basePath,
- courseName,
- moduleName,
- "quizzes",
- quizName + ".md"
- );
- const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
- /\r\n/g,
- "\n"
- );
- return localQuizMarkdownUtils.parseMarkdown(rawFile);
- },
-
- async updateQuiz(
- courseName: string,
- moduleName: string,
- quizName: string,
- quiz: LocalQuiz
- ) {
- const folder = path.join(basePath, courseName, moduleName, "quizzes");
- await fs.mkdir(folder, { recursive: true });
- const filePath = path.join(
- basePath,
- courseName,
- moduleName,
- "quizzes",
- quizName + ".md"
- );
-
- const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
- console.log(`Saving quiz ${filePath}`);
- await fs.writeFile(filePath, quizMarkdown);
- },
-};
-
-const pages = {
- async getPageNames(courseName: string, moduleName: string) {
- const filePath = path.join(basePath, courseName, moduleName, "pages");
- if (!(await directoryOrFileExists(filePath))) {
- console.log(
- `Error loading course by name, pages folder does not exist in ${filePath}`
- );
- await fs.mkdir(filePath);
- }
-
- const files = await fs.readdir(filePath);
- return files.map((f) => f.replace(/\.md$/, ""));
- },
-
- async getPage(courseName: string, moduleName: string, pageName: string) {
- const filePath = path.join(
- basePath,
- courseName,
- moduleName,
- "pages",
- pageName + ".md"
- );
- const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
- /\r\n/g,
- "\n"
- );
- return localPageMarkdownUtils.parseMarkdown(rawFile);
- },
- async updatePage(
- courseName: string,
- moduleName: string,
- pageName: string,
- page: LocalCoursePage
- ) {
- const folder = path.join(basePath, courseName, moduleName, "pages");
- await fs.mkdir(folder, { recursive: true });
-
- const filePath = path.join(
- basePath,
- courseName,
- moduleName,
- "pages",
- page.name + ".md"
- );
-
- const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
- console.log(`Saving page ${filePath}`);
- await fs.writeFile(filePath, pageMarkdown);
-
- const pageNameIsChanged = pageName !== page.name;
- if (pageNameIsChanged) {
- console.log("removing old page after name change " + pageName);
- const oldFilePath = path.join(
- basePath,
- courseName,
- moduleName,
- "pages",
- pageName + ".md"
- );
- await fs.unlink(oldFilePath);
- }
- },
-};
-
-const modules = {
- async getModuleNames(courseName: string) {
- const courseDirectory = path.join(basePath, courseName);
- const moduleDirectories = await fs.readdir(courseDirectory, {
- withFileTypes: true,
- });
-
- const modulePromises = moduleDirectories
- .filter((dirent) => dirent.isDirectory())
- .map((dirent) => dirent.name);
-
- const modules = await Promise.all(modulePromises);
- return modules.sort((a, b) => a.localeCompare(b));
- },
- async createModule(courseName: string, moduleName: string) {
- const courseDirectory = path.join(basePath, courseName);
-
- await fs.mkdir(courseDirectory + "/" + moduleName, { recursive: true });
- },
-};
-
-const settings = {
- async getAllCoursesSettings() {
- const courses = await fileStorageService.getCourseNames();
-
- const courseSettings = await Promise.all(
- courses.map(
- async (c) => await fileStorageService.settings.getCourseSettings(c)
- )
- );
- return courseSettings;
- },
- async getCourseSettings(courseName: string): Promise {
- const courseDirectory = path.join(basePath, courseName);
- const settingsPath = path.join(courseDirectory, "settings.yml");
- if (!(await directoryOrFileExists(settingsPath))) {
- const errorMessage = `Error loading settings for ${courseName}, 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 updateCourseSettings(
- courseName: string,
- settings: LocalCourseSettings
- ) {
- const courseDirectory = path.join(basePath, courseName);
- 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);
- },
-};
+import { quizFileStorageService } from "./quizFileStorageService";
+import { pageFileStorageService } from "./pageFileStorageService";
+import { moduleFileStorageService } from "./moduleFileStorageService";
+import { settingsFileStorageService } from "./settingsFileStorageService";
export const fileStorageService = {
- async getCourseNames() {
- console.log("loading course ids");
- const courseDirectories = await fs.readdir(basePath, {
- withFileTypes: true,
- });
- const coursePromises = await Promise.all(
- courseDirectories
- .filter((dirent) => dirent.isDirectory())
- .map(async (dirent) => {
- const coursePath = path.join(basePath, dirent.name);
- const settingsPath = path.join(coursePath, "settings.yml");
- const hasSettings = await directoryOrFileExists(settingsPath);
- return {
- dirent,
- hasSettings,
- };
- })
- );
-
- const courseNamesFromDirectories = coursePromises
- .filter(({ hasSettings }) => hasSettings)
- .map(({ dirent }) => dirent.name);
-
- return courseNamesFromDirectories;
- },
- settings,
- modules,
+ settings: settingsFileStorageService,
+ modules: moduleFileStorageService,
assignments: assignmentsFileStorageService,
- quizzes,
- pages,
+ quizzes: quizFileStorageService,
+ pages: pageFileStorageService,
+
async getEmptyDirectories(): Promise {
if (!(await directoryOrFileExists(basePath))) {
diff --git a/nextjs/src/services/fileStorage/moduleFileStorageService.ts b/nextjs/src/services/fileStorage/moduleFileStorageService.ts
index e69de29..89fcc7d 100644
--- a/nextjs/src/services/fileStorage/moduleFileStorageService.ts
+++ b/nextjs/src/services/fileStorage/moduleFileStorageService.ts
@@ -0,0 +1,24 @@
+import { promises as fs } from "fs";
+import path from "path";
+import { basePath } from "./utils/fileSystemUtils";
+
+export const moduleFileStorageService = {
+ async getModuleNames(courseName: string) {
+ const courseDirectory = path.join(basePath, courseName);
+ const moduleDirectories = await fs.readdir(courseDirectory, {
+ withFileTypes: true,
+ });
+
+ const modulePromises = moduleDirectories
+ .filter((dirent) => dirent.isDirectory())
+ .map((dirent) => dirent.name);
+
+ const modules = await Promise.all(modulePromises);
+ return modules.sort((a, b) => a.localeCompare(b));
+ },
+ async createModule(courseName: string, moduleName: string) {
+ const courseDirectory = path.join(basePath, courseName);
+
+ await fs.mkdir(courseDirectory + "/" + moduleName, { recursive: true });
+ },
+};
diff --git a/nextjs/src/services/fileStorage/pageFileStorageService.ts b/nextjs/src/services/fileStorage/pageFileStorageService.ts
index e69de29..01cc56c 100644
--- a/nextjs/src/services/fileStorage/pageFileStorageService.ts
+++ b/nextjs/src/services/fileStorage/pageFileStorageService.ts
@@ -0,0 +1,68 @@
+import { localPageMarkdownUtils, LocalCoursePage } from "@/models/local/page/localCoursePage";
+import { promises as fs } from "fs";
+import path from "path";
+import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils";
+
+export const pageFileStorageService = {
+ async getPageNames(courseName: string, moduleName: string) {
+ const filePath = path.join(basePath, courseName, moduleName, "pages");
+ if (!(await directoryOrFileExists(filePath))) {
+ console.log(
+ `Error loading course by name, pages folder does not exist in ${filePath}`
+ );
+ await fs.mkdir(filePath);
+ }
+
+ const files = await fs.readdir(filePath);
+ return files.map((f) => f.replace(/\.md$/, ""));
+ },
+
+ async getPage(courseName: string, moduleName: string, pageName: string) {
+ const filePath = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "pages",
+ pageName + ".md"
+ );
+ const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
+ /\r\n/g,
+ "\n"
+ );
+ return localPageMarkdownUtils.parseMarkdown(rawFile);
+ },
+ async updatePage(
+ courseName: string,
+ moduleName: string,
+ pageName: string,
+ page: LocalCoursePage
+ ) {
+ const folder = path.join(basePath, courseName, moduleName, "pages");
+ await fs.mkdir(folder, { recursive: true });
+
+ const filePath = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "pages",
+ page.name + ".md"
+ );
+
+ const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
+ console.log(`Saving page ${filePath}`);
+ await fs.writeFile(filePath, pageMarkdown);
+
+ const pageNameIsChanged = pageName !== page.name;
+ if (pageNameIsChanged) {
+ console.log("removing old page after name change " + pageName);
+ const oldFilePath = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "pages",
+ pageName + ".md"
+ );
+ await fs.unlink(oldFilePath);
+ }
+ },
+};
\ No newline at end of file
diff --git a/nextjs/src/services/fileStorage/quizFileStorageService.ts b/nextjs/src/services/fileStorage/quizFileStorageService.ts
index e69de29..9b0d0b0 100644
--- a/nextjs/src/services/fileStorage/quizFileStorageService.ts
+++ b/nextjs/src/services/fileStorage/quizFileStorageService.ts
@@ -0,0 +1,58 @@
+import {
+ localQuizMarkdownUtils,
+ LocalQuiz,
+} from "@/models/local/quiz/localQuiz";
+import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
+import path from "path";
+import { basePath, directoryOrFileExists } from "./utils/fileSystemUtils";
+import { promises as fs } from "fs";
+
+export const quizFileStorageService = {
+ async getQuizNames(courseName: string, moduleName: string) {
+ const filePath = path.join(basePath, courseName, moduleName, "quizzes");
+ if (!(await directoryOrFileExists(filePath))) {
+ console.log(
+ `Error loading course by name, quiz folder does not exist in ${filePath}`
+ );
+ await fs.mkdir(filePath);
+ }
+
+ const files = await fs.readdir(filePath);
+ return files.map((f) => f.replace(/\.md$/, ""));
+ },
+ async getQuiz(courseName: string, moduleName: string, quizName: string) {
+ const filePath = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "quizzes",
+ quizName + ".md"
+ );
+ const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
+ /\r\n/g,
+ "\n"
+ );
+ return localQuizMarkdownUtils.parseMarkdown(rawFile);
+ },
+
+ async updateQuiz(
+ courseName: string,
+ moduleName: string,
+ quizName: string,
+ quiz: LocalQuiz
+ ) {
+ const folder = path.join(basePath, courseName, moduleName, "quizzes");
+ await fs.mkdir(folder, { recursive: true });
+ const filePath = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "quizzes",
+ quizName + ".md"
+ );
+
+ const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
+ console.log(`Saving quiz ${filePath}`);
+ await fs.writeFile(filePath, quizMarkdown);
+ },
+};
diff --git a/nextjs/src/services/fileStorage/settingsFileStorageService.ts b/nextjs/src/services/fileStorage/settingsFileStorageService.ts
index e69de29..6089fc4 100644
--- a/nextjs/src/services/fileStorage/settingsFileStorageService.ts
+++ b/nextjs/src/services/fileStorage/settingsFileStorageService.ts
@@ -0,0 +1,56 @@
+import {
+ LocalCourseSettings,
+ localCourseYamlUtils,
+} from "@/models/local/localCourse";
+import { promises as fs } from "fs";
+import path from "path";
+import {
+ basePath,
+ directoryOrFileExists,
+ getCourseNames,
+} from "./utils/fileSystemUtils";
+
+const getCourseSettings = async (
+ courseName: string
+): Promise => {
+ const courseDirectory = path.join(basePath, courseName);
+ const settingsPath = path.join(courseDirectory, "settings.yml");
+ if (!(await directoryOrFileExists(settingsPath))) {
+ const errorMessage = `Error loading settings for ${courseName}, 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 };
+};
+
+export const settingsFileStorageService = {
+ getCourseSettings,
+ async getAllCoursesSettings() {
+ const courses = await getCourseNames();
+
+ const courseSettings = await Promise.all(
+ courses.map(async (c) => await getCourseSettings(c))
+ );
+ return courseSettings;
+ },
+
+ async updateCourseSettings(
+ courseName: string,
+ settings: LocalCourseSettings
+ ) {
+ const courseDirectory = path.join(basePath, courseName);
+ 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);
+ },
+};
diff --git a/nextjs/src/services/fileStorage/utils/fileSystemUtils.ts b/nextjs/src/services/fileStorage/utils/fileSystemUtils.ts
index 7095c64..cc025ae 100644
--- a/nextjs/src/services/fileStorage/utils/fileSystemUtils.ts
+++ b/nextjs/src/services/fileStorage/utils/fileSystemUtils.ts
@@ -1,4 +1,5 @@
import { promises as fs } from "fs";
+import path from "path";
export const hasFileSystemEntries = async (
directoryPath: string
@@ -19,6 +20,32 @@ export const directoryOrFileExists = async (directoryPath: string): Promise dirent.isDirectory())
+ .map(async (dirent) => {
+ const coursePath = path.join(basePath, dirent.name);
+ const settingsPath = path.join(coursePath, "settings.yml");
+ const hasSettings = await directoryOrFileExists(settingsPath);
+ return {
+ dirent,
+ hasSettings,
+ };
+ })
+ );
+
+ const courseNamesFromDirectories = coursePromises
+ .filter(({ hasSettings }) => hasSettings)
+ .map(({ dirent }) => dirent.name);
+
+ return courseNamesFromDirectories;
+}
+
export const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";