From ab5dbed3832dda9196e9c8e90ad5bb7acb938b4d Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Thu, 19 Sep 2024 17:45:26 -0600 Subject: [PATCH] redoing paginated canvas requests --- nextjs/run.sh | 11 +++ nextjs/src/app/api/canvas/[...rest]/route.ts | 84 +++++-------------- .../app/course/[courseName]/calendar/Day.tsx | 56 ++++++------- .../[courseName]/modules/ExpandableModule.tsx | 14 +++- .../modules/ModuleCanvasStatus.tsx | 7 +- .../quiz/[quizName]/QuizPreview.tsx | 11 +-- nextjs/src/components/icons/CheckIcon.tsx | 15 ++++ nextjs/src/components/icons/ExpandIcon.tsx | 28 +++++++ .../canvas/canvasAssignmentService.ts | 14 ++-- .../src/services/canvas/canvasPageService.ts | 4 +- .../src/services/canvas/canvasServiceUtils.ts | 63 +++++++------- 11 files changed, 161 insertions(+), 146 deletions(-) create mode 100755 nextjs/run.sh create mode 100644 nextjs/src/components/icons/CheckIcon.tsx create mode 100644 nextjs/src/components/icons/ExpandIcon.tsx diff --git a/nextjs/run.sh b/nextjs/run.sh new file mode 100755 index 0000000..f5b5065 --- /dev/null +++ b/nextjs/run.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +docker run -it --rm \ + --name canvas-manager-2 \ + -u 1000:1000 \ + -p 3000:3000 \ + -w /app \ + -v .:/app \ + -v ~/projects/faculty/1810/2024-fall-alex/modules:/app/storage/intro_to_web \ + node \ + bash -c "npm i && npm run dev -- -H 0.0.0.0" diff --git a/nextjs/src/app/api/canvas/[...rest]/route.ts b/nextjs/src/app/api/canvas/[...rest]/route.ts index 7477d17..b1c5385 100644 --- a/nextjs/src/app/api/canvas/[...rest]/route.ts +++ b/nextjs/src/app/api/canvas/[...rest]/route.ts @@ -2,44 +2,24 @@ import { NextRequest, NextResponse } from "next/server"; import { axiosClient } from "@/services/axiosUtils"; import { withErrorHandling } from "@/services/withErrorHandling"; import { - AxiosResponseHeaders, isAxiosError, - RawAxiosResponseHeaders, } from "axios"; -const getUrl = (params: { rest: string[] }) => { + +const appendQueryParams = (url: URL, req: NextRequest) => { + req.nextUrl.searchParams.forEach((value, key) => { + url.searchParams.set(key, value); + }); +}; +const getUrl = (params: { rest: string[] }, req: NextRequest) => { const { rest } = params; const path = rest.join("/"); const newUrl = `https://snow.instructure.com/api/v1/${path}`; - return new URL(newUrl); -}; - -const getNextUrl = ( - headers: AxiosResponseHeaders | RawAxiosResponseHeaders -): string | undefined => { - const linkHeader: string | undefined = - typeof headers.get === "function" - ? (headers.get("link") as string) - : ((headers as RawAxiosResponseHeaders)["link"] as string); - - if (!linkHeader) { - console.log("could not find link header in the response"); - return undefined; - } - - const links = linkHeader.split(",").map((link) => link.trim()); - const nextLink = links.find((link) => link.includes('rel="next"')); - - if (!nextLink) { - console.log( - "could not find next url in link header, reached end of pagination" - ); - return undefined; - } - - const nextUrl = nextLink.split(";")[0].trim().slice(1, -1); - return nextUrl; -}; + const url = new URL(newUrl); + + appendQueryParams(url, req); + + return url;}; const proxyResponseHeaders = (response: any) => { const headers = new Headers(); @@ -50,43 +30,19 @@ const proxyResponseHeaders = (response: any) => { }; export async function GET( - _req: NextRequest, + req: NextRequest, { params }: { params: { rest: string[] } } ) { return withErrorHandling(async () => { try { - const url = getUrl(params); + const url = getUrl(params, req); - var requestCount = 1; - url.searchParams.set("per_page", "100"); - - const { data: firstData, headers: firstHeaders } = await axiosClient.get( + const response = await axiosClient.get( url.toString() ); - if (!Array.isArray(firstData)) { - return NextResponse.json(firstData); - } - - var returnData = firstData; - var nextUrl = getNextUrl(firstHeaders); - - while (nextUrl) { - requestCount += 1; - const { data, headers } = await axiosClient.get(nextUrl); - if (data) { - returnData = [...returnData, data]; - } - nextUrl = getNextUrl(headers); - } - - if (requestCount > 1) { - console.log( - `Requesting ${typeof returnData} took ${requestCount} requests` - ); - } - - return NextResponse.json(returnData); + const headers = proxyResponseHeaders(response); + return new NextResponse(JSON.stringify(response.data), { headers }); } catch (error: any) { return new NextResponse( JSON.stringify({ error: error.message || "Canvas GET request failed" }), @@ -101,7 +57,7 @@ export async function POST( { params }: { params: { rest: string[] } } ) { return withErrorHandling(async () => { - const url = getUrl(params); + const url = getUrl(params, req); const body = await req.json(); let response; try { @@ -130,7 +86,7 @@ export async function PUT( { params }: { params: { rest: string[] } } ) { return withErrorHandling(async () => { - const url = getUrl(params); + const url = getUrl(params, req); const body = await req.json(); try { const response = await axiosClient.put(url.toString(), body); @@ -166,7 +122,7 @@ export async function DELETE( ) { return withErrorHandling(async () => { try { - const url = getUrl(params); + const url = getUrl(params, req); const response = await axiosClient.delete(url.toString()); const headers = proxyResponseHeaders(response); diff --git a/nextjs/src/app/course/[courseName]/calendar/Day.tsx b/nextjs/src/app/course/[courseName]/calendar/Day.tsx index ea37f20..ee40a21 100644 --- a/nextjs/src/app/course/[courseName]/calendar/Day.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/Day.tsx @@ -15,45 +15,37 @@ import { LocalAssignment } from "@/models/local/assignment/localAssignment"; import { LocalQuiz } from "@/models/local/quiz/localQuiz"; import { LocalCoursePage } from "@/models/local/page/localCoursePage"; import { useCanvasAssignmentsQuery } from "@/hooks/canvas/canvasAssignmentHooks"; -import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment"; import { useCanvasQuizzesQuery } from "@/hooks/canvas/canvasQuizHooks"; import { useCanvasPagesQuery } from "@/hooks/canvas/canvasPageHooks"; -import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel"; -import { CanvasPage } from "@/models/canvas/pages/canvasPageModel"; export default function Day({ day, month }: { day: string; month: number }) { const dayAsDate = getDateFromStringOrThrow( day, "calculating same month in day" ); + const isToday = + getDateOnlyMarkdownString(new Date()) === + getDateOnlyMarkdownString(dayAsDate); const { data: settings } = useLocalCourseSettingsQuery(); - const { data: canvasAssignments } = useCanvasAssignmentsQuery(); - const { data: canvasQuizzes } = useCanvasQuizzesQuery(); - const { data: canvasPages } = useCanvasPagesQuery(); - const itemsContext = useCalendarItemsContext(); const { itemDrop } = useDraggingContext(); - const dateKey = getDateOnlyMarkdownString(dayAsDate); - const todaysModules = itemsContext[dateKey]; - - const { todaysAssignments, todaysQuizzes, todaysPages } = getTodaysItems( - todaysModules, - canvasAssignments, - canvasQuizzes, - canvasPages - ); + const { todaysAssignments, todaysQuizzes, todaysPages } = useTodaysItems(day); const isInSameMonth = dayAsDate.getMonth() + 1 == month; - const classIsToday = settings.daysOfWeek.includes(getDayOfWeek(dayAsDate)); + const classOnThisDay = settings.daysOfWeek.includes(getDayOfWeek(dayAsDate)); - const todayClass = classIsToday ? " bg-slate-900 " : " "; - const monthClass = isInSameMonth ? " border border-slate-600 " : " "; + const meetingClasses = classOnThisDay ? " bg-slate-900 " : " "; + const monthClass = isInSameMonth + ? isToday + ? " border border-slate-400 bg-slate-700 " + : " border border-slate-700 " + : " "; return (
itemDrop(e, day)} onDragOver={(e) => e.preventDefault()} > @@ -100,18 +92,18 @@ function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) { ); } -function getTodaysItems( - todaysModules: { - [moduleName: string]: { - assignments: LocalAssignment[]; - quizzes: LocalQuiz[]; - pages: LocalCoursePage[]; - }; - }, - canvasAssignments: CanvasAssignment[], - canvasQuizzes: CanvasQuiz[], - canvasPages: CanvasPage[] -) { +function useTodaysItems(day: string) { + const dayAsDate = getDateFromStringOrThrow( + day, + "calculating same month in day items" + ); + const itemsContext = useCalendarItemsContext(); + const dateKey = getDateOnlyMarkdownString(dayAsDate); + const todaysModules = itemsContext[dateKey]; + + const { data: canvasAssignments } = useCanvasAssignmentsQuery(); + const { data: canvasQuizzes } = useCanvasQuizzesQuery(); + const { data: canvasPages } = useCanvasPagesQuery(); const todaysAssignments: { moduleName: string; assignment: LocalAssignment; diff --git a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx index 0f16c19..a4ee71f 100644 --- a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx @@ -22,6 +22,7 @@ import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling" import { isServer } from "@tanstack/react-query"; import { ModuleCanvasStatus } from "./ModuleCanvasStatus"; import ClientOnly from "@/components/ClientOnly"; +import ExpandIcon from "../../../../components/icons/ExpandIcon"; export default function ExpandableModule({ moduleName, @@ -78,9 +79,16 @@ export default function ExpandableModule({ onClick={() => setExpanded((e) => !e)} >
{moduleName}
- - - +
+ + + + +
)} {canvasModule && !canvasModule.published &&
Not Published
} - {canvasModule && canvasModule.published &&
Published
} + {canvasModule && canvasModule.published && ( +
+ +
+ )}
); } diff --git a/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx b/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx index bcbcf64..2783c7b 100644 --- a/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx +++ b/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizPreview.tsx @@ -1,3 +1,4 @@ +import CheckIcon from "@/components/icons/CheckIcon"; import { useQuizQuery } from "@/hooks/localCourse/quizHooks"; import { LocalQuiz } from "@/models/local/quiz/localQuiz"; import { @@ -110,15 +111,7 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) { >
{answer.correct ? ( - - - + ) : question.questionType === QuestionType.MULTIPLE_ANSWERS ? ( {"[ ]"} ) : ( diff --git a/nextjs/src/components/icons/CheckIcon.tsx b/nextjs/src/components/icons/CheckIcon.tsx new file mode 100644 index 0000000..d49b03a --- /dev/null +++ b/nextjs/src/components/icons/CheckIcon.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +export default function CheckIcon() { + return ( + + + + ); +} diff --git a/nextjs/src/components/icons/ExpandIcon.tsx b/nextjs/src/components/icons/ExpandIcon.tsx new file mode 100644 index 0000000..987d962 --- /dev/null +++ b/nextjs/src/components/icons/ExpandIcon.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +export default function ExpandIcon({style}: { + style?: React.CSSProperties | undefined; +}) { + const size = "24px"; + return ( + + + + + + ); +} diff --git a/nextjs/src/services/canvas/canvasAssignmentService.ts b/nextjs/src/services/canvas/canvasAssignmentService.ts index 08c8692..270e664 100644 --- a/nextjs/src/services/canvas/canvasAssignmentService.ts +++ b/nextjs/src/services/canvas/canvasAssignmentService.ts @@ -1,5 +1,9 @@ import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment"; -import { canvasApi, canvasServiceUtils } from "./canvasServiceUtils"; +import { + canvasApi, + canvasServiceUtils, + paginatedRequest, +} from "./canvasServiceUtils"; import { LocalAssignment } from "@/models/local/assignment/localAssignment"; import { axiosClient } from "../axiosUtils"; import { markdownToHTMLSafe } from "../htmlMarkdownUtils"; @@ -31,7 +35,6 @@ const createRubric = async ( }; }, {} as { [key: number]: { description: string; points: number; ratings: { [key: number]: { description: string; points: number } } } }); - console.log(criterion); const rubricBody = { rubric_association_id: assignmentCanvasId, rubric: { @@ -70,10 +73,9 @@ const createRubric = async ( export const canvasAssignmentService = { async getAll(courseId: number): Promise { - const url = `${canvasApi}/courses/${courseId}/assignments`; - const { data: assignments } = await axiosClient.get( - url - ); + console.log("getting canvas assignments"); + const url = `${canvasApi}/courses/${courseId}/assignments`; //per_page=100 + const assignments = await paginatedRequest({ url }); return assignments.map((a) => ({ ...a, due_at: a.due_at ? new Date(a.due_at).toLocaleString() : undefined, // timezones? diff --git a/nextjs/src/services/canvas/canvasPageService.ts b/nextjs/src/services/canvas/canvasPageService.ts index 5561bd4..6e912f4 100644 --- a/nextjs/src/services/canvas/canvasPageService.ts +++ b/nextjs/src/services/canvas/canvasPageService.ts @@ -1,6 +1,6 @@ import { CanvasPage } from "@/models/canvas/pages/canvasPageModel"; import { LocalCoursePage } from "@/models/local/page/localCoursePage"; -import { canvasApi, canvasServiceUtils } from "./canvasServiceUtils"; +import { canvasApi, paginatedRequest } from "./canvasServiceUtils"; import { markdownToHTMLSafe } from "../htmlMarkdownUtils"; import { axiosClient } from "../axiosUtils"; import { rateLimitAwareDelete } from "./canvasWebRequestor"; @@ -9,7 +9,7 @@ export const canvasPageService = { async getAll(courseId: number): Promise { console.log("requesting pages"); const url = `${canvasApi}/courses/${courseId}/pages`; - const pages = await canvasServiceUtils.paginatedRequest({ + const pages = await paginatedRequest({ url, }); return pages.flatMap((pageList) => pageList); diff --git a/nextjs/src/services/canvas/canvasServiceUtils.ts b/nextjs/src/services/canvas/canvasServiceUtils.ts index 48dab46..49cf90e 100644 --- a/nextjs/src/services/canvas/canvasServiceUtils.ts +++ b/nextjs/src/services/canvas/canvasServiceUtils.ts @@ -25,35 +25,40 @@ const getNextUrl = ( return nextUrl; }; -export const canvasServiceUtils = { - async paginatedRequest(request: { url: string }): Promise { - var requestCount = 1; - const url = new URL(request.url); - url.searchParams.set("per_page", "100"); +export async function paginatedRequest(request: { + url: string; +}): Promise { + var requestCount = 1; + const url = new URL(request.url); + url.searchParams.set("per_page", "100"); - const { data: firstData, headers: firstHeaders } = await axiosClient.get( - url.toString() + const { data: firstData, headers: firstHeaders } = await axiosClient.get( + url.toString() + ); + + if (!Array.isArray(firstData)) { + return firstData; + } + + + var returnData = firstData ? [firstData] : []; + 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, data]; + } + nextUrl = getNextUrl(headers); + } + + if (requestCount > 1) { + console.log( + `Requesting ${typeof returnData} took ${requestCount} requests` ); + } - var returnData: T[] = firstData ? [firstData] : []; - 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, data]; - } - nextUrl = getNextUrl(headers); - } - - if (requestCount > 1) { - console.log( - `Requesting ${typeof returnData} took ${requestCount} requests` - ); - } - - return returnData; - }, -}; + return returnData; +}