mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
adding canvas services
This commit is contained in:
@@ -13,7 +13,7 @@ export default async function CoursePage({}: {}) {
|
|||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex flex-row min-h-0">
|
<div className="flex flex-row min-h-0">
|
||||||
<DraggingContextProvider>
|
<DraggingContextProvider>
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
<CourseNavigation />
|
<CourseNavigation />
|
||||||
<CourseCalendar />
|
<CourseCalendar />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
8
nextjs/src/hooks/canvas/canvasQuizHooks.ts
Normal file
8
nextjs/src/hooks/canvas/canvasQuizHooks.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const canvasQuizKeys = {
|
||||||
|
quizzes: (canvasCourseId: number )=> ["canvas", canvasCourseId, "quizzes"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useCanvasQuizQuery
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { CanvasRubric } from "./canvasRubric";
|
||||||
|
import { CanvasRubricAssociation } from "./canvasRubricAssociation";
|
||||||
|
|
||||||
|
export interface CanvasRubricCreationResponse {
|
||||||
|
rubric: CanvasRubric;
|
||||||
|
rubric_association: CanvasRubricAssociation;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { LocalAssignment } from "../localAssignment";
|
||||||
|
|
||||||
|
export const assignmentPoints = (assignment: LocalAssignment) => {
|
||||||
|
const basePoints = assignment.rubric
|
||||||
|
.map((r) =>
|
||||||
|
r.label.toLowerCase().includes("(extra credit)") ? 0 : r.points
|
||||||
|
)
|
||||||
|
.reduce((acc, current) => acc + current, 0);
|
||||||
|
return basePoints;
|
||||||
|
};
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
|
import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
|
||||||
import { canvasServiceUtils } from "./canvasServiceUtils";
|
import { baseCanvasUrl, canvasServiceUtils } 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";
|
||||||
|
|
||||||
export const canvasAssignmentService = {
|
export const canvasAssignmentService = {
|
||||||
async getAll(courseId: number): Promise<CanvasAssignment[]> {
|
async getAll(courseId: number): Promise<CanvasAssignment[]> {
|
||||||
@@ -15,4 +20,136 @@ export const canvasAssignmentService = {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async create(
|
||||||
|
canvasCourseId: number,
|
||||||
|
localAssignment: LocalAssignment,
|
||||||
|
canvasAssignmentGroupId?: number
|
||||||
|
): Promise<number> {
|
||||||
|
console.log(`Creating assignment: ${localAssignment.name}`);
|
||||||
|
const url = `${baseCanvasUrl}/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),
|
||||||
|
due_at: localAssignment.dueAt,
|
||||||
|
lock_at: localAssignment.lockAt,
|
||||||
|
points_possible: assignmentPoints(localAssignment),
|
||||||
|
assignment_group_id: canvasAssignmentGroupId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axiosClient.post<CanvasAssignment>(url, body);
|
||||||
|
const canvasAssignment = response.data;
|
||||||
|
|
||||||
|
if (!canvasAssignment) throw new Error("Created Canvas assignment is null");
|
||||||
|
|
||||||
|
await this.createRubric(
|
||||||
|
canvasCourseId,
|
||||||
|
canvasAssignment.id,
|
||||||
|
localAssignment
|
||||||
|
);
|
||||||
|
|
||||||
|
return canvasAssignment.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(
|
||||||
|
courseId: number,
|
||||||
|
canvasAssignmentId: number,
|
||||||
|
localAssignment: LocalAssignment,
|
||||||
|
canvasAssignmentGroupId?: number
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`Updating assignment: ${localAssignment.name}`);
|
||||||
|
const url = `${baseCanvasUrl}/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),
|
||||||
|
due_at: localAssignment.dueAt,
|
||||||
|
lock_at: localAssignment.lockAt,
|
||||||
|
points_possible: assignmentPoints(localAssignment),
|
||||||
|
assignment_group_id: canvasAssignmentGroupId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await axiosClient.put(url, body);
|
||||||
|
await this.createRubric(courseId, canvasAssignmentId, localAssignment);
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(
|
||||||
|
courseId: number,
|
||||||
|
assignmentCanvasId: number,
|
||||||
|
assignmentName: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`Deleting assignment from Canvas: ${assignmentName}`);
|
||||||
|
const url = `${baseCanvasUrl}/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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createRubric(
|
||||||
|
courseId: number,
|
||||||
|
assignmentCanvasId: number,
|
||||||
|
localAssignment: LocalAssignment
|
||||||
|
): Promise<void> {
|
||||||
|
const criterion = localAssignment.rubric.map((rubricItem, i) => ({
|
||||||
|
description: rubricItem.label,
|
||||||
|
points: rubricItem.points,
|
||||||
|
ratings: [
|
||||||
|
{ description: "Full Marks", points: rubricItem.points },
|
||||||
|
{ description: "No Marks", points: 0 },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 = `${baseCanvasUrl}/courses/${courseId}/rubrics`;
|
||||||
|
const rubricResponse = await axiosClient.post<CanvasRubricCreationResponse>(
|
||||||
|
rubricUrl,
|
||||||
|
rubricBody
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rubricResponse.data) throw new Error("Failed to create rubric");
|
||||||
|
|
||||||
|
const assignmentPointAdjustmentUrl = `${baseCanvasUrl}/courses/${courseId}/assignments/${assignmentCanvasId}`;
|
||||||
|
const assignmentPointAdjustmentBody = {
|
||||||
|
assignment: { points_possible: assignmentPoints(localAssignment) },
|
||||||
|
};
|
||||||
|
|
||||||
|
await axiosClient.put(
|
||||||
|
assignmentPointAdjustmentUrl,
|
||||||
|
assignmentPointAdjustmentBody
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
154
nextjs/src/services/canvas/canvasQuizService.ts
Normal file
154
nextjs/src/services/canvas/canvasQuizService.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
|
||||||
|
import { axiosClient } from "../axiosUtils";
|
||||||
|
import { baseCanvasUrl } from "./canvasServiceUtils";
|
||||||
|
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||||
|
import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
|
||||||
|
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
|
||||||
|
import { canvasAssignmentService } from "./canvasAssignmentService";
|
||||||
|
import { LocalQuizQuestion } from "@/models/local/quiz/localQuizQuestion";
|
||||||
|
|
||||||
|
const getAnswers = (question: LocalQuizQuestion) => {
|
||||||
|
return question.answers.map((answer: any) => ({
|
||||||
|
answer_html: answer.htmlText,
|
||||||
|
answer_weight: answer.correct ? 100 : 0,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const createQuestionOnly = async (
|
||||||
|
canvasCourseId: number,
|
||||||
|
canvasQuizId: number,
|
||||||
|
question: LocalQuizQuestion,
|
||||||
|
position: number
|
||||||
|
): Promise<{ question: any; position: number }> => {
|
||||||
|
console.log("Creating individual question", question);
|
||||||
|
|
||||||
|
const url = `${baseCanvasUrl}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/questions`;
|
||||||
|
const body = {
|
||||||
|
question: {
|
||||||
|
question_text: markdownToHTMLSafe(question.text),
|
||||||
|
question_type: `${question.questionType}_question`,
|
||||||
|
points_possible: question.points,
|
||||||
|
position,
|
||||||
|
answers: getAnswers(question),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axiosClient.post(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 = `${baseCanvasUrl}/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")
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletionTasks = assignmentsToDelete.map((assignment) =>
|
||||||
|
canvasAssignmentService.delete(
|
||||||
|
canvasCourseId,
|
||||||
|
assignment.id,
|
||||||
|
assignment.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(deletionTasks);
|
||||||
|
console.log(`Deleted ${assignmentsToDelete.length} redundant assignments`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canvasQuizService = {
|
||||||
|
async getAll(canvasCourseId: number): Promise<CanvasQuiz[]> {
|
||||||
|
const url = `${baseCanvasUrl}/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,
|
||||||
|
canvasAssignmentGroupId?: number
|
||||||
|
): Promise<number> {
|
||||||
|
console.log("Creating quiz", localQuiz);
|
||||||
|
|
||||||
|
const url = `${baseCanvasUrl}/courses/${canvasCourseId}/quizzes`;
|
||||||
|
const body = {
|
||||||
|
quiz: {
|
||||||
|
title: localQuiz.name,
|
||||||
|
description: markdownToHTMLSafe(localQuiz.description),
|
||||||
|
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 response = await axiosClient.post(url, body);
|
||||||
|
const canvasQuiz: CanvasQuiz = response.data;
|
||||||
|
|
||||||
|
if (!canvasQuiz) throw new Error("Created quiz is null");
|
||||||
|
|
||||||
|
await this.createQuizQuestions(canvasCourseId, canvasQuiz.id, localQuiz);
|
||||||
|
return canvasQuiz.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createQuizQuestions(
|
||||||
|
canvasCourseId: number,
|
||||||
|
canvasQuizId: number,
|
||||||
|
localQuiz: LocalQuiz
|
||||||
|
) {
|
||||||
|
console.log("Creating quiz questions", localQuiz);
|
||||||
|
|
||||||
|
const tasks = localQuiz.questions.map((question, index) =>
|
||||||
|
createQuestionOnly(canvasCourseId, canvasQuizId, question, index)
|
||||||
|
);
|
||||||
|
const questionAndPositions = await Promise.all(tasks);
|
||||||
|
await hackFixQuestionOrdering(
|
||||||
|
canvasCourseId,
|
||||||
|
canvasQuizId,
|
||||||
|
questionAndPositions
|
||||||
|
);
|
||||||
|
await hackFixRedundantAssignments(canvasCourseId);
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user