moving v2 to top level

This commit is contained in:
2024-12-17 09:19:21 -07:00
parent 5f0b3554dc
commit 576ee02afb
468 changed files with 79 additions and 15430 deletions

View File

@@ -0,0 +1,65 @@
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
import { axiosClient } from "../axiosUtils";
import { CanvasAssignmentGroup } from "@/models/canvas/assignments/canvasAssignmentGroup";
import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
import { rateLimitAwareDelete } from "./canvasWebRequestor";
export const canvasAssignmentGroupService = {
async getAll(courseId: number): Promise<CanvasAssignmentGroup[]> {
console.log("Requesting assignment groups");
const url = `${canvasApi}/courses/${courseId}/assignment_groups`;
const assignmentGroups = await paginatedRequest<
CanvasAssignmentGroup[]
>({
url,
});
return assignmentGroups.flatMap((groupList) => groupList);
},
async create(
canvasCourseId: number,
localAssignmentGroup: LocalAssignmentGroup
): Promise<LocalAssignmentGroup> {
console.log(`Creating assignment group: ${localAssignmentGroup.name}`);
const url = `${canvasApi}/courses/${canvasCourseId}/assignment_groups`;
const body = {
name: localAssignmentGroup.name,
group_weight: localAssignmentGroup.weight,
};
const { data: canvasAssignmentGroup } =
await axiosClient.post<CanvasAssignmentGroup>(url, body);
return {
...localAssignmentGroup,
canvasId: canvasAssignmentGroup.id,
};
},
async update(
canvasCourseId: number,
localAssignmentGroup: LocalAssignmentGroup
): Promise<void> {
console.log(`Updating assignment group: ${localAssignmentGroup.name}, ${localAssignmentGroup.canvasId}`);
if (!localAssignmentGroup.canvasId) {
throw new Error("Cannot update assignment group if canvas ID is null");
}
const url = `${canvasApi}/courses/${canvasCourseId}/assignment_groups/${localAssignmentGroup.canvasId}`;
const body = {
name: localAssignmentGroup.name,
group_weight: localAssignmentGroup.weight,
};
await axiosClient.put(url, body);
},
async delete(
canvasCourseId: number,
canvasAssignmentGroupId: number,
assignmentGroupName: string
): Promise<void> {
console.log(`Deleting assignment group: ${assignmentGroupName}`);
const url = `${canvasApi}/courses/${canvasCourseId}/assignment_groups/${canvasAssignmentGroupId}`;
await rateLimitAwareDelete(url);
},
};

View File

@@ -0,0 +1,148 @@
import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { axiosClient } from "../axiosUtils";
import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
import { CanvasRubricCreationResponse } from "@/models/canvas/assignments/canvasRubricCreationResponse";
import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
import { getDateFromString } from "@/models/local/utils/timeUtils";
import { getRubricCriterion } from "./canvasRubricUtils";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export const canvasAssignmentService = {
async getAll(courseId: number): Promise<CanvasAssignment[]> {
console.log("getting canvas assignments");
const url = `${canvasApi}/courses/${courseId}/assignments`; //per_page=100
const assignments = await paginatedRequest<CanvasAssignment[]>({ url });
return assignments.map((a) => ({
...a,
due_at: a.due_at ? new Date(a.due_at).toLocaleString() : undefined, // timezones?
lock_at: a.lock_at ? new Date(a.lock_at).toLocaleString() : undefined, // timezones?
}));
},
async create(
canvasCourseId: number,
localAssignment: LocalAssignment,
settings: LocalCourseSettings,
canvasAssignmentGroupId?: number
) {
console.log(`Creating assignment: ${localAssignment.name}`);
const url = `${canvasApi}/courses/${canvasCourseId}/assignments`;
const body = {
assignment: {
name: localAssignment.name,
submission_types: localAssignment.submissionTypes.map((t) =>
t.toString()
),
allowed_extensions: localAssignment.allowedFileUploadExtensions.map(
(e) => e.toString()
),
description: markdownToHTMLSafe(localAssignment.description, settings),
due_at: getDateFromString(localAssignment.dueAt)?.toISOString(),
lock_at:
localAssignment.lockAt &&
getDateFromString(localAssignment.lockAt)?.toISOString(),
points_possible: assignmentPoints(localAssignment.rubric),
assignment_group_id: canvasAssignmentGroupId,
},
};
const response = await axiosClient.post<CanvasAssignment>(url, body);
const canvasAssignment = response.data;
await createRubric(canvasCourseId, canvasAssignment.id, localAssignment);
return canvasAssignment.id;
},
async update(
courseId: number,
canvasAssignmentId: number,
localAssignment: LocalAssignment,
settings: LocalCourseSettings,
canvasAssignmentGroupId?: number
) {
console.log(`Updating assignment: ${localAssignment.name}`);
const url = `${canvasApi}/courses/${courseId}/assignments/${canvasAssignmentId}`;
const body = {
assignment: {
name: localAssignment.name,
submission_types: localAssignment.submissionTypes.map((t) =>
t.toString()
),
allowed_extensions: localAssignment.allowedFileUploadExtensions.map(
(e) => e.toString()
),
description: markdownToHTMLSafe(localAssignment.description, settings),
due_at: getDateFromString(localAssignment.dueAt)?.toISOString(),
lock_at:
localAssignment.lockAt &&
getDateFromString(localAssignment.lockAt)?.toISOString(),
points_possible: assignmentPoints(localAssignment.rubric),
assignment_group_id: canvasAssignmentGroupId,
},
};
await axiosClient.put(url, body);
await createRubric(courseId, canvasAssignmentId, localAssignment);
},
async delete(
courseId: number,
assignmentCanvasId: number,
assignmentName: string
) {
console.log(`Deleting assignment from Canvas: ${assignmentName}`);
const url = `${canvasApi}/courses/${courseId}/assignments/${assignmentCanvasId}`;
const response = await axiosClient.delete(url);
if (!response.status.toString().startsWith("2")) {
console.error(`Failed to delete assignment: ${assignmentName}`);
throw new Error("Failed to delete assignment");
}
},
};
const createRubric = async (
courseId: number,
assignmentCanvasId: number,
localAssignment: LocalAssignment
) => {
const criterion = getRubricCriterion(localAssignment.rubric);
const rubricBody = {
rubric_association_id: assignmentCanvasId,
rubric: {
title: `Rubric for Assignment: ${localAssignment.name}`,
association_id: assignmentCanvasId,
association_type: "Assignment",
use_for_grading: true,
criteria: criterion,
},
rubric_association: {
association_id: assignmentCanvasId,
association_type: "Assignment",
purpose: "grading",
use_for_grading: true,
},
};
const rubricUrl = `${canvasApi}/courses/${courseId}/rubrics`;
const rubricResponse = await axiosClient.post<CanvasRubricCreationResponse>(
rubricUrl,
rubricBody
);
if (!rubricResponse.data) throw new Error("Failed to create rubric");
const assignmentPointAdjustmentUrl = `${canvasApi}/courses/${courseId}/assignments/${assignmentCanvasId}`;
const assignmentPointAdjustmentBody = {
assignment: { points_possible: assignmentPoints(localAssignment.rubric) },
};
await axiosClient.put(
assignmentPointAdjustmentUrl,
assignmentPointAdjustmentBody
);
};

View File

@@ -0,0 +1,66 @@
import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems";
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
import { axiosClient } from "../axiosUtils";
import { canvasApi } from "./canvasServiceUtils";
import { CanvasModule } from "@/models/canvas/modules/canvasModule";
export const canvasModuleService = {
async updateModuleItem(
canvasCourseId: number,
canvasModuleId: number,
item: CanvasModuleItem
) {
console.log(`Updating module item ${item.title}`);
const url = `${canvasApi}/courses/${canvasCourseId}/modules/${canvasModuleId}/items/${item.id}`;
const body = {
module_item: { title: item.title, position: item.position },
};
const { data } = await axiosClient.put<CanvasModuleItem>(url, body);
if (!data) throw new Error("Something went wrong updating module item");
},
async createModuleItem(
canvasCourseId: number,
canvasModuleId: number,
title: string,
type: "Assignment" | "Quiz",
contentId: number | string
) {
console.log(`Creating new module item ${title}`);
const url = `${canvasApi}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
const body = { module_item: { title, type, content_id: contentId } };
await axiosClient.post(url, body);
},
async createPageModuleItem(
canvasCourseId: number,
canvasModuleId: number,
title: string,
canvasPage: CanvasPage
) {
console.log(`Creating new module item ${title}`);
const url = `${canvasApi}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
const body = {
module_item: { title, type: "Page", page_url: canvasPage.url },
};
await axiosClient.post<CanvasModuleItem>(url, body);
},
async getCourseModules(canvasCourseId: number) {
const url = `${canvasApi}/courses/${canvasCourseId}/modules`;
const response = await axiosClient.get<CanvasModule[]>(url);
return response.data;
},
async createModule(canvasCourseId: number, moduleName: string) {
const url = `${canvasApi}/courses/${canvasCourseId}/modules`;
const body = {
module: {
name: moduleName,
},
};
const response = await axiosClient.post<CanvasModule>(url, body);
return response.data.id;
},
};

View File

@@ -0,0 +1,62 @@
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
import { axiosClient } from "../axiosUtils";
import { rateLimitAwareDelete } from "./canvasWebRequestor";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export const canvasPageService = {
async getAll(courseId: number): Promise<CanvasPage[]> {
console.log("requesting pages");
const url = `${canvasApi}/courses/${courseId}/pages`;
const pages = await paginatedRequest<CanvasPage[]>({
url,
});
return pages.flatMap((pageList) => pageList);
},
async create(
canvasCourseId: number,
page: LocalCoursePage,
settings: LocalCourseSettings
): Promise<CanvasPage> {
console.log(`Creating course page: ${page.name}`);
const url = `${canvasApi}/courses/${canvasCourseId}/pages`;
const body = {
wiki_page: {
title: page.name,
body: markdownToHTMLSafe(page.text, settings),
},
};
const { data: canvasPage } = await axiosClient.post<CanvasPage>(url, body);
if (!canvasPage) {
throw new Error("Created canvas course page was null");
}
return canvasPage;
},
async update(
courseId: number,
canvasPageId: number,
page: LocalCoursePage,
settings: LocalCourseSettings
): Promise<void> {
console.log(`Updating course page: ${page.name}`);
const url = `${canvasApi}/courses/${courseId}/pages/${canvasPageId}`;
const body = {
wiki_page: {
title: page.name,
body: markdownToHTMLSafe(page.text, settings),
},
};
await axiosClient.put(url, body);
},
async delete(courseId: number, canvasPageId: number): Promise<void> {
console.log(`Deleting page from canvas ${canvasPageId}`);
const url = `${canvasApi}/courses/${courseId}/pages/${canvasPageId}`;
await rateLimitAwareDelete(url);
},
};

View File

@@ -0,0 +1,182 @@
import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
import { axiosClient } from "../axiosUtils";
import { canvasApi } from "./canvasServiceUtils";
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
import { canvasAssignmentService } from "./canvasAssignmentService";
import {
LocalQuizQuestion,
QuestionType,
} from "@/models/local/quiz/localQuizQuestion";
import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
const getAnswers = (
question: LocalQuizQuestion,
settings: LocalCourseSettings
) => {
if (question.questionType === QuestionType.MATCHING)
return question.answers.map((a) => ({
answer_match_left: a.text,
answer_match_right: a.matchedText,
}));
return question.answers.map((answer) => ({
answer_html: markdownToHTMLSafe(answer.text, settings),
answer_weight: answer.correct ? 100 : 0,
}));
};
const createQuestionOnly = async (
canvasCourseId: number,
canvasQuizId: number,
question: LocalQuizQuestion,
position: number,
settings: LocalCourseSettings
) => {
console.log("Creating individual question"); //, question);
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/questions`;
const body = {
question: {
question_text: markdownToHTMLSafe(question.text, settings),
question_type: `${question.questionType}_question`,
points_possible: question.points,
position,
answers: getAnswers(question, settings),
},
};
const response = await axiosClient.post<CanvasQuizQuestion>(url, body);
const newQuestion = response.data;
if (!newQuestion) throw new Error("Created question is null");
return { question: newQuestion, position };
};
const hackFixQuestionOrdering = async (
canvasCourseId: number,
canvasQuizId: number,
questionAndPositions: Array<{ question: any; position: number }>
) => {
console.log("Fixing question order");
const order = questionAndPositions.map((qp) => ({
type: "question",
id: qp.question.id.toString(),
}));
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/reorder`;
await axiosClient.post(url, { order });
};
const hackFixRedundantAssignments = async (canvasCourseId: number) => {
console.log("hack fixing redundant quiz assignments that are auto-created");
const assignments = await canvasAssignmentService.getAll(canvasCourseId);
const assignmentsToDelete = assignments.filter(
(assignment) =>
!assignment.is_quiz_assignment &&
assignment.submission_types.includes("online_quiz")
);
await Promise.all(
assignmentsToDelete.map(
async (assignment) =>
await canvasAssignmentService.delete(
canvasCourseId,
assignment.id,
assignment.name
)
)
);
console.log(`Deleted ${assignmentsToDelete.length} redundant assignments`);
};
const createQuizQuestions = async (
canvasCourseId: number,
canvasQuizId: number,
localQuiz: LocalQuiz,
settings: LocalCourseSettings
) => {
console.log("Creating quiz questions"); //, localQuiz);
const tasks = localQuiz.questions.map(
async (question, index) =>
await createQuestionOnly(
canvasCourseId,
canvasQuizId,
question,
index,
settings
)
);
const questionAndPositions = await Promise.all(tasks);
await hackFixQuestionOrdering(
canvasCourseId,
canvasQuizId,
questionAndPositions
);
await hackFixRedundantAssignments(canvasCourseId);
};
export const canvasQuizService = {
async getAll(canvasCourseId: number): Promise<CanvasQuiz[]> {
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes`;
const response = await axiosClient.get<CanvasQuiz[]>(url);
return response.data.map((quiz) => ({
...quiz,
due_at: quiz.due_at ? new Date(quiz.due_at).toLocaleString() : undefined,
lock_at: quiz.lock_at
? new Date(quiz.lock_at).toLocaleString()
: undefined,
}));
},
async create(
canvasCourseId: number,
localQuiz: LocalQuiz,
settings: LocalCourseSettings,
canvasAssignmentGroupId?: number
) {
console.log("Creating quiz", localQuiz);
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes`;
const body = {
quiz: {
title: localQuiz.name,
description: markdownToHTMLSafe(localQuiz.description, settings),
shuffle_answers: localQuiz.shuffleAnswers,
access_code: localQuiz.password,
show_correct_answers: localQuiz.showCorrectAnswers,
allowed_attempts: localQuiz.allowedAttempts,
one_question_at_a_time: localQuiz.oneQuestionAtATime,
cant_go_back: false,
due_at: localQuiz.dueAt
? getDateFromStringOrThrow(
localQuiz.dueAt,
"creating quiz"
).toISOString()
: undefined,
lock_at: localQuiz.lockAt
? getDateFromStringOrThrow(
localQuiz.lockAt,
"creating quiz"
).toISOString()
: undefined,
assignment_group_id: canvasAssignmentGroupId,
},
};
const { data: canvasQuiz } = await axiosClient.post<CanvasQuiz>(url, body);
await createQuizQuestions(canvasCourseId, canvasQuiz.id, localQuiz, settings);
return canvasQuiz.id;
},
async delete(canvasCourseId: number, canvasQuizId: number) {
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}`;
await axiosClient.delete(url);
},
};

View File

@@ -0,0 +1,22 @@
import { RubricItem } from "@/models/local/assignment/rubricItem";
export const getRubricCriterion = (rubric: RubricItem[]) => {
const criterion = rubric
.map((rubricItem) => ({
description: rubricItem.label,
points: rubricItem.points,
ratings: {
0: { description: "Full Marks", points: rubricItem.points },
1: { description: "No Marks", points: 0 },
},
}))
.reduce((acc, item, index) => {
return {
...acc,
[index]: item,
};
}, {} as { [key: number]: { description: string; points: number; ratings: { [key: number]: { description: string; points: number } } } });
return criterion
}

View File

@@ -0,0 +1,67 @@
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
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?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?per_page=100`;
const allCourses = await paginatedRequest<CanvasCourseModel[]>({ url });
const coursesInTerm = allCourses
.flatMap((l) => l)
.filter((c) => c.enrollment_term_id === termId);
return coursesInTerm;
},
async getCourse(courseId: number): Promise<CanvasCourseModel> {
const url = `${canvasApi}/courses/${courseId}`;
const { data } = await axiosClient.get<CanvasCourseModel>(url);
return data;
},
async getCurrentTermsFor(queryDate: Date = new Date()) {
const terms = await getAllTerms();
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 getEnrolledStudents(canvasCourseId: number) {
console.log(`Getting students for course ${canvasCourseId}`);
const url = `${canvasApi}/courses/${canvasCourseId}/enrollments?enrollment_type=student`;
const { data } = await axiosClient.get<CanvasEnrollmentModel[]>(url);
if (!data)
throw new Error(
`Something went wrong getting enrollments for ${canvasCourseId}`
);
return data;
},
};

View File

@@ -0,0 +1,68 @@
// services/canvasServiceUtils.ts
import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios";
import { axiosClient } from "../axiosUtils";
export const baseCanvasUrl = "https://snow.instructure.com";
export const canvasApi = baseCanvasUrl + "/api/v1";
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;
};
export async function paginatedRequest<T extends any[]>(request: {
url: string;
}): Promise<T> {
var requestCount = 1;
const url = new URL(request.url);
url.searchParams.set("per_page", "100");
const { data: firstData, headers: firstHeaders } = await axiosClient.get<T>(
url.toString()
);
var returnData = Array.isArray(firstData) ? [...firstData] : [firstData]; // terms come across as nested objects {enrolmentTerms: terms[]}
var nextUrl = getNextUrl(firstHeaders);
while (nextUrl) {
requestCount += 1;
const { data, headers } = await axiosClient.get<T>(nextUrl);
if (data) {
returnData = returnData.concat(Array.isArray(data) ? [...data] : [data]);
}
nextUrl = getNextUrl(headers);
}
if (requestCount > 1) {
console.log(
`Requesting ${typeof returnData} took ${requestCount} requests`
);
}
return returnData as T;
}

View File

@@ -0,0 +1,66 @@
import { AxiosResponse } from "axios";
import { axiosClient } from "../axiosUtils";
type FetchOptions = Omit<RequestInit, "method">;
const rateLimitRetryCount = 6;
const rateLimitSleepInterval = 1000;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const isRateLimited = async (
response: AxiosResponse
): Promise<boolean> => {
const content = await response.data;
return (
response.status === 403 &&
content.includes("403 Forbidden (Rate Limit Exceeded)")
);
};
const rateLimitAwarePost = async (url: string, body: any, retryCount = 0) => {
const response = await axiosClient.post(url, body);
if (await isRateLimited(response)) {
if (retryCount < rateLimitRetryCount) {
console.info(
`Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
);
await sleep(rateLimitSleepInterval);
return await rateLimitAwarePost(url, body, retryCount + 1);
}
}
return response;
};
export const rateLimitAwareDelete = async (
url: string,
retryCount = 0
): Promise<void> => {
try {
const response = await axiosClient.delete(url);
if (await isRateLimited(response)) {
console.info("After delete response in rate limited");
await sleep(rateLimitSleepInterval);
return await rateLimitAwareDelete(url, retryCount + 1);
}
} catch (e) {
const error = e as Error & { response?: Response };
if (error.response?.status === 403) {
if (retryCount < rateLimitRetryCount) {
console.info(
`Hit rate limit in delete, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
);
await sleep(rateLimitSleepInterval);
return await rateLimitAwareDelete(url, retryCount + 1);
} else {
console.info(
`Hit rate limit in delete, ${rateLimitRetryCount} retries did not fix it`
);
}
}
throw e;
}
};

View File

@@ -0,0 +1,101 @@
import fs from "fs/promises";
import path from "path";
import axios from "axios";
import { canvasApi } from "../canvasServiceUtils";
import { axiosClient } from "@/services/axiosUtils";
import FormData from "form-data";
export const downloadUrlToTempDirectory = async (
sourceUrl: string
): Promise<string> => {
try {
const fileName =
path.basename(new URL(sourceUrl).pathname) || `tempfile-${Date.now()}`;
const tempFilePath = path.join("/tmp", fileName);
const response = await axios.get(sourceUrl, {
responseType: "arraybuffer",
});
await fs.writeFile(tempFilePath, response.data);
return tempFilePath;
} catch (error) {
console.error("Error downloading or saving the file:", error);
throw error;
}
};
const getFileSize = async (pathToFile: string): Promise<number> => {
try {
const stats = await fs.stat(pathToFile);
return stats.size;
} catch (error) {
console.error("Error reading file size:", error);
throw error;
}
};
export const uploadToCanvasPart1 = async (
pathToUpload: string,
canvasCourseId: number
) => {
try {
const url = `${canvasApi}/courses/${canvasCourseId}/files`;
const formData = new FormData();
formData.append("name", path.basename(pathToUpload));
formData.append("size", (await getFileSize(pathToUpload)).toString());
const response = await axiosClient.post(url, formData);
const upload_url = response.data.upload_url;
const upload_params = response.data.upload_params;
return { upload_url, upload_params };
} catch (error) {
console.error("Error uploading file to Canvas part 1:", error);
throw error;
}
};
export const uploadToCanvasPart2 = async ({
pathToUpload,
upload_url,
upload_params,
}: {
pathToUpload: string;
upload_url: string;
upload_params: { [key: string]: string };
}) => {
try {
const formData = new FormData();
Object.keys(upload_params).forEach((key) => {
formData.append(key, upload_params[key]);
});
const fileBuffer = await fs.readFile(pathToUpload);
const fileName = path.basename(pathToUpload);
formData.append("file", fileBuffer, fileName);
const response = await axiosClient.post(upload_url, formData, {
headers: formData.getHeaders(),
validateStatus: (status) => status < 500,
});
if (response.status === 301) {
const redirectUrl = response.headers.location;
if (!redirectUrl) {
throw new Error(
"Redirect URL not provided in the Location header on redirect from second part of canvas file upload"
);
}
const redirectResponse = await axiosClient.get(redirectUrl);
console.log("redirect response", redirectResponse.data);
}
return response.data.url;
} catch (error) {
console.error("Error uploading file to Canvas part 2:", error);
throw error;
}
};

View File

@@ -0,0 +1,93 @@
import { RubricItem } from "@/models/local/assignment/rubricItem";
import { describe, expect, it } from "vitest";
import { getRubricCriterion } from "./canvasRubricUtils";
import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
describe("can prepare rubric for canvas", () =>{
it("can parse normal rubric into criterion", () =>{
const rubric: RubricItem[] = [
{
label: "first",
points: 1
},
{
label: "second",
points: 2
},
]
const criterion = getRubricCriterion(rubric)
expect(criterion).toStrictEqual({
0: {
description: "first",
points: 1,
ratings: {
0: { description: "Full Marks", points: 1 },
1: { description: "No Marks", points: 0 },
}
},
1: {
description: "second",
points: 2,
ratings: {
0: { description: "Full Marks", points: 2 },
1: { description: "No Marks", points: 0 },
}
}
})
})
it("can parse negative rubric into criterion", () =>{
const rubric: RubricItem[] = [
{
label: "first",
points: 1
},
{
label: "second",
points: -2
},
]
const criterion = getRubricCriterion(rubric)
expect(criterion).toStrictEqual({
0: {
description: "first",
points: 1,
ratings: {
0: { description: "Full Marks", points: 1 },
1: { description: "No Marks", points: 0 },
}
},
1: {
description: "second",
points: -2,
ratings: {
0: { description: "Full Marks", points: -2 },
1: { description: "No Marks", points: 0 },
}
}
})
})
it("negative rubric items do not contribute to the total", () =>{
const rubric: RubricItem[] = [
{
label: "first",
points: 1
},
{
label: "second",
points: -2
},
{
label: "second",
points: 3
},
]
const points = assignmentPoints(rubric)
expect(points).toBe(4)
})
})