mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
starting to resurect tests
This commit is contained in:
@@ -1,14 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useLocalCourseNamesQuery } from "@/hooks/localCourse/localCoursesHooks";
|
import { useLocalCoursesSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function CourseList() {
|
export default function CourseList() {
|
||||||
const { data: courses } = useLocalCourseNamesQuery();
|
const { data: allSettings } = useLocalCoursesSettingsQuery();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{courses.map((c) => (
|
{allSettings.map((settings) => (
|
||||||
<Link href={`/course/${c}`} key={c} shallow={true}>
|
<Link
|
||||||
{c}{" "}
|
href={getCourseUrl(settings.name)}
|
||||||
|
key={settings.name}
|
||||||
|
shallow={true}
|
||||||
|
>
|
||||||
|
{settings.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
|
|
||||||
import SelectInput from "@/components/form/SelectInput";
|
|
||||||
import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks";
|
|
||||||
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
|
|
||||||
import { useEmptyDirectoriesQuery } from "@/hooks/localCourse/storageDirectoryHooks";
|
|
||||||
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
|
||||||
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
|
||||||
import { DayOfWeek } from "@/models/local/localCourse";
|
|
||||||
import React, { useMemo, useState } from "react";
|
|
||||||
|
|
||||||
const sampleCompose = `
|
|
||||||
services:
|
|
||||||
canvas_manager:
|
|
||||||
image: alexmickelson/canvas_management:2
|
|
||||||
user: 1000:1000 # userid:groupid that matches file ownership on host system
|
|
||||||
ports:
|
|
||||||
- 8080:8080 # hostPort:containerPort - you can change the first one if you like
|
|
||||||
env_file:
|
|
||||||
- .env # needs to have your CANVAS_TOKEN set
|
|
||||||
environment:
|
|
||||||
- storageDirectory=/app/storage
|
|
||||||
- TZ=America/Denver
|
|
||||||
volumes:
|
|
||||||
- ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/UX
|
|
||||||
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend
|
|
||||||
- ~/projects/faculty/1810/2024-fall-alex/modules:/app/storage/intro_to_web
|
|
||||||
- ~/projects/faculty/1420/2024-fall/Modules:/app/storage/1420
|
|
||||||
- ~/projects/faculty/1425/2024-fall/Modules:/app/storage/1425
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function NewCourseForm() {
|
|
||||||
const today = useMemo(() => new Date(), []);
|
|
||||||
const { data: canvasTerms } = useCanvasTermsQuery(today);
|
|
||||||
const { data: emptyDirectories } = useEmptyDirectoriesQuery();
|
|
||||||
const [selectedTerm, setSelectedTerm] = useState<
|
|
||||||
CanvasEnrollmentTermModel | undefined
|
|
||||||
>();
|
|
||||||
const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm?.id);
|
|
||||||
const [selectedDaysOfWeek, setSelectedDaysOfWeek] = useState<DayOfWeek[]>([]);
|
|
||||||
const [selectedCanvasCourse, setSelectedCanvasCourse] = useState<
|
|
||||||
CanvasCourseModel | undefined
|
|
||||||
>();
|
|
||||||
const [selectedDirectory, setSelectedDirectory] = useState<
|
|
||||||
string | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SelectInput
|
|
||||||
value={selectedTerm}
|
|
||||||
setValue={setSelectedTerm}
|
|
||||||
label={"Canvas Term"}
|
|
||||||
options={canvasTerms}
|
|
||||||
getOptionName={(t) => t.name}
|
|
||||||
/>
|
|
||||||
{selectedTerm && (
|
|
||||||
<>
|
|
||||||
<SelectInput
|
|
||||||
value={selectedCanvasCourse}
|
|
||||||
setValue={setSelectedCanvasCourse}
|
|
||||||
label={"Course"}
|
|
||||||
options={canvasCourses}
|
|
||||||
getOptionName={(c) => c.name}
|
|
||||||
/>
|
|
||||||
<SelectInput
|
|
||||||
value={selectedDirectory}
|
|
||||||
setValue={setSelectedDirectory}
|
|
||||||
label={"Storage Folder"}
|
|
||||||
options={emptyDirectories}
|
|
||||||
getOptionName={(d) => d}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<DayOfWeekInput
|
|
||||||
selectedDays={selectedDaysOfWeek}
|
|
||||||
updateSettings={(day) => {
|
|
||||||
setSelectedDaysOfWeek((oldDays) => {
|
|
||||||
const hasDay = oldDays.includes(day);
|
|
||||||
|
|
||||||
return hasDay
|
|
||||||
? oldDays.filter((d) => d !== day)
|
|
||||||
: [day, ...oldDays];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
|
import { LocalCourse } from "@/models/local/localCourse";
|
||||||
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||||
import { withErrorHandling } from "@/services/withErrorHandling";
|
import { withErrorHandling } from "@/services/withErrorHandling";
|
||||||
|
|
||||||
export const GET = async () =>
|
export const GET = async () =>
|
||||||
await withErrorHandling(async () => {
|
await withErrorHandling(async () => {
|
||||||
const courses = await fileStorageService.getCourseNames();
|
const courses = await fileStorageService.getCourseNames();
|
||||||
return Response.json(courses);
|
return Response.json(courses);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const POST = async (request: Request) =>
|
||||||
|
await withErrorHandling(async () => {
|
||||||
|
const newCourse: LocalCourse = await request.json();
|
||||||
|
await fileStorageService.updateCourseSettings(
|
||||||
|
newCourse.settings.name,
|
||||||
|
newCourse.settings
|
||||||
|
);
|
||||||
|
return Response.json({});
|
||||||
|
});
|
||||||
|
|||||||
9
nextjs/src/app/api/courses/settings/route.ts
Normal file
9
nextjs/src/app/api/courses/settings/route.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||||
|
import { withErrorHandling } from "@/services/withErrorHandling";
|
||||||
|
|
||||||
|
export const GET = async () =>
|
||||||
|
await withErrorHandling(async () => {
|
||||||
|
const settings = await fileStorageService.getAllCoursesSettings();
|
||||||
|
|
||||||
|
return Response.json(settings);
|
||||||
|
});
|
||||||
@@ -3,22 +3,16 @@
|
|||||||
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCourseContext } from "./context/courseContext";
|
import { useCourseContext } from "./context/courseContext";
|
||||||
|
import { getCourseSettingsUrl } from "@/services/urlUtils";
|
||||||
|
|
||||||
export default function CourseSettingsLink() {
|
export default function CourseSettingsLink() {
|
||||||
const {courseName} = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
const { data: settings } = useLocalCourseSettingsQuery();
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{settings.name}
|
{settings.name}
|
||||||
|
|
||||||
<Link
|
<Link href={getCourseSettingsUrl(courseName)} shallow={true}>
|
||||||
href={
|
|
||||||
"/course/" +
|
|
||||||
encodeURIComponent(courseName) +
|
|
||||||
"/settings"
|
|
||||||
}
|
|
||||||
shallow={true}
|
|
||||||
>
|
|
||||||
Course Settings
|
Course Settings
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { useCourseContext } from "../context/courseContext";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { IModuleItem } from "@/models/local/IModuleItem";
|
import { IModuleItem } from "@/models/local/IModuleItem";
|
||||||
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import { getDayOfWeek } from "@/models/local/localCourse";
|
import { getDayOfWeek } from "@/models/local/localCourse";
|
||||||
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
|
|
||||||
export default function Day({ day, month }: { day: string; month: number }) {
|
export default function Day({ day, month }: { day: string; month: number }) {
|
||||||
const dayAsDate = getDateFromStringOrThrow(
|
const dayAsDate = getDateFromStringOrThrow(
|
||||||
@@ -32,13 +33,11 @@ export default function Day({ day, month }: { day: string; month: number }) {
|
|||||||
const classIsToday = settings.daysOfWeek.includes(getDayOfWeek(dayAsDate));
|
const classIsToday = settings.daysOfWeek.includes(getDayOfWeek(dayAsDate));
|
||||||
|
|
||||||
const todayClass = classIsToday ? " bg-slate-900 " : " ";
|
const todayClass = classIsToday ? " bg-slate-900 " : " ";
|
||||||
const monthClass = isInSameMonth ? " border border-slate-600 " : " "
|
const monthClass = isInSameMonth ? " border border-slate-600 " : " ";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={" rounded-lg p-2 pb-4 m-1 " + todayClass + monthClass}
|
||||||
" rounded-lg p-2 pb-4 m-1 " + todayClass + monthClass
|
|
||||||
}
|
|
||||||
onDrop={(e) => itemDrop(e, day)}
|
onDrop={(e) => itemDrop(e, day)}
|
||||||
onDragOver={(e) => e.preventDefault()}
|
onDragOver={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
@@ -135,14 +134,7 @@ function DraggableListItem({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={getModuleItemUrl(courseName, moduleName, type, item.name)}
|
||||||
"/course/" +
|
|
||||||
encodeURIComponent(courseName) +
|
|
||||||
"/modules/" +
|
|
||||||
encodeURIComponent(moduleName) +
|
|
||||||
`/${type}/` +
|
|
||||||
encodeURIComponent(item.name)
|
|
||||||
}
|
|
||||||
shallow={true}
|
shallow={true}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import { useCourseContext } from "../context/courseContext";
|
|
||||||
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
usePageNamesQuery,
|
|
||||||
usePagesQueries,
|
|
||||||
} from "@/hooks/localCourse/pageHooks";
|
|
||||||
import {
|
|
||||||
useQuizNamesQuery,
|
|
||||||
useQuizzesQueries,
|
|
||||||
} from "@/hooks/localCourse/quizHooks";
|
|
||||||
import {
|
|
||||||
useAssignmentNamesQuery,
|
|
||||||
useAssignmentsQueries,
|
|
||||||
} from "@/hooks/localCourse/assignmentHooks";
|
|
||||||
|
|
||||||
export default function DayItemsInModule({
|
|
||||||
day,
|
|
||||||
moduleName,
|
|
||||||
}: {
|
|
||||||
day: string;
|
|
||||||
moduleName: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ul className="list-disc ms-4">
|
|
||||||
<Assignments moduleName={moduleName} day={day} />
|
|
||||||
<Quizzes moduleName={moduleName} day={day} />
|
|
||||||
<Pages moduleName={moduleName} day={day} />
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Pages({ moduleName, day }: { moduleName: string; day: string }) {
|
|
||||||
const { courseName } = useCourseContext();
|
|
||||||
const { data: pageNames } = usePageNamesQuery(moduleName);
|
|
||||||
const { data: pages } = usePagesQueries(moduleName, pageNames);
|
|
||||||
const todaysPages = useMemo(
|
|
||||||
() =>
|
|
||||||
pages.filter((p) => {
|
|
||||||
const dueDate = getDateFromStringOrThrow(
|
|
||||||
p.dueAt,
|
|
||||||
"due at for page in day"
|
|
||||||
);
|
|
||||||
const dayAsDate = getDateFromStringOrThrow(
|
|
||||||
day,
|
|
||||||
"in pages in DayItemsInModule"
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
dueDate.getFullYear() === dayAsDate.getFullYear() &&
|
|
||||||
dueDate.getMonth() === dayAsDate.getMonth() &&
|
|
||||||
dueDate.getDate() === dayAsDate.getDate()
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[day, pages]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{todaysPages.map((p) => (
|
|
||||||
<li
|
|
||||||
key={p.name}
|
|
||||||
role="button"
|
|
||||||
draggable="true"
|
|
||||||
onDragStart={(e) => {
|
|
||||||
e.dataTransfer.setData(
|
|
||||||
"draggableItem",
|
|
||||||
JSON.stringify({
|
|
||||||
type: "page",
|
|
||||||
item: p,
|
|
||||||
sourceModuleName: moduleName,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
"/course/" +
|
|
||||||
encodeURIComponent(courseName) +
|
|
||||||
"/modules/" +
|
|
||||||
encodeURIComponent(moduleName) +
|
|
||||||
"/page/" +
|
|
||||||
encodeURIComponent(p.name)
|
|
||||||
}
|
|
||||||
shallow={true}
|
|
||||||
>
|
|
||||||
{p.name}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Quizzes({ moduleName, day }: { moduleName: string; day: string }) {
|
|
||||||
const { data: quizNames } = useQuizNamesQuery(moduleName);
|
|
||||||
const { data: quizzes } = useQuizzesQueries(moduleName, quizNames);
|
|
||||||
const { courseName } = useCourseContext();
|
|
||||||
|
|
||||||
const todaysQuizzes = useMemo(
|
|
||||||
() =>
|
|
||||||
quizzes.filter((q) => {
|
|
||||||
const dueDate = getDateFromStringOrThrow(
|
|
||||||
q.dueAt,
|
|
||||||
"due at for quiz in day"
|
|
||||||
);
|
|
||||||
const dayAsDate = getDateFromStringOrThrow(
|
|
||||||
day,
|
|
||||||
"in quizzes in DayItemsInModule"
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
dueDate.getFullYear() === dayAsDate.getFullYear() &&
|
|
||||||
dueDate.getMonth() === dayAsDate.getMonth() &&
|
|
||||||
dueDate.getDate() === dayAsDate.getDate()
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[day, quizzes]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{todaysQuizzes.map((q) => (
|
|
||||||
<li
|
|
||||||
key={q.name}
|
|
||||||
role="button"
|
|
||||||
draggable="true"
|
|
||||||
onDragStart={(e) => {
|
|
||||||
e.dataTransfer.setData(
|
|
||||||
"draggableItem",
|
|
||||||
JSON.stringify({
|
|
||||||
type: "quiz",
|
|
||||||
item: q,
|
|
||||||
sourceModuleName: moduleName,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onDragEnd={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
"/course/" +
|
|
||||||
encodeURIComponent(courseName) +
|
|
||||||
"/modules/" +
|
|
||||||
encodeURIComponent(moduleName) +
|
|
||||||
"/quiz/" +
|
|
||||||
encodeURIComponent(q.name)
|
|
||||||
}
|
|
||||||
shallow={true}
|
|
||||||
>
|
|
||||||
{q.name}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Assignments({ moduleName, day }: { moduleName: string; day: string }) {
|
|
||||||
const { data: assignmentNames } = useAssignmentNamesQuery(moduleName);
|
|
||||||
const { courseName } = useCourseContext();
|
|
||||||
const { data: assignments } = useAssignmentsQueries(
|
|
||||||
moduleName,
|
|
||||||
assignmentNames
|
|
||||||
);
|
|
||||||
const todaysAssignments = useMemo(
|
|
||||||
() =>
|
|
||||||
assignments.filter((a) => {
|
|
||||||
const dueDate = getDateFromStringOrThrow(
|
|
||||||
a.dueAt,
|
|
||||||
"due at for assignment in day"
|
|
||||||
);
|
|
||||||
const dayAsDate = getDateFromStringOrThrow(
|
|
||||||
day,
|
|
||||||
"in assignment in DayItemsInModule"
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
dueDate.getFullYear() === dayAsDate.getFullYear() &&
|
|
||||||
dueDate.getMonth() === dayAsDate.getMonth() &&
|
|
||||||
dueDate.getDate() === dayAsDate.getDate()
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[assignments, day]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{todaysAssignments.map((a) => (
|
|
||||||
<li
|
|
||||||
key={a.name}
|
|
||||||
role="button"
|
|
||||||
draggable="true"
|
|
||||||
onDragStart={(e) => {
|
|
||||||
e.dataTransfer.setData(
|
|
||||||
"draggableItem",
|
|
||||||
JSON.stringify({
|
|
||||||
type: "assignment",
|
|
||||||
item: a,
|
|
||||||
sourceModuleName: moduleName,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
"/course/" +
|
|
||||||
encodeURIComponent(courseName) +
|
|
||||||
"/modules/" +
|
|
||||||
encodeURIComponent(moduleName) +
|
|
||||||
"/assignment/" +
|
|
||||||
encodeURIComponent(a.name)
|
|
||||||
}
|
|
||||||
shallow={true}
|
|
||||||
>
|
|
||||||
{a.name}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,7 @@ blockquote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@apply font-mono text-sm bg-gray-800 px-1;
|
@apply font-mono text-sm bg-gray-800 px-1 leading-tight inline-block;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
@apply mb-3;
|
@apply mb-3;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import SelectInput from "@/components/form/SelectInput";
|
|
||||||
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import NewCourseForm from "./NewCourseForm";
|
|
||||||
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
|
import NewCourseForm from "./NewCourseForm";
|
||||||
|
|
||||||
export default function AddNewCourse() {
|
export default function AddNewCourse() {
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
@@ -14,10 +12,9 @@ export default function AddNewCourse() {
|
|||||||
|
|
||||||
<div className={" collapsable " + (showForm && "expand")}>
|
<div className={" collapsable " + (showForm && "expand")}>
|
||||||
<div className="border rounded-md p-3 m-3">
|
<div className="border rounded-md p-3 m-3">
|
||||||
|
<SuspenseAndErrorHandling>
|
||||||
<SuspenseAndErrorHandling>
|
{showForm && <NewCourseForm />}
|
||||||
{showForm && <NewCourseForm />}
|
</SuspenseAndErrorHandling>
|
||||||
</SuspenseAndErrorHandling>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
182
nextjs/src/app/newCourse/NewCourseForm.tsx
Normal file
182
nextjs/src/app/newCourse/NewCourseForm.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"use client";
|
||||||
|
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
|
||||||
|
import SelectInput from "@/components/form/SelectInput";
|
||||||
|
import { Spinner } from "@/components/Spinner";
|
||||||
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
|
import { useCourseListInTermQuery } from "@/hooks/canvas/canvasCourseHooks";
|
||||||
|
import { useCanvasTermsQuery } from "@/hooks/canvas/canvasHooks";
|
||||||
|
import {
|
||||||
|
useCreateLocalCourseMutation,
|
||||||
|
useLocalCoursesSettingsQuery,
|
||||||
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
|
import { useEmptyDirectoriesQuery } from "@/hooks/localCourse/storageDirectoryHooks";
|
||||||
|
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
||||||
|
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
||||||
|
import { DayOfWeek } from "@/models/local/localCourse";
|
||||||
|
import { getCourseUrl } from "@/services/urlUtils";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const sampleCompose = `services:
|
||||||
|
canvas_manager:
|
||||||
|
image: alexmickelson/canvas_management:2 # pull this image regularly
|
||||||
|
user: 1000:1000 # userid:groupid that matches file ownership on host system
|
||||||
|
ports:
|
||||||
|
- 8080:8080 # hostPort:containerPort - you can change the first one if you like
|
||||||
|
env_file:
|
||||||
|
- .env # needs to have your CANVAS_TOKEN set
|
||||||
|
environment:
|
||||||
|
- TZ=America/Denver # prevents timezone issues for due dates
|
||||||
|
volumes:
|
||||||
|
- ~/projects/faculty/1430/2024-fall-alex/modules:/app/storage/UX
|
||||||
|
- ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function NewCourseForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const today = useMemo(() => new Date(), []);
|
||||||
|
const { data: canvasTerms } = useCanvasTermsQuery(today);
|
||||||
|
const [selectedTerm, setSelectedTerm] = useState<
|
||||||
|
CanvasEnrollmentTermModel | undefined
|
||||||
|
>();
|
||||||
|
const [selectedDaysOfWeek, setSelectedDaysOfWeek] = useState<DayOfWeek[]>([]);
|
||||||
|
const [selectedCanvasCourse, setSelectedCanvasCourse] = useState<
|
||||||
|
CanvasCourseModel | undefined
|
||||||
|
>();
|
||||||
|
const [selectedDirectory, setSelectedDirectory] = useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
|
const createCourse = useCreateLocalCourseMutation();
|
||||||
|
|
||||||
|
const formIsComplete =
|
||||||
|
selectedTerm && selectedCanvasCourse && selectedDirectory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SelectInput
|
||||||
|
value={selectedTerm}
|
||||||
|
setValue={setSelectedTerm}
|
||||||
|
label={"Canvas Term"}
|
||||||
|
options={canvasTerms}
|
||||||
|
getOptionName={(t) => t.name}
|
||||||
|
/>
|
||||||
|
<SuspenseAndErrorHandling>
|
||||||
|
{selectedTerm && (
|
||||||
|
<OtherSettings
|
||||||
|
selectedTerm={selectedTerm}
|
||||||
|
selectedCanvasCourse={selectedCanvasCourse}
|
||||||
|
setSelectedCanvasCourse={setSelectedCanvasCourse}
|
||||||
|
selectedDirectory={selectedDirectory}
|
||||||
|
setSelectedDirectory={setSelectedDirectory}
|
||||||
|
selectedDaysOfWeek={selectedDaysOfWeek}
|
||||||
|
setSelectedDaysOfWeek={setSelectedDaysOfWeek}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SuspenseAndErrorHandling>
|
||||||
|
<div className="m-3 text-center">
|
||||||
|
<button
|
||||||
|
disabled={!formIsComplete || createCourse.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
if (formIsComplete) {
|
||||||
|
createCourse
|
||||||
|
.mutateAsync({
|
||||||
|
modules: [],
|
||||||
|
settings: {
|
||||||
|
name: selectedDirectory,
|
||||||
|
assignmentGroups: [],
|
||||||
|
daysOfWeek: selectedDaysOfWeek,
|
||||||
|
canvasId: selectedCanvasCourse.id,
|
||||||
|
startDate: selectedTerm.start_at ?? "",
|
||||||
|
endDate: selectedTerm.start_at ?? "",
|
||||||
|
defaultDueTime: { hour: 11, minute: 59 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
router.push(getCourseUrl(selectedDirectory), undefined, {
|
||||||
|
shallow: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save New Course Configuration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{createCourse.isPending && <Spinner />}
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<div>Example docker compose</div>
|
||||||
|
<code className="language-yml">{sampleCompose}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OtherSettings({
|
||||||
|
selectedTerm,
|
||||||
|
selectedCanvasCourse,
|
||||||
|
setSelectedCanvasCourse,
|
||||||
|
selectedDirectory,
|
||||||
|
setSelectedDirectory,
|
||||||
|
selectedDaysOfWeek,
|
||||||
|
setSelectedDaysOfWeek,
|
||||||
|
}: {
|
||||||
|
selectedTerm: CanvasEnrollmentTermModel;
|
||||||
|
selectedCanvasCourse: CanvasCourseModel | undefined;
|
||||||
|
setSelectedCanvasCourse: React.Dispatch<
|
||||||
|
React.SetStateAction<CanvasCourseModel | undefined>
|
||||||
|
>;
|
||||||
|
selectedDirectory: string | undefined;
|
||||||
|
setSelectedDirectory: React.Dispatch<
|
||||||
|
React.SetStateAction<string | undefined>
|
||||||
|
>;
|
||||||
|
selectedDaysOfWeek: DayOfWeek[];
|
||||||
|
setSelectedDaysOfWeek: React.Dispatch<React.SetStateAction<DayOfWeek[]>>;
|
||||||
|
}) {
|
||||||
|
const { data: canvasCourses } = useCourseListInTermQuery(selectedTerm.id);
|
||||||
|
const { data: allSettings } = useLocalCoursesSettingsQuery();
|
||||||
|
const { data: emptyDirectories } = useEmptyDirectoriesQuery();
|
||||||
|
|
||||||
|
const populatedCanvasCourseIds = allSettings.map((s) => s.canvasId);
|
||||||
|
const availableCourses = canvasCourses.filter(
|
||||||
|
(canvas) => !populatedCanvasCourseIds.includes(canvas.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectInput
|
||||||
|
value={selectedCanvasCourse}
|
||||||
|
setValue={setSelectedCanvasCourse}
|
||||||
|
label={"Course"}
|
||||||
|
options={availableCourses}
|
||||||
|
getOptionName={(c) => c.name}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
value={selectedDirectory}
|
||||||
|
setValue={setSelectedDirectory}
|
||||||
|
label={"Storage Folder"}
|
||||||
|
options={emptyDirectories}
|
||||||
|
getOptionName={(d) => d}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
New folders will not be created automatically, you are expected to mount
|
||||||
|
a docker volume for each coures.
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<DayOfWeekInput
|
||||||
|
selectedDays={selectedDaysOfWeek}
|
||||||
|
updateSettings={(day) => {
|
||||||
|
setSelectedDaysOfWeek((oldDays) => {
|
||||||
|
const hasDay = oldDays.includes(day);
|
||||||
|
|
||||||
|
return hasDay
|
||||||
|
? oldDays.filter((d) => d !== day)
|
||||||
|
: [day, ...oldDays];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import AddNewCourse from "./AddNewCourse";
|
|
||||||
import CourseList from "./CourseList";
|
import CourseList from "./CourseList";
|
||||||
|
import AddNewCourse from "./newCourse/AddNewCourse";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
import { localCourseKeys } from "./localCourse/localCourseKeys";
|
import { localCourseKeys } from "./localCourse/localCourseKeys";
|
||||||
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
|
||||||
|
import { LocalCourseSettings } from "@/models/local/localCourse";
|
||||||
// https://tanstack.com/query/latest/docs/framework/react/guides/ssr
|
// https://tanstack.com/query/latest/docs/framework/react/guides/ssr
|
||||||
export const hydrateCourses = async (queryClient: QueryClient) => {
|
export const hydrateCourses = async (queryClient: QueryClient) => {
|
||||||
const courseNames = await fileStorageService.getCourseNames();
|
const allSettings = await fileStorageService.getAllCoursesSettings();
|
||||||
|
const courseNames = allSettings.map((s) => s.name);
|
||||||
await queryClient.prefetchQuery({
|
await queryClient.prefetchQuery({
|
||||||
queryKey: localCourseKeys.allCourses,
|
queryKey: localCourseKeys.allCoursesSettings,
|
||||||
queryFn: () => courseNames,
|
queryFn: () => allSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
courseNames.map(async (courseName) => {
|
allSettings.map(async (settings) => {
|
||||||
await hydrateCourse(queryClient, courseName);
|
await hydrateCourse(queryClient, settings);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hydrateCourse = async (
|
export const hydrateCourse = async (
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
courseName: string
|
courseSettings: LocalCourseSettings
|
||||||
) => {
|
) => {
|
||||||
const settings = await fileStorageService.getCourseSettings(courseName);
|
const courseName = courseSettings.name
|
||||||
const moduleNames = await fileStorageService.getModuleNames(courseName);
|
const moduleNames = await fileStorageService.getModuleNames(courseName);
|
||||||
const modulesData = await Promise.all(
|
const modulesData = await Promise.all(
|
||||||
moduleNames.map(async (moduleName) => {
|
moduleNames.map(async (moduleName) => {
|
||||||
@@ -69,7 +71,7 @@ export const hydrateCourse = async (
|
|||||||
|
|
||||||
await queryClient.prefetchQuery({
|
await queryClient.prefetchQuery({
|
||||||
queryKey: localCourseKeys.settings(courseName),
|
queryKey: localCourseKeys.settings(courseName),
|
||||||
queryFn: () => settings,
|
queryFn: () => courseSettings,
|
||||||
});
|
});
|
||||||
await queryClient.prefetchQuery({
|
await queryClient.prefetchQuery({
|
||||||
queryKey: localCourseKeys.moduleNames(courseName),
|
queryKey: localCourseKeys.moduleNames(courseName),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const localCourseKeys = {
|
export const localCourseKeys = {
|
||||||
allCourses: ["all courses"] as const,
|
allCoursesSettings: ["all courses settings"] as const,
|
||||||
|
allCoursesNames: ["all courses names"] as const,
|
||||||
settings: (courseName: string) =>
|
settings: (courseName: string) =>
|
||||||
["course details", courseName, "settings"] as const,
|
["course details", courseName, "settings"] as const,
|
||||||
moduleNames: (courseName: string) =>
|
moduleNames: (courseName: string) =>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { LocalCourseSettings } from "@/models/local/localCourse";
|
import { LocalCourse, LocalCourseSettings } from "@/models/local/localCourse";
|
||||||
import {
|
import {
|
||||||
useMutation,
|
useMutation,
|
||||||
useQueries,
|
useQueries,
|
||||||
@@ -30,24 +30,43 @@ import {
|
|||||||
} from "./quizHooks";
|
} from "./quizHooks";
|
||||||
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
|
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
|
||||||
|
|
||||||
export const useLocalCourseNamesQuery = () =>
|
export const useLocalCoursesSettingsQuery = () =>
|
||||||
useSuspenseQuery({
|
useSuspenseQuery({
|
||||||
queryKey: localCourseKeys.allCourses,
|
queryKey: localCourseKeys.allCoursesSettings,
|
||||||
queryFn: async (): Promise<string[]> => {
|
queryFn: async () => {
|
||||||
const url = `/api/courses`;
|
const url = `/api/courses/settings`;
|
||||||
const response = await axiosClient.get(url);
|
const response = await axiosClient.get<LocalCourseSettings[]>(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useLocalCourseSettingsQuery = () => {
|
export const useLocalCourseSettingsQuery = () => {
|
||||||
const { courseName } = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
|
const { data: settingsList } = useLocalCoursesSettingsQuery();
|
||||||
return useSuspenseQuery({
|
return useSuspenseQuery({
|
||||||
queryKey: localCourseKeys.settings(courseName),
|
queryKey: localCourseKeys.settings(courseName),
|
||||||
queryFn: async (): Promise<LocalCourseSettings> => {
|
queryFn: () => {
|
||||||
const url = `/api/courses/${courseName}/settings`;
|
const s = settingsList.find((s) => s.name === courseName);
|
||||||
const response = await axiosClient.get(url);
|
if (!s) {
|
||||||
return response.data;
|
console.log(courseName, settingsList);
|
||||||
|
throw Error("Could not find settings for course " + courseName);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateLocalCourseMutation = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (newCourse: LocalCourse) => {
|
||||||
|
const url = `/api/courses`;
|
||||||
|
await axiosClient.post(url, newCourse);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: localCourseKeys.allCoursesSettings,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -68,6 +87,9 @@ export const useUpdateLocalCourseSettingsMutation = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: localCourseKeys.settings(courseName),
|
queryKey: localCourseKeys.settings(courseName),
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: localCourseKeys.allCoursesSettings,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ describe("Can properly handle expected date formats", () => {
|
|||||||
const dateObject = getDateFromString(dateString);
|
const dateObject = getDateFromString(dateString);
|
||||||
expect(dateObject).not.toBeUndefined();
|
expect(dateObject).not.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
it("can use other ISO format", () => {
|
||||||
|
const dateString = "2024-08-26T06:00:00Z";
|
||||||
|
const dateObject = getDateFromString(dateString);
|
||||||
|
expect(dateObject).not.toBeUndefined();
|
||||||
|
});
|
||||||
it("can get correct time from format", () => {
|
it("can get correct time from format", () => {
|
||||||
const dateString = "08/28/2024 23:59:00";
|
const dateString = "08/28/2024 23:59:00";
|
||||||
const dateObject = getDateFromString(dateString);
|
const dateObject = getDateFromString(dateString);
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ const _getDateFromISO = (value: string): Date | undefined => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getDateFromString = (value: string): Date | undefined => {
|
export const getDateFromString = (value: string): Date | undefined => {
|
||||||
|
|
||||||
const ampmDateRegex =
|
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"
|
/^\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"
|
||||||
const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/; //"MM/DD/YYYY HH:mm:ss"
|
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+)$/; //"2024-08-26T00:00:00.0000000"
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}((.\d+)|(Z))$/; //"2024-08-26T00:00:00.0000000"
|
||||||
|
|
||||||
if (isoDateRegex.test(value)) {
|
if (isoDateRegex.test(value)) {
|
||||||
return _getDateFromISO(value);
|
return _getDateFromISO(value);
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import {
|
|||||||
LocalCourseSettings,
|
LocalCourseSettings,
|
||||||
localCourseYamlUtils,
|
localCourseYamlUtils,
|
||||||
} from "@/models/local/localCourse";
|
} from "@/models/local/localCourse";
|
||||||
import {
|
import { directoryOrFileExists } from "./utils/fileSystemUtils";
|
||||||
directoryOrFileExists,
|
|
||||||
hasFileSystemEntries,
|
|
||||||
} from "./utils/fileSystemUtils";
|
|
||||||
import {
|
import {
|
||||||
LocalAssignment,
|
LocalAssignment,
|
||||||
localAssignmentMarkdown,
|
localAssignmentMarkdown,
|
||||||
@@ -53,6 +50,15 @@ export const fileStorageService = {
|
|||||||
return courseNamesFromDirectories;
|
return courseNamesFromDirectories;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getAllCoursesSettings() {
|
||||||
|
const courses = await fileStorageService.getCourseNames();
|
||||||
|
|
||||||
|
const settings = await Promise.all(
|
||||||
|
courses.map(async (c) => await fileStorageService.getCourseSettings(c))
|
||||||
|
);
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
|
||||||
async getCourseSettings(courseName: string): Promise<LocalCourseSettings> {
|
async getCourseSettings(courseName: string): Promise<LocalCourseSettings> {
|
||||||
const courseDirectory = path.join(basePath, courseName);
|
const courseDirectory = path.join(basePath, courseName);
|
||||||
const settingsPath = path.join(courseDirectory, "settings.yml");
|
const settingsPath = path.join(courseDirectory, "settings.yml");
|
||||||
@@ -97,6 +103,11 @@ export const fileStorageService = {
|
|||||||
const modules = await Promise.all(modulePromises);
|
const modules = await Promise.all(modulePromises);
|
||||||
return modules.sort((a, b) => a.localeCompare(b));
|
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 });
|
||||||
|
},
|
||||||
|
|
||||||
async getAssignmentNames(courseName: string, moduleName: string) {
|
async getAssignmentNames(courseName: string, moduleName: string) {
|
||||||
const filePath = path.join(basePath, courseName, moduleName, "assignments");
|
const filePath = path.join(basePath, courseName, moduleName, "assignments");
|
||||||
@@ -263,7 +274,6 @@ export const fileStorageService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directories = await fs.readdir(basePath, { withFileTypes: true });
|
const directories = await fs.readdir(basePath, { withFileTypes: true });
|
||||||
console.log(directories);
|
|
||||||
const emptyDirectories = (
|
const emptyDirectories = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
directories
|
directories
|
||||||
@@ -282,4 +292,10 @@ export const fileStorageService = {
|
|||||||
|
|
||||||
return emptyDirectories;
|
return emptyDirectories;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async createCourseFolderForTesting(courseName: string) {
|
||||||
|
const courseDirectory = path.join(basePath, courseName);
|
||||||
|
|
||||||
|
await fs.mkdir(courseDirectory, { recursive: true });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,293 +1,252 @@
|
|||||||
// import path from "path";
|
import path from "path";
|
||||||
// import { describe, it, expect, beforeEach } from "vitest";
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
// import fs from "fs";
|
import fs from "fs";
|
||||||
// import { DayOfWeek, LocalCourse } from "@/models/local/localCourse";
|
import {
|
||||||
// import { AssignmentSubmissionType } from "@/models/local/assignmnet/assignmentSubmissionType";
|
DayOfWeek,
|
||||||
// import { QuestionType } from "@/models/local/quiz/localQuizQuestion";
|
LocalCourse,
|
||||||
// import { fileStorageService } from "../fileStorage/fileStorageService";
|
LocalCourseSettings,
|
||||||
|
} from "@/models/local/localCourse";
|
||||||
|
import { QuestionType } from "@/models/local/quiz/localQuizQuestion";
|
||||||
|
import { fileStorageService } from "../fileStorage/fileStorageService";
|
||||||
|
|
||||||
// describe("FileStorageTests", () => {
|
describe("FileStorageTests", () => {
|
||||||
// beforeEach(() => {
|
beforeEach(() => {
|
||||||
// const storageDirectory = process.env.STORAGE_DIRECTORY ?? "/tmp/canvasManagerTests";
|
const storageDirectory =
|
||||||
// if (fs.existsSync(storageDirectory)) {
|
process.env.STORAGE_DIRECTORY ?? "/tmp/canvasManagerTests";
|
||||||
// fs.rmdirSync(storageDirectory, { recursive: true });
|
if (fs.existsSync(storageDirectory)) {
|
||||||
// }
|
fs.rmdirSync(storageDirectory, { recursive: true });
|
||||||
// fs.mkdirSync(storageDirectory, { recursive: true });
|
}
|
||||||
// });
|
fs.mkdirSync(storageDirectory, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
// it("empty course can be saved and loaded", async () => {
|
it("course settings can be saved and loaded", async () => {
|
||||||
// const testCourse: LocalCourse = {
|
const name = "test empty course";
|
||||||
// settings: {
|
await fileStorageService.createCourseFolderForTesting(name);
|
||||||
// name: "test empty course",
|
const settings: LocalCourseSettings = {
|
||||||
// assignmentGroups: [],
|
name,
|
||||||
// daysOfWeek: [],
|
assignmentGroups: [],
|
||||||
// startDate: "07/09/2024 23:59:00",
|
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
||||||
// endDate: "07/09/2024 23:59:00",
|
startDate: "07/09/2024 23:59:00",
|
||||||
// defaultDueTime: { hour: 1, minute: 59 },
|
endDate: "07/09/2024 23:59:00",
|
||||||
// },
|
defaultDueTime: { hour: 1, minute: 59 },
|
||||||
// modules: [],
|
canvasId: 0,
|
||||||
// };
|
};
|
||||||
|
|
||||||
// await fileStorageService.saveCourseAsync(testCourse);
|
await fileStorageService.updateCourseSettings(name, settings);
|
||||||
|
|
||||||
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
const loadedSettings = await fileStorageService.getCourseSettings(name);
|
||||||
// const loadedCourse = loadedCourses.find(
|
|
||||||
// (c) => c.settings.name === testCourse.settings.name
|
|
||||||
// );
|
|
||||||
|
|
||||||
// expect(loadedCourse).toEqual(testCourse);
|
expect(loadedSettings).toEqual(settings);
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("course settings can be saved and loaded", async () => {
|
it("empty course modules can be created", async () => {
|
||||||
// const testCourse: LocalCourse = {
|
const courseName = "test empty course";
|
||||||
// settings: {
|
const moduleName = "test module 1";
|
||||||
// assignmentGroups: [],
|
|
||||||
// name: "Test Course with settings",
|
|
||||||
// daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
|
||||||
// startDate: "07/09/2024 23:59:00",
|
|
||||||
// endDate: "07/09/2024 23:59:00",
|
|
||||||
// defaultDueTime: { hour: 1, minute: 59 },
|
|
||||||
// },
|
|
||||||
// modules: [],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// await fileStorageService.saveCourseAsync(testCourse);
|
await fileStorageService.createModule(courseName, moduleName);
|
||||||
|
|
||||||
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
const moduleNames = await fileStorageService.getModuleNames(courseName);
|
||||||
// const loadedCourse = loadedCourses.find(
|
|
||||||
// (c) => c.settings.name === testCourse.settings.name
|
|
||||||
// );
|
|
||||||
|
|
||||||
// expect(loadedCourse?.settings).toEqual(testCourse.settings);
|
expect(moduleNames).toContain(moduleName);
|
||||||
// });
|
});
|
||||||
|
|
||||||
// it("empty course modules can be saved and loaded", async () => {
|
// it("course modules with assignments can be saved and loaded", async () => {
|
||||||
// const testCourse: LocalCourse = {
|
// const testCourse: LocalCourse = {
|
||||||
// settings: {
|
// settings: {
|
||||||
// name: "Test Course with modules",
|
// name: "Test Course with modules and assignments",
|
||||||
// assignmentGroups: [],
|
// assignmentGroups: [],
|
||||||
// daysOfWeek: [],
|
// daysOfWeek: [],
|
||||||
// startDate: "07/09/2024 23:59:00",
|
// startDate: "07/09/2024 23:59:00",
|
||||||
// endDate: "07/09/2024 23:59:00",
|
// endDate: "07/09/2024 23:59:00",
|
||||||
// defaultDueTime: { hour: 1, minute: 59 },
|
// defaultDueTime: { hour: 1, minute: 59 },
|
||||||
// },
|
// },
|
||||||
// modules: [
|
// modules: [
|
||||||
// {
|
// {
|
||||||
// name: "test module 1",
|
// name: "test module 1 with assignments",
|
||||||
// assignments: [],
|
// assignments: [
|
||||||
// quizzes: [],
|
// {
|
||||||
// pages: [],
|
// name: "test assignment",
|
||||||
// },
|
// description: "here is the description",
|
||||||
// ],
|
// dueAt: "07/09/2024 23:59:00",
|
||||||
// };
|
// lockAt: "07/09/2024 23:59:00",
|
||||||
|
// submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||||
|
// localAssignmentGroupName: "Final Project",
|
||||||
|
// rubric: [
|
||||||
|
// { points: 4, label: "do task 1" },
|
||||||
|
// { points: 2, label: "do task 2" },
|
||||||
|
// ],
|
||||||
|
// allowedFileUploadExtensions: [],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// quizzes: [],
|
||||||
|
// pages: [],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// };
|
||||||
|
|
||||||
// await fileStorageService.saveCourseAsync(testCourse);
|
// await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
// const loadedCourse = loadedCourses.find(
|
// const loadedCourse = loadedCourses.find(
|
||||||
// (c) => c.settings.name === testCourse.settings.name
|
// (c) => c.settings.name === testCourse.settings.name
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// expect(loadedCourse?.modules).toEqual(testCourse.modules);
|
// expect(loadedCourse?.modules[0].assignments).toEqual(
|
||||||
// });
|
// testCourse.modules[0].assignments
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
// it("course modules with assignments can be saved and loaded", async () => {
|
// it("course modules with quizzes can be saved and loaded", async () => {
|
||||||
// const testCourse: LocalCourse = {
|
// const testCourse: LocalCourse = {
|
||||||
// settings: {
|
// settings: {
|
||||||
// name: "Test Course with modules and assignments",
|
// name: "Test Course with modules and quiz",
|
||||||
// assignmentGroups: [],
|
// assignmentGroups: [],
|
||||||
// daysOfWeek: [],
|
// daysOfWeek: [],
|
||||||
// startDate: "07/09/2024 23:59:00",
|
// startDate: "07/09/2024 23:59:00",
|
||||||
// endDate: "07/09/2024 23:59:00",
|
// endDate: "07/09/2024 23:59:00",
|
||||||
// defaultDueTime: { hour: 1, minute: 59 },
|
// defaultDueTime: { hour: 1, minute: 59 },
|
||||||
// },
|
// },
|
||||||
// modules: [
|
// modules: [
|
||||||
// {
|
// {
|
||||||
// name: "test module 1 with assignments",
|
// name: "test module 1 with quiz",
|
||||||
// assignments: [
|
// assignments: [],
|
||||||
// {
|
// quizzes: [
|
||||||
// name: "test assignment",
|
// {
|
||||||
// description: "here is the description",
|
// name: "Test Quiz",
|
||||||
// dueAt: "07/09/2024 23:59:00",
|
// description: "quiz description",
|
||||||
// lockAt: "07/09/2024 23:59:00",
|
// lockAt: "07/09/2024 12:05:00",
|
||||||
// submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
// dueAt: "07/09/2024 12:05:00",
|
||||||
// localAssignmentGroupName: "Final Project",
|
// shuffleAnswers: true,
|
||||||
// rubric: [
|
// oneQuestionAtATime: true,
|
||||||
// { points: 4, label: "do task 1" },
|
// localAssignmentGroupName: "Assignments",
|
||||||
// { points: 2, label: "do task 2" },
|
// questions: [
|
||||||
// ],
|
// {
|
||||||
// allowedFileUploadExtensions: [],
|
// text: "test essay",
|
||||||
// },
|
// questionType: QuestionType.ESSAY,
|
||||||
// ],
|
// points: 1,
|
||||||
// quizzes: [],
|
// answers: [],
|
||||||
// pages: [],
|
// matchDistractors: [],
|
||||||
// },
|
// },
|
||||||
// ],
|
// ],
|
||||||
// };
|
// showCorrectAnswers: false,
|
||||||
|
// allowedAttempts: 0,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// pages: [],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// };
|
||||||
|
|
||||||
// await fileStorageService.saveCourseAsync(testCourse);
|
// await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
// const loadedCourse = loadedCourses.find(
|
// const loadedCourse = loadedCourses.find(
|
||||||
// (c) => c.settings.name === testCourse.settings.name
|
// (c) => c.settings.name === testCourse.settings.name
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// expect(loadedCourse?.modules[0].assignments).toEqual(
|
// expect(loadedCourse?.modules[0].quizzes).toEqual(
|
||||||
// testCourse.modules[0].assignments
|
// testCourse.modules[0].quizzes
|
||||||
// );
|
// );
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// it("course modules with quizzes can be saved and loaded", async () => {
|
// it("markdown storage fully populated does not lose data", async () => {
|
||||||
// const testCourse: LocalCourse = {
|
// const testCourse: LocalCourse = {
|
||||||
// settings: {
|
// settings: {
|
||||||
// name: "Test Course with modules and quiz",
|
// name: "Test Course with lots of data",
|
||||||
// assignmentGroups: [],
|
// assignmentGroups: [],
|
||||||
// daysOfWeek: [],
|
// daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
||||||
// startDate: "07/09/2024 23:59:00",
|
// startDate: "07/09/2024 23:59:00",
|
||||||
// endDate: "07/09/2024 23:59:00",
|
// endDate: "07/09/2024 23:59:00",
|
||||||
// defaultDueTime: { hour: 1, minute: 59 },
|
// defaultDueTime: { hour: 1, minute: 59 },
|
||||||
// },
|
// },
|
||||||
// modules: [
|
// modules: [
|
||||||
// {
|
// {
|
||||||
// name: "test module 1 with quiz",
|
// name: "new test module",
|
||||||
// assignments: [],
|
// assignments: [
|
||||||
// quizzes: [
|
// {
|
||||||
// {
|
// name: "test assignment",
|
||||||
// name: "Test Quiz",
|
// description: "here is the description",
|
||||||
// description: "quiz description",
|
// dueAt: "07/09/2024 23:59:00",
|
||||||
// lockAt: "07/09/2024 12:05:00",
|
// lockAt: "07/09/2024 23:59:00",
|
||||||
// dueAt: "07/09/2024 12:05:00",
|
// submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
||||||
// shuffleAnswers: true,
|
// localAssignmentGroupName: "Final Project",
|
||||||
// oneQuestionAtATime: true,
|
// rubric: [
|
||||||
// localAssignmentGroupName: "Assignments",
|
// { points: 4, label: "do task 1" },
|
||||||
// questions: [
|
// { points: 2, label: "do task 2" },
|
||||||
// {
|
// ],
|
||||||
// text: "test essay",
|
// allowedFileUploadExtensions: [],
|
||||||
// questionType: QuestionType.ESSAY,
|
// },
|
||||||
// points: 1,
|
// ],
|
||||||
// answers: [],
|
// quizzes: [
|
||||||
// matchDistractors: [],
|
// {
|
||||||
// },
|
// name: "Test Quiz",
|
||||||
// ],
|
// description: "quiz description",
|
||||||
// showCorrectAnswers: false,
|
// lockAt: "07/09/2024 23:59:00",
|
||||||
// allowedAttempts: 0,
|
// dueAt: "07/09/2024 23:59:00",
|
||||||
// },
|
// shuffleAnswers: true,
|
||||||
// ],
|
// oneQuestionAtATime: false,
|
||||||
// pages: [],
|
// localAssignmentGroupName: "someId",
|
||||||
// },
|
// allowedAttempts: -1,
|
||||||
// ],
|
// questions: [
|
||||||
// };
|
// {
|
||||||
|
// text: "test short answer",
|
||||||
|
// questionType: QuestionType.SHORT_ANSWER,
|
||||||
|
// points: 1,
|
||||||
|
// answers: [],
|
||||||
|
// matchDistractors: [],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// showCorrectAnswers: false,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// pages: [],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// };
|
||||||
|
|
||||||
// await fileStorageService.saveCourseAsync(testCourse);
|
// await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
// const loadedCourse = loadedCourses.find(
|
// const loadedCourse = loadedCourses.find(
|
||||||
// (c) => c.settings.name === testCourse.settings.name
|
// (c) => c.settings.name === testCourse.settings.name
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// expect(loadedCourse?.modules[0].quizzes).toEqual(
|
// expect(loadedCourse).toEqual(testCourse);
|
||||||
// testCourse.modules[0].quizzes
|
// });
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it("markdown storage fully populated does not lose data", async () => {
|
// it("markdown storage can persist pages", async () => {
|
||||||
// const testCourse: LocalCourse = {
|
// const testCourse: LocalCourse = {
|
||||||
// settings: {
|
// settings: {
|
||||||
// name: "Test Course with lots of data",
|
// name: "Test Course with page",
|
||||||
// assignmentGroups: [],
|
// assignmentGroups: [],
|
||||||
// daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
// daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
||||||
// startDate: "07/09/2024 23:59:00",
|
// startDate: "07/09/2024 23:59:00",
|
||||||
// endDate: "07/09/2024 23:59:00",
|
// endDate: "07/09/2024 23:59:00",
|
||||||
// defaultDueTime: { hour: 1, minute: 59 },
|
// defaultDueTime: { hour: 1, minute: 59 },
|
||||||
// },
|
// },
|
||||||
// modules: [
|
// modules: [
|
||||||
// {
|
// {
|
||||||
// name: "new test module",
|
// name: "page test module",
|
||||||
// assignments: [
|
// assignments: [],
|
||||||
// {
|
// quizzes: [],
|
||||||
// name: "test assignment",
|
// pages: [
|
||||||
// description: "here is the description",
|
// {
|
||||||
// dueAt: "07/09/2024 23:59:00",
|
// name: "test page persistence",
|
||||||
// lockAt: "07/09/2024 23:59:00",
|
// dueAt: "07/09/2024 23:59:00",
|
||||||
// submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
|
// text: "this is some\n## markdown\n",
|
||||||
// localAssignmentGroupName: "Final Project",
|
// },
|
||||||
// rubric: [
|
// ],
|
||||||
// { points: 4, label: "do task 1" },
|
// },
|
||||||
// { points: 2, label: "do task 2" },
|
// ],
|
||||||
// ],
|
// };
|
||||||
// allowedFileUploadExtensions: [],
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// quizzes: [
|
|
||||||
// {
|
|
||||||
// name: "Test Quiz",
|
|
||||||
// description: "quiz description",
|
|
||||||
// lockAt: "07/09/2024 23:59:00",
|
|
||||||
// dueAt: "07/09/2024 23:59:00",
|
|
||||||
// shuffleAnswers: true,
|
|
||||||
// oneQuestionAtATime: false,
|
|
||||||
// localAssignmentGroupName: "someId",
|
|
||||||
// allowedAttempts: -1,
|
|
||||||
// questions: [
|
|
||||||
// {
|
|
||||||
// text: "test short answer",
|
|
||||||
// questionType: QuestionType.SHORT_ANSWER,
|
|
||||||
// points: 1,
|
|
||||||
// answers: [],
|
|
||||||
// matchDistractors: [],
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// showCorrectAnswers: false,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// pages: [],
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// await fileStorageService.saveCourseAsync(testCourse);
|
// await fileStorageService.saveCourseAsync(testCourse);
|
||||||
|
|
||||||
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
||||||
// const loadedCourse = loadedCourses.find(
|
// const loadedCourse = loadedCourses.find(
|
||||||
// (c) => c.settings.name === testCourse.settings.name
|
// (c) => c.settings.name === testCourse.settings.name
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// expect(loadedCourse).toEqual(testCourse);
|
// expect(loadedCourse).toEqual(testCourse);
|
||||||
// });
|
// });
|
||||||
|
});
|
||||||
// it("markdown storage can persist pages", async () => {
|
|
||||||
// const testCourse: LocalCourse = {
|
|
||||||
// settings: {
|
|
||||||
// name: "Test Course with page",
|
|
||||||
// assignmentGroups: [],
|
|
||||||
// daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
|
|
||||||
// startDate: "07/09/2024 23:59:00",
|
|
||||||
// endDate: "07/09/2024 23:59:00",
|
|
||||||
// defaultDueTime: { hour: 1, minute: 59 },
|
|
||||||
// },
|
|
||||||
// modules: [
|
|
||||||
// {
|
|
||||||
// name: "page test module",
|
|
||||||
// assignments: [],
|
|
||||||
// quizzes: [],
|
|
||||||
// pages: [
|
|
||||||
// {
|
|
||||||
// name: "test page persistence",
|
|
||||||
// dueAt: "07/09/2024 23:59:00",
|
|
||||||
// text: "this is some\n## markdown\n",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// await fileStorageService.saveCourseAsync(testCourse);
|
|
||||||
|
|
||||||
// const loadedCourses = await fileStorageService.loadSavedCourses();
|
|
||||||
// const loadedCourse = loadedCourses.find(
|
|
||||||
// (c) => c.settings.name === testCourse.settings.name
|
|
||||||
// );
|
|
||||||
|
|
||||||
// expect(loadedCourse).toEqual(testCourse);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|||||||
24
nextjs/src/services/urlUtils.ts
Normal file
24
nextjs/src/services/urlUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
export function getModuleItemUrl(
|
||||||
|
courseName: string,
|
||||||
|
moduleName: string,
|
||||||
|
type: "assignment" | "page" | "quiz",
|
||||||
|
itemName: string
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
"/course/" +
|
||||||
|
encodeURIComponent(courseName) +
|
||||||
|
"/modules/" +
|
||||||
|
encodeURIComponent(moduleName) +
|
||||||
|
`/${type}/` +
|
||||||
|
encodeURIComponent(itemName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCourseUrl(courseName: string) {
|
||||||
|
return "/course/" + encodeURIComponent(courseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCourseSettingsUrl(courseName: string) {
|
||||||
|
return "/course/" + encodeURIComponent(courseName) + "/settings";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user