Files
mr-drews-assignment-creator/lib/canvas/api.js
T
bizzleandClaude Opus 4.8 4036ad5839 Sync Canvas LMS export feature + refreshed Settings screenshot
Brings the share build up to date with the main project: QTI .zip export
and optional direct Canvas API push, plus the Canvas settings card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:43:35 -04:00

87 lines
3.2 KiB
JavaScript

// lib/canvas/api.js — render the neutral Canvas IR (map.js) into Canvas *Classic Quizzes*
// REST payloads. Same IR the .zip path uses, so question mapping lives in exactly one place.
// Used by the server route app/api/canvas/route.js (browser→Canvas is blocked by CORS).
export function normalizeBaseUrl(url) {
let u = String(url || "").trim();
if (!u) return "";
if (!/^https?:\/\//i.test(u)) u = "https://" + u;
return u.replace(/\/+$/, "");
}
// Strip tags + decode the few entities we emit, for the plain answer_text fallback.
function stripTags(html) {
return String(html || "")
.replace(/<[^>]+>/g, "")
.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"');
}
// IR.quiz -> body for POST /api/v1/courses/:id/quizzes
export function quizPayload(ir) {
const q = ir.quiz;
const quiz = {
title: q.title,
description: q.description || "",
quiz_type: q.quizType,
shuffle_answers: !!q.shuffleAnswers,
allowed_attempts: q.allowedAttempts,
scoring_policy: q.scoringPolicy,
show_correct_answers: q.showCorrectAnswers !== false,
one_question_at_a_time: !!q.oneQuestionAtATime,
cant_go_back: !!(q.oneQuestionAtATime && q.cantGoBack),
published: false, // import unpublished so the teacher reviews before students see it
};
if (q.timeLimit !== "" && q.timeLimit != null) quiz.time_limit = q.timeLimit;
if (q.accessCode) quiz.access_code = q.accessCode;
if (q.dueAt) quiz.due_at = q.dueAt;
if (q.unlockAt) quiz.unlock_at = q.unlockAt;
if (q.lockAt) quiz.lock_at = q.lockAt;
return { quiz };
}
// IR.items -> array of bodies for POST /api/v1/courses/:id/quizzes/:qid/questions
export function questionPayloads(ir) {
return ir.items.map((it, i) => ({ question: questionBody(it, i + 1) }));
}
function questionBody(it, position) {
const body = {
question_name: it.title || `Question ${position}`,
question_text: it.promptHtml || "",
question_type: it.canvasType,
points_possible: it.points || 0,
position,
};
if (it.feedback?.neutralHtml) body.neutral_comments_html = it.feedback.neutralHtml;
switch (it.canvasType) {
case "multiple_choice_question":
case "true_false_question":
body.answers = it.choices.map((c) => ({
answer_html: c.html, answer_text: stripTags(c.html), answer_weight: c.correct ? 100 : 0,
}));
break;
case "fill_in_multiple_blanks_question":
body.answers = it.blanks.flatMap((b) =>
b.answers.map((a) => ({ answer_text: a.textPlain, answer_weight: 100, blank_id: b.name })));
break;
case "matching_question": {
const rightText = (ident) => (it.rights.find((r) => r.ident === ident)?.textPlain) || "";
body.answers = it.lefts.map((l) => ({
answer_match_left: l.textPlain,
answer_match_right: rightText(l.correctRightIdent),
answer_weight: 100,
}));
const used = new Set(it.lefts.map((l) => l.correctRightIdent));
const distractors = it.rights.filter((r) => !used.has(r.ident)).map((r) => r.textPlain);
if (distractors.length) body.matching_answer_incorrect_matches = distractors.join("\n");
break;
}
case "essay_question":
case "text_only_question":
default:
body.answers = [];
}
return body;
}