-
-
Save dslusser/6d3282bb249f3834b25c2687b4d76474 to your computer and use it in GitHub Desktop.
Snippet to download Canvas rubric data for current assignment as a CSV file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(async function(){ | |
// More info on usage - dws: | |
// https://community.canvaslms.com/t5/Canvas-Developers-Group/Rubric-Analysis-Using-the-API/ba-p/270213 | |
// Basically just copy/paste this script to the Developer Tools -> Console tab on any Canvas Assignment page | |
// that has a rubic as a grading method. | |
// Original Github Gist: https://gist.github.com/acbart/0bfd1b2dbc324b345c305e362e00273c | |
// https://stackoverflow.com/questions/8735792/how-to-parse-link-header-from-github-api | |
const linkParser = (linkHeader)=>{ | |
let re = /,[\s]*<(.*?)>;[\s]*rel="next"/g; | |
let result = re.exec(linkHeader); | |
if (result == null) { | |
return null; | |
} | |
return result[1]; | |
} | |
function downloadBlob(content, filename, contentType) { | |
// Create a blob | |
var blob = new Blob([content], { type: contentType }); | |
var url = URL.createObjectURL(blob); | |
// Create a link to download it | |
var pom = document.createElement('a'); | |
pom.href = url; | |
pom.setAttribute('download', filename); | |
pom.click(); | |
} | |
const course = ENV.context_asset_string; | |
const courseId = course.split("_")[1]; | |
const base = ENV.DEEP_LINKING_POST_MESSAGE_ORIGIN; | |
const api = `${base}/api/v1`; | |
const courseUrl = `${api}/courses/${courseId}`; | |
const noApiCourseUrl = `${base}/courses/${courseId}`; | |
const assignmentId = ENV.ASSIGNMENT_ID || prompt("What assignment ID should I use?"); | |
// Get all the data at this endpoint | |
async function getAll(endpoint, data) { | |
if (data === undefined) { | |
data = {}; | |
} | |
let all = []; | |
let next = courseUrl+endpoint; | |
do { | |
let response = await fetch(next); | |
all.push(...await response.json()); | |
let links = response.headers.get('link'); | |
next = linkParser(links); | |
} while (next != null); | |
return await all; | |
} | |
const getNearest = (goal, values) => | |
values.reduce((prev, curr) => Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); | |
let users = await getAll('/users?per_page=100'); | |
let userMap = Object.fromEntries(users.map(u => [u.id, u])); | |
let submissions = await getAll(`/assignments/${assignmentId}/submissions?include[]=assignment&include[]=rubric_assessment&per_page=100`); | |
const keeps = ['workflow_state', 'user_id', 'submitted_at', | |
'late', 'score', 'grade', 'grader_id', | |
'graded_at', 'assignment_id', 'id']; | |
//const criteriaNameAndPointsArray = new Array(); // dws | |
let results = submissions.map(submission => { | |
if (!(submission.user_id in userMap)) { | |
return false; | |
} | |
const result = {points_possible: submission.assignment.points_possible}; | |
keeps.forEach(keepKey => { result[keepKey] = submission[keepKey]}); | |
const rubricMap = {}; | |
submission.assignment.rubric.forEach(rubricItem => { | |
rubricMap[rubricItem.id] = rubricItem; | |
}); | |
if ('rubric_assessment' in submission) { | |
result.subscores = Object.fromEntries( | |
Object.entries(submission.rubric_assessment) | |
.map(([id, data]) => { | |
const rubricData = rubricMap[id]; | |
ratings = Object.fromEntries(rubricData.ratings.map(rating => { | |
return [rating.points, rating.description]; | |
})) | |
//console.dir(rubricData, { depth: null }) // dws | |
//console.log(rubricData.description) // dws | |
//console.log(rubricData.description + " - " + rubricData.points + " points max") //dws | |
rubricData.description = rubricData.description + " - " + rubricData.points + " points max"; //dws | |
//console.log(rubricData.description) // dws | |
//console.log("rubricData = " + rubricData) //dws | |
//console.log("rubricData.description = " + rubricData.description) //dws | |
//console.log("rubricData.points = " + rubricData.points) //dws | |
//console.log("New title = " + rubricData.description + " - " + rubricData.points + " points max") //dws | |
//var criteriaNameAndPoints = rubricData.description + " - " + rubricData.points + " points max" //dws | |
//console.log("criteriaNameAndPoints = " + criteriaNameAndPoints) //dws | |
//criteriaNameAndPointsArray.push(criteriaNameAndPoints) // dws | |
//console.log("criteriaNameAndPointsArray = " + criteriaNameAndPointsArray) //dws | |
// ORIGINAL - dws | |
return [rubricData.description, { | |
name: rubricData.description, | |
points: data.points, | |
score: ratings[getNearest(data.points, Object.keys(ratings))], | |
comment: data.comments, | |
points_possible: rubricData.points | |
}]; | |
// NEW (No longer needed now, refactored) - dws | |
/*return [rubricData.description, { | |
name: criteriaNameAndPoints, | |
points: data.points, | |
score: ratings[getNearest(data.points, Object.keys(ratings))], | |
comment: data.comments, | |
points_possible: rubricData.points | |
}];*/ | |
})); | |
} else { | |
result.subscores = {}; | |
} | |
return result; | |
}); | |
//console.log("criteraNameAndPointsArray = " + criteriaNameAndPointsArray) //dws | |
const headers = new Set(); | |
//console.log("results = " + JSON.stringify(results, null, 2)) // dws | |
//console.dir(results, { depth: null }) // dws | |
results.filter(Boolean).forEach(item => Object.keys(item.subscores).forEach(h => headers.add(h))); | |
//console.log("headers = " + headers) // dws | |
const defaultHeaders = ["Student", "SID", "Email", "Submitted", "Late", "Grader", "Graded", "Grade", ...headers, "Comments"]; | |
//console.log("defaultHeaders = " + defaultHeaders) // dws | |
// TODO: Make sure we have at least one submission so we can get the assignment name! | |
const filename = submissions[0].assignment.name + "_RubricScores.csv"; | |
const csv = [defaultHeaders.join(","), | |
...results.filter(Boolean).map((sub => { | |
//console.log(userMap, sub.user_id); //THIS IS AN ORIGINAL CONSOLE.LOG, UNCOMMENT LATER | |
const user = userMap[sub.user_id]; | |
const grader = userMap[sub.grader_id]; | |
const comments = []; | |
const items = []; | |
headers.forEach(h => { | |
if (h in sub.subscores) { | |
//items.push(sub.subscores[h].score); // ORG - Full Credit, Partial Credit, No Credit word values - dws | |
items.push(sub.subscores[h].points); // NEW - Use point values instead - dws | |
if (sub.subscores[h].comment) { | |
comments.push(`${h}: ${sub.subscores[h].comment}`) | |
} | |
} else { | |
items.push(""); | |
} | |
}); | |
return [user.sortable_name, user.login_id, user.email, | |
sub.submitted_at, sub.late, | |
grader?.sortable_name, grader?.graded_at, | |
sub.grade, ...items, comments.join("\n---\n")] | |
.map(String) // convert every value to String | |
.map(v => v.replaceAll('"', '""')) // escape double colons | |
.map(v => `"${v}"`) // quote it | |
.join(','); // comma-separated | |
}))].join("\n"); | |
downloadBlob(csv, filename, "text/csv;charset=utf-8;") | |
console.log(results); | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment