Edit: Adam - I just checked, and the copy and paste of the code below did not work for me. However, I noticed that they are on version 1.2.4, and there is a difference on the match lines of the two versions. Try replacing the one match line at the top with the two that I have shown below
Ron
Adam - I have version 1.2.3 - in case that is different.
Here is my line 34(and extras):
function popUp(text) {
$("#export_rubric_dialog").html(`<p>${text}</p>`);
$("#export_rubric_dialog").dialog({ buttons: {} });
}
If you want, I am not sure if it will work, but below is my entire version of the script. Delete the version you have, then on tampermonkey dashboard, click on the + sign next to installed scripts. Delete all the content in the window, and copy and paste the following - then click on file and save. Also check for differences in the top 10 lines or so between what you have and what I have
Ron
// ==UserScript==
// @Name Export Rubric Scores
// @namespace https://github.com/UCBoulder
// @description Export all rubric criteria scores for an assignment to a CSV
// @match https://canvas.*.edu/courses/*/gradebook/speed_grader?*
// @match https://*.instructure.com/courses/*/gradebook/speed_grader?*
// @grant none
// @run-at document-idle
// @version 1.2.3
// ==/UserScript==
/* globals $ */
// wait until the window jQuery is loaded
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(`<p>${text}</p>`);
$("#export_rubric_dialog").dialog({ buttons: {} });
}
function popClose() {
$("#export_rubric_dialog").dialog("close");
}
function getAllPages(url, callback) {
getRemainingPages(url, [], callback);
}
// Recursively work through paginated JSON list
function getRemainingPages(nextUrl, listSoFar, callback) {
$.getJSON(nextUrl, function(responseList, textStatus, jqXHR) {
var nextLink = null;
$.each(jqXHR.getResponseHeader("link").split(','), function (linkIndex, linkEntry) {
if (linkEntry.split(';')[1].includes('rel="next"')) {
nextLink = linkEntry.split(';')[0].slice(1, -1);
}
});
if (nextLink == null) {
// all pages have been retrieved
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}<br/><br/>Please refresh and try again.`, null);
window.removeEventListener("error", showError);
});
}
// escape commas and quotes for CSV formatting
function csvEncode(string) {
if (string && (string.includes('"') || string.includes(','))) {
return '"' + string.replace(/"/g, '""') + '"';
}
return string;
}
function showError(event) {
popUp(event.message);
window.removeEventListener("error", showError);
}
defer(function() {
'use strict';
// utility function for downloading a file
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($('<div id="export_rubric_dialog" title="Export Rubric Scores"></div>'));
// Only add the export button if a rubric is appearing
if ($('#rubric_summary_holder').length > 0) {
$('#gradebook_header div.statsMetric').append('<button type="button" class="Button" id="export_rubric_btn">Export Rubric Scores</button>');
$('#export_rubric_btn').click(function() {
popUp("Exporting scores, please wait...");
window.addEventListener("error", showError);
// Get some initial data from the current URL
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];
// Get the rubric data
$.getJSON(`/api/v1/courses/${courseId}/assignments/${assignId}`, function(assignment) {
// Get the user data
getAllPages(`/api/v1/courses/${courseId}/enrollments?per_page=100`, function(enrollments) {
// Get the rubric score data
getAllPages(`/api/v1/courses/${courseId}/assignments/${assignId}/submissions?include[]=rubric_assessment&per_page=100`, function(submissions) {
// Known Canvas bug where a rubric can appear in the UI but not in the API
if (!('rubric_settings' in assignment)) {
popUp(`ERROR: No rubric settings found at /api/v1/courses/${courseId}/assignments/${assignId}.<br/><br/> `
+ 'This is likely due to a Canvas bug where a rubric has entered a "soft-deleted" state. '
+ 'Please use the <a href="https://community.canvaslms.com/t5/Canvas-Admin-Blog/Undeleting-things-in-Canvas/ba-p/267116">Undelete feature</a> '
+ 'to restore the rubric associated with this assignment or contact Canvas Support.');
return;
}
// If rubric is set to hide points, then also hide points in export
// If rubric is set to use free form comments, then also hide ratings in export
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;
}
// Fill out the csv header and map criterion ids to sort index
// Also create an object that maps criterion ids to an object mapping rating ids to descriptions
var critOrder = {};
var critRatingDescs = {};
var 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';
// Iterate through submissions
var csvRows = [header];
$.each(submissions, function(subIndex, submission) {
const user = enrollments.find(i => i.user_id === submission.user_id).user;
if (user) {
var row = `${user.name},${user.sis_user_id},${submission.score},${submission.attempt}`;
// Add criteria scores and ratings
// Need to turn rubric_assessment object into an array
var crits = []
var critIds = []
if (submission.rubric_assessment != null) {
$.each(submission.rubric_assessment, function(critKey, critValue) {
if (hideRatings) {
crits.push({'id': critKey, 'points': critValue.points, 'rating': null});
} else {
crits.push({'id': critKey, 'points': critValue.points, 'rating': critRatingDescs[critKey][critValue.rating_id]});
}
critIds.push(critKey);
});
}
// Check for any criteria entries that might be missing; set them to null
$.each(critOrder, function(critKey, critValue) {
if (!critIds.includes(critKey)) {
crits.push({'id': critKey, 'points': null, 'rating': null});
}
});
// Sort into same order as column order
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();
saveText(csvRows, `Rubric Scores ${assignment.name.replace(/[^a-zA-Z 0-9]+/g, '')}.csv`);
window.removeEventListener("error", showError);
});
});
}).fail(function (jqXHR, textStatus, errorThrown) {
popUp(`ERROR ${jqXHR.status} while retrieving assignment data from Canvas. Please refresh and try again.`, null);
window.removeEventListener("error", showError);
});
});
}
});