mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
adding canvas service
This commit is contained in:
@@ -1,22 +1,35 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { createQueryClient } from "@/services/utils/queryClient";
|
||||
import { dehydrate } from "@tanstack/react-query";
|
||||
import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Canvas Manager 2.0",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export async function getDehydratedClient() {
|
||||
const queryClient = createQueryClient();
|
||||
|
||||
// await hydrateOpenSections(queryClient);
|
||||
const dehydratedState = dehydrate(queryClient);
|
||||
return dehydratedState;
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const dehydratedState = await getDehydratedClient();
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<MyQueryClientProvider dehydratedState={dehydratedState}>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</MyQueryClientProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { canvasAssignmentService } from "@/services/canvas/canvasAssignmentService";
|
||||
import Image from "next/image";
|
||||
|
||||
export default async function Home() {
|
||||
const assignments = await canvasAssignmentService.getAll(960410);
|
||||
|
||||
11
nextjs/src/hooks/cavnasCouresHooks.ts
Normal file
11
nextjs/src/hooks/cavnasCouresHooks.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
|
||||
export const canvasCourseKeys = {
|
||||
courseDetails: (canavasId: number) => ["canvas course", canavasId] as const,
|
||||
};
|
||||
|
||||
|
||||
export const useCanvasCourseQuery =(canvasId: number) => useSuspenseQuery({
|
||||
queryKey: canvasCourseKeys.courseDetails(canvasId),
|
||||
queryFn: async () => canvasserv
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface CanvasEnrollmentTermModel {
|
||||
id: number;
|
||||
name: string;
|
||||
sis_term_id?: string;
|
||||
sis_import_id?: number;
|
||||
start_at?: string; // ISO 8601 date string
|
||||
end_at?: string; // ISO 8601 date string
|
||||
grading_period_group_id?: number;
|
||||
workflow_state?: string;
|
||||
overrides?: {
|
||||
[key: string]: {
|
||||
start_at?: string; // ISO 8601 date string
|
||||
end_at?: string; // ISO 8601 date string
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CanvasUserDisplayModel } from "../users/userDisplayModel";
|
||||
import { CanvasGradeModel } from "./canvasG'radeModel";
|
||||
import { CanvasGradeModel } from "./canvasGradeModel";
|
||||
|
||||
export interface CanvasEnrollmentModel {
|
||||
id: number;
|
||||
|
||||
124
nextjs/src/services/canvas/canvasService.ts
Normal file
124
nextjs/src/services/canvas/canvasService.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
||||
import { canvasServiceUtils } from "./canvasServiceUtils";
|
||||
import { webRequestor } from "./webRequestor";
|
||||
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
||||
import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems";
|
||||
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
||||
import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel";
|
||||
|
||||
const getTerms = async () => {
|
||||
const url = `accounts/10/terms`;
|
||||
const data = await canvasServiceUtils.paginatedRequest<{
|
||||
enrollment_terms: CanvasEnrollmentTermModel[];
|
||||
}>({ url });
|
||||
const terms = data.flatMap((r) => r.enrollment_terms);
|
||||
return terms;
|
||||
};
|
||||
|
||||
export const canvasService = {
|
||||
async getCourses(termId: number) {
|
||||
const url = `courses`;
|
||||
const coursesResponse =
|
||||
await canvasServiceUtils.paginatedRequest<CanvasCourseModel>({ url });
|
||||
return coursesResponse
|
||||
.flat()
|
||||
.filter((c) => c.enrollment_term_id === termId);
|
||||
},
|
||||
|
||||
async getCourse(courseId: number): Promise<CanvasCourseModel> {
|
||||
const url = `courses/${courseId}`;
|
||||
const { data, response } = await webRequestor.get<CanvasCourseModel>(url);
|
||||
|
||||
if (!data) {
|
||||
console.error((await response?.text()) ?? "");
|
||||
console.error(response?.url ?? "");
|
||||
throw new Error("Error getting course from Canvas");
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async getCurrentTermsFor(queryDate: Date = new Date()) {
|
||||
const terms = await getTerms();
|
||||
const currentTerms = terms
|
||||
.filter(
|
||||
(t) =>
|
||||
t.end_at &&
|
||||
new Date(t.end_at) > queryDate &&
|
||||
new Date(t.end_at) <
|
||||
new Date(queryDate.setFullYear(queryDate.getFullYear() + 1))
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.start_at ?? "").getTime() -
|
||||
new Date(b.start_at ?? "").getTime()
|
||||
)
|
||||
.slice(0, 3);
|
||||
|
||||
return currentTerms;
|
||||
},
|
||||
|
||||
async updateModuleItem(
|
||||
canvasCourseId: number,
|
||||
canvasModuleId: number,
|
||||
item: CanvasModuleItem
|
||||
) {
|
||||
console.log(`Updating module item ${item.title}`);
|
||||
const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items/${item.id}`;
|
||||
const body = {
|
||||
module_item: { title: item.title, position: item.position },
|
||||
};
|
||||
const { data, response } =
|
||||
await webRequestor.putWithDeserialize<CanvasModuleItem>(url, body);
|
||||
|
||||
if (!data) throw new Error("Something went wrong updating module item");
|
||||
},
|
||||
|
||||
async createModuleItem(
|
||||
canvasCourseId: number,
|
||||
canvasModuleId: number,
|
||||
title: string,
|
||||
type: string,
|
||||
contentId: number | string
|
||||
): Promise<void> {
|
||||
console.log(`Creating new module item ${title}`);
|
||||
const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
|
||||
const body = { module_item: { title, type, content_id: contentId } };
|
||||
const response = await webRequestor.post(url, body);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error("Something went wrong creating module item");
|
||||
},
|
||||
|
||||
async createPageModuleItem(
|
||||
canvasCourseId: number,
|
||||
canvasModuleId: number,
|
||||
title: string,
|
||||
canvasPage: CanvasPage
|
||||
): Promise<void> {
|
||||
console.log(`Creating new module item ${title}`);
|
||||
const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
|
||||
const body = {
|
||||
module_item: { title, type: "Page", page_url: canvasPage.url },
|
||||
};
|
||||
const { data, response } =
|
||||
await webRequestor.postWithDeserialize<CanvasModuleItem>(url, body);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error("Something went wrong creating page module item");
|
||||
},
|
||||
|
||||
async getEnrolledStudents(canvasCourseId: number) {
|
||||
console.log(`Getting students for course ${canvasCourseId}`);
|
||||
const url = `courses/${canvasCourseId}/enrollments?enrollment_type=student`;
|
||||
const { data, response } =
|
||||
await webRequestor.getMany<CanvasEnrollmentModel>(url);
|
||||
|
||||
if (!data)
|
||||
throw new Error(
|
||||
`Something went wrong getting enrollments for ${canvasCourseId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -34,16 +34,15 @@ const deserialize = async <T>(response: Response): Promise<T | undefined> => {
|
||||
}
|
||||
};
|
||||
|
||||
const rateLimitAwarePostAsync = async (
|
||||
const rateLimitAwarePost = async (
|
||||
url: string,
|
||||
options: FetchOptions,
|
||||
body: any,
|
||||
retryCount = 0
|
||||
): Promise<Response> => {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -55,7 +54,7 @@ const rateLimitAwarePostAsync = async (
|
||||
`Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
||||
);
|
||||
await sleep(rateLimitSleepInterval);
|
||||
return await rateLimitAwarePostAsync(url, options, retryCount + 1);
|
||||
return await rateLimitAwarePost(url, body, retryCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +70,7 @@ const rateLimitAwarePostAsync = async (
|
||||
return response;
|
||||
};
|
||||
|
||||
const recursiveDeleteAsync = async (
|
||||
const recursiveDelete = async (
|
||||
url: string,
|
||||
options: FetchOptions,
|
||||
retryCount = 0
|
||||
@@ -89,7 +88,7 @@ const recursiveDeleteAsync = async (
|
||||
if (await isRateLimited(response)) {
|
||||
console.info("After delete response in rate limited");
|
||||
await sleep(rateLimitSleepInterval);
|
||||
return await recursiveDeleteAsync(url, options, retryCount + 1);
|
||||
return await recursiveDelete(url, options, retryCount + 1);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -101,7 +100,7 @@ const recursiveDeleteAsync = async (
|
||||
`Hit rate limit in delete, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
||||
);
|
||||
await sleep(rateLimitSleepInterval);
|
||||
return await recursiveDeleteAsync(url, options, retryCount + 1);
|
||||
return await recursiveDelete(url, options, retryCount + 1);
|
||||
} else {
|
||||
console.info(
|
||||
`Hit rate limit in delete, ${rateLimitRetryCount} retries did not fix it`
|
||||
@@ -112,10 +111,7 @@ const recursiveDeleteAsync = async (
|
||||
}
|
||||
};
|
||||
export const webRequestor = {
|
||||
getMany: async <T>(
|
||||
url: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<[T[] | undefined, Response]> => {
|
||||
getMany: async <T>(url: string, options: FetchOptions = {}) => {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
@@ -124,13 +120,10 @@ export const webRequestor = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
return [await deserialize<T[]>(response), response];
|
||||
return { data: await deserialize<T[]>(response), response };
|
||||
},
|
||||
|
||||
get: async <T>(
|
||||
url: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<[T | undefined, Response]> => {
|
||||
get: async <T>(url: string, options: FetchOptions = {}) => {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
@@ -139,27 +132,23 @@ export const webRequestor = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
return [await deserialize<T>(response), response];
|
||||
return { data: await deserialize<T>(response), response };
|
||||
},
|
||||
|
||||
post: async (url: string, options: FetchOptions = {}): Promise<Response> => {
|
||||
return await rateLimitAwarePostAsync(url, options);
|
||||
post: async (url: string, body: any) => {
|
||||
return await rateLimitAwarePost(url, body);
|
||||
},
|
||||
|
||||
postWithDeserialize: async <T>(
|
||||
url: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<[T | undefined, Response]> => {
|
||||
const response = await rateLimitAwarePostAsync(url, options);
|
||||
return [await deserialize<T>(response), response];
|
||||
postWithDeserialize: async <T>(url: string, body: any) => {
|
||||
const response = await rateLimitAwarePost(url, body);
|
||||
return { data: await deserialize<T[]>(response), response };
|
||||
},
|
||||
|
||||
put: async (url: string, options: FetchOptions = {}): Promise<Response> => {
|
||||
put: async (url: string, body: any = {}) => {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -167,26 +156,22 @@ export const webRequestor = {
|
||||
return response;
|
||||
},
|
||||
|
||||
putWithDeserialize: async <T>(
|
||||
url: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<[T | undefined, Response]> => {
|
||||
putWithDeserialize: async <T>(url: string, body: any = {}) => {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
body: JSON.stringify(body),
|
||||
method: "PUT",
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return [await deserialize<T>(response), response];
|
||||
return { data: await deserialize<T[]>(response), response };
|
||||
},
|
||||
|
||||
delete: async (
|
||||
url: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<Response> => {
|
||||
return await recursiveDeleteAsync(url, options);
|
||||
return await recursiveDelete(url, options);
|
||||
},
|
||||
};
|
||||
|
||||
23
nextjs/src/services/utils/MyQueryClientProvider.tsx
Normal file
23
nextjs/src/services/utils/MyQueryClientProvider.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DehydratedState,
|
||||
hydrate,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { FC, ReactNode, useState } from "react";
|
||||
import { createQueryClient } from "./queryClient";
|
||||
|
||||
export const MyQueryClientProvider: FC<{
|
||||
children: ReactNode;
|
||||
dehydratedState: DehydratedState;
|
||||
}> = ({ children, dehydratedState }) => {
|
||||
const [queryClient] = useState(createQueryClient());
|
||||
|
||||
hydrate(queryClient, dehydratedState);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
143
nextjs/src/services/utils/queryClient.tsx
Normal file
143
nextjs/src/services/utils/queryClient.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import toast, { ErrorIcon, CheckmarkIcon } from "react-hot-toast";
|
||||
import { ReactNode } from "react";
|
||||
import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
|
||||
|
||||
const addErrorAsToast = async (error: any) => {
|
||||
console.error("error from toast", error);
|
||||
const message = getErrorMessage(error);
|
||||
|
||||
toast(
|
||||
(t: any) => (
|
||||
<div className="row">
|
||||
<div className="col-auto my-auto">
|
||||
<ErrorIcon />
|
||||
</div>
|
||||
<div className="col my-auto">
|
||||
<div className="white-space">{message}</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://snow.kualibuild.com/app/651eeebc32976c013a4c4739/run"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Report Bug
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-auto my-auto">
|
||||
<button
|
||||
onClick={() => toast.dismiss(t.id)}
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
>
|
||||
<i className="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration: Infinity,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export function getErrorMessage(error: any) {
|
||||
if (error?.response?.status === 422) {
|
||||
console.log(error.response.data.detail);
|
||||
const serializationMessages = error.response.data.detail.map(
|
||||
(d: any) => `${d.type} - ${d.loc[1]}`
|
||||
);
|
||||
return `Deserialization error on request:\n${serializationMessages.join(
|
||||
"\n"
|
||||
)}`;
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
if (error.response?.data.detail) {
|
||||
if (typeof error.response?.data.detail === "string") {
|
||||
return error.response?.data.detail;
|
||||
} else return JSON.stringify(error.response?.data.detail);
|
||||
}
|
||||
console.log(error);
|
||||
return "Error With Request";
|
||||
}
|
||||
|
||||
export function createInfoToast(
|
||||
children: ReactNode,
|
||||
onClose: () => void = () => {}
|
||||
) {
|
||||
const closeHandler = (t: any) => {
|
||||
toast.dismiss(t.id);
|
||||
onClose();
|
||||
};
|
||||
toast(
|
||||
(t: any) => (
|
||||
<div className="row">
|
||||
<div className="col-auto my-auto">
|
||||
<i className="bi bi-info-circle-fill"></i>
|
||||
</div>
|
||||
<div className="col my-auto">{children}</div>
|
||||
<div className="col-auto my-auto">
|
||||
<button
|
||||
onClick={() => closeHandler(t)}
|
||||
className="btn btn-outline-secondary py-1"
|
||||
>
|
||||
<i className="bi-x-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration: Infinity,
|
||||
style: {
|
||||
maxWidth: "75em",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function createSuccessToast(message: string) {
|
||||
toast(
|
||||
(t: any) => (
|
||||
<div className="row">
|
||||
<div className="col-auto my-auto">
|
||||
<CheckmarkIcon />
|
||||
</div>
|
||||
<div className="col my-auto"> {message}</div>
|
||||
<div className="col-auto my-auto">
|
||||
<button
|
||||
onClick={() => toast.dismiss(t.id)}
|
||||
className="btn btn-outline-secondary py-1"
|
||||
>
|
||||
<i className="bi-x-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration: Infinity,
|
||||
style: {
|
||||
maxWidth: "75em",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const createQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 0,
|
||||
},
|
||||
mutations: {
|
||||
onError: addErrorAsToast,
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
queryCache: new QueryCache({
|
||||
onError: addErrorAsToast,
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: addErrorAsToast,
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user