// ==UserScript== // @name Export Rubric Scores // @namespace https://github.com/UCBoulder // @description Export all rubric criteria scores for an assignment to a CSV // @match https://*/courses/*/gradebook/speed_grader?* // @grant none // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://code.jquery.com/ui/1.14.1/jquery-ui.min.js // @run-at document-idle // @version 1.2.6 // ==/UserScript== /* globals $ */ function defer(method) { if (typeof $ !== 'undefined') { method(); } else { setTimeout(function () { defer(method); }, 100); } } function waitForElement(selector, callback) { if ($(selector).length) { callback(); } else { setTimeout(function () { waitForElement(selector, callback); }, 100); } } function popUp(text) { $("#export_rubric_dialog").html(`

${text}

`); $("#export_rubric_dialog").dialog({ buttons: {} }); } function popClose() { $("#export_rubric_dialog").dialog("close"); } function getAllPages(url, callback) { getRemainingPages(url, [], callback); } function getRemainingPages(nextUrl, listSoFar, callback) { $.getJSON(nextUrl, function (responseList, textStatus, jqXHR) { let nextLink = null; const linkHeader = jqXHR.getResponseHeader("link"); if (linkHeader) { $.each(linkHeader.split(','), function (linkIndex, linkEntry) { if (linkEntry.split(';')[1].includes('rel="next"')) { nextLink = linkEntry.split(';')[0].slice(1, -1); } }); } if (!nextLink) { callback(listSoFar.concat(responseList)); } else { getRemainingPages(nextLink, listSoFar.concat(responseList), callback); } }).fail(function (jqXHR, textStatus, errorThrown) { popUp(`ERROR ${jqXHR.status} while retrieving data from Canvas. Url: ${nextUrl}

Please refresh and try again.`); window.removeEventListener("error", showError); }); } function csvEncode(string) { if (string && (string.includes('"') || string.includes(','))) { return '"' + string.replace(/"/g, '""') + '"'; } return string; } function showError(event) { popUp("JavaScript error: " + event.message); window.removeEventListener("error", showError); } defer(function () { 'use strict'; var saveText = (function () { var a = document.createElement("a"); document.body.appendChild(a); a.style = "display: none"; return function (textArray, fileName) { var blob = new Blob(textArray, { type: "text" }), url = window.URL.createObjectURL(blob); a.href = url; a.download = fileName; a.click(); window.URL.revokeObjectURL(url); }; })(); $("body").append($('
')); try { if ($('#rubric_summary_holder').length > 0) { $('#gradebook_header div.statsMetric').append(''); } else { console.warn("Rubric summary holder not found. Export button not inserted."); } } catch (e) { popUp("DOM error while trying to insert export button: " + e.message); } $('#export_rubric_btn').click(function () { try { popUp("Exporting scores, please wait..."); window.addEventListener("error", showError); const courseId = window.location.href.split('/')[4]; const urlParams = window.location.href.split('?')[1].split('&'); const assignId = urlParams.find(i => i.split('=')[0] === "assignment_id").split('=')[1]; $.getJSON(`/api/v1/courses/${courseId}/assignments/${assignId}`, function (assignment) { getAllPages(`/api/v1/courses/${courseId}/enrollments?per_page=100`, function (enrollments) { getAllPages(`/api/v1/courses/${courseId}/assignments/${assignId}/submissions?include[]=rubric_assessment&per_page=100`, function (submissions) { if (!('rubric_settings' in assignment)) { popUp(`ERROR: No rubric settings found at /api/v1/courses/${courseId}/assignments/${assignId}.

This is likely due to a Canvas bug where a rubric has entered a "soft-deleted" state. Please use the Undelete feature to restore the rubric associated with this assignment or contact Canvas Support.`); return; } const hidePoints = assignment.rubric_settings.hide_points; const hideRatings = assignment.rubric_settings.free_form_criterion_comments; if (hidePoints && hideRatings) { popUp("ERROR: This rubric is configured to use free-form comments instead of ratings AND to hide points, so there is nothing to export!"); return; } let critOrder = {}; let critRatingDescs = {}; let header = "Student Name,Student ID,Posted Score,Attempt Number"; $.each(assignment.rubric, function (critIndex, criterion) { critOrder[criterion.id] = critIndex; if (!hideRatings) { critRatingDescs[criterion.id] = {}; $.each(criterion.ratings, function (i, rating) { critRatingDescs[criterion.id][rating.id] = rating.description; }); header += ',' + csvEncode('Rating: ' + criterion.description); } if (!hidePoints) { header += ',' + csvEncode('Points: ' + criterion.description); } }); header += '\n'; var csvRows = [header]; $.each(submissions, function (subIndex, submission) { const enrollment = enrollments.find(i => i.user_id === submission.user_id); if (enrollment && enrollment.user) { const user = enrollment.user; let row = `${user.name},${user.sis_user_id},${submission.score},${submission.attempt}`; let crits = []; let critIds = []; if (submission.rubric_assessment != null) { $.each(submission.rubric_assessment, function (critKey, critValue) { crits.push({ id: critKey, points: critValue.points ?? null, rating: hideRatings ? null : (critRatingDescs[critKey]?.[critValue.rating_id] ?? null) }); critIds.push(critKey); }); } $.each(critOrder, function (critKey) { if (!critIds.includes(critKey)) { crits.push({ id: critKey, points: null, rating: null }); } }); crits.sort(function (a, b) { return critOrder[a.id] - critOrder[b.id]; }); $.each(crits, function (critIndex, criterion) { if (!hideRatings) { row += `,${csvEncode(criterion.rating)}`; } if (!hidePoints) { row += `,${criterion.points}`; } }); row += '\n'; csvRows.push(row); } }); popClose(); const fileName = `Rubric_Scores_${assignment.name.replace(/[^a-zA-Z0-9]/g, '_')}.csv`; saveText(csvRows, fileName); window.removeEventListener("error", showError); }); }); }).fail(function (jqXHR, textStatus, errorThrown) { popUp(`ERROR ${jqXHR.status} while retrieving assignment data from Canvas. Please refresh and try again.`); window.removeEventListener("error", showError); }); } catch (e) { popUp("Unexpected error: " + e.message); window.removeEventListener("error", showError); } }); });