adding canvas services

This commit is contained in:
2024-09-17 17:04:54 -06:00
parent 44330b85e9
commit f0f987764c
7 changed files with 319 additions and 3 deletions

View File

@@ -27,7 +27,7 @@ export default function CourseCalendar() {
className="
h-full
overflow-y-scroll
border-4
border-4
border-slate-600
rounded-xl
bg-slate-950

View File

@@ -13,7 +13,7 @@ export default async function CoursePage({}: {}) {
<div className="h-full flex flex-col">
<div className="flex flex-row min-h-0">
<DraggingContextProvider>
<div className="flex-1 min-h-0">
<div className="flex-1 min-h-0 flex flex-col">
<CourseNavigation />
<CourseCalendar />
</div>

View File

@@ -0,0 +1,8 @@
export const canvasQuizKeys = {
quizzes: (canvasCourseId: number )=> ["canvas", canvasCourseId, "quizzes"],
}
export const useCanvasQuizQuery

View File

@@ -0,0 +1,7 @@
import { CanvasRubric } from "./canvasRubric";
import { CanvasRubricAssociation } from "./canvasRubricAssociation";
export interface CanvasRubricCreationResponse {
rubric: CanvasRubric;
rubric_association: CanvasRubricAssociation;
}

View File

@@ -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;
};

View File

@@ -1,5 +1,10 @@
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 = {
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
);
},
};

View 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);
},
};