How to auto import and delete assignment comments on Canvas
Canvas does have a built-in feature to import numeric grades from a csv file, but it does not yet support importing the textual comments. This is one of the common difficulties that TAs and instructors face when using Canvas to report the grades.
Still, there is a workaround to use the Canvas API to avoid manually copy-and-pasting the comments. In this post, I’ll walk you through this workaround.
Importing comments
Step 1. You should prepare a CSV file that has the following format
- The very first column needs to be the “ID” column which contains the SIS User ID that is unique for each student. The column name should exactly be “ID” or it will cause an error.
- Then include the assignment column that contains the comments. The name of the assignment column has to be in the format of
Assignment Name (Assignment ID)
.
An easy way to come up with this file is to use the exported gradebook.
- On Canvas gradebook page, press [Actions] - [Export Entire Gradebook]
- Delete all columns except for the ID column and assignment column that you wish to post comments for
- Delete any filler rows between the header row and the list of students (e.g., the “Points Possible” row)
- Replace the scores listed with the comments you wish to post. If the scores on Canvas will remain safe, so no worries. If you leave the cell blank, there will be no comment created.
Step 2. Install TamperMonkey
TamperMonkey is a browser extension that allows you to run userscripts in certain web pages. It is available for Chrome and Firefox.
You can install it at https://www.tampermonkey.net
Step 3. Create a new script with TamperMonkey
Click on the TamperMonkey icon on the top bar of your browser and press [Create a new script].
You’ll see an editor where you can write codes. There, please paste the following codes.
// ==UserScript==
// @name Comments Importer
// @namespace https://github.com/UCBoulder
// @description Bulk import assignment comments into the Canvas gradebook.
// @match https://*/courses/*/gradebook
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.1.0/papaparse.min.js
// @run-at document-idle
// @version 1.1.4
// ==/UserScript==
/* globals $ Papa */
// wait until the window jQuery is loaded
function defer(method) {
if (typeof $ !== 'undefined' && typeof $().dialog !== 'undefined') {
method();
}
else {
setTimeout(function() { defer(method); }, 100);
}
}
defer(function() {
'use strict';
// utility function for downloading an error report
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);
};
}());
// prep jquery info dialog
$("body").append($('<div id="comments_dialog" title="Import Comments"></div>'));
$("#comments_dialog").dialog({ autoOpen: false });
function popUp(text) {
$("#comments_dialog").html(`<p>${text}</p>`);
$("#comments_dialog").dialog('open');
}
// prep jquery confirm dialog
$("body").append($('<div id="comments_modal" title="Import Comments"></div>'));
$("#comments_modal").dialog({ modal: true, autoOpen: false });
function confirm(text, callback) {
$("#comments_modal").html(`<p>${text}</p>`);
$("#comments_modal").dialog({
buttons: {
"Confirm": function() {
$(this).dialog("close");
callback(true);
},
"Cancel": function() {
$(this).dialog("close");
callback(false);
}
}
});
$("#comments_modal").dialog('open');
}
// prep jquery progress dialog
$("body").append($('<div id="comments_progress" title="Import Comments"><p>Importing comments. Do not navigate from this page.</p><div id="comments_bar"></div></div>'));
$("#comments_progress").dialog({ buttons: {}, autoOpen: false });
function showProgress(amount) {
if (amount === 100) {
$("#comments_progress").dialog("close");
} else {
$("#comments_bar").progressbar({ value: amount });
$("#comments_progress").dialog("open");
}
}
// add choose file button to gradebook
var importDiv = $(`<div style="padding-top:10px">
<label for="comments_file">Import comments: </label>
<input type="file" id="comments_file"/>
</div>`);
$("div.gradebook-menus").append(importDiv);
// handle when file is selected
$('#comments_file').change(function(evt) {
$("#comments_file").hide();
// parse CSV
Papa.parse(evt.target.files[0], {
header: true,
dynamicTyping: false,
complete: function(results) {
$("#comments_file").val('');
var data = results.data;
var referral = ' Visit <a href="https://oit.colorado.edu/services/teaching-learning-applications/canvas/enhancements-integrations/enhancements#oit" target="_blank">Canvas - Enhancements</a> for formatting guidelines.';
if (data.length < 1) {
popUp("ERROR: File should contain a header row and at least one data row." + referral);
$("#comments_file").show();
return;
}
if (!Object.keys(data[0]).includes("ID")) {
popUp("ERROR: No 'ID' column found." + referral);
$("#comments_file").show();
return;
}
if (Object.keys(data[0]).length < 2) {
popUp("ERROR: Header row should have a 'ID' column and at least one assignment column." + referral);
$("#comments_file").show();
return;
}
// build requests
var requests = [];
for (const row of data) {
var student = row["ID"];
for (const assignment of Object.keys(row)) {
if (assignment === "ID") {
continue;
}
// extract assignment id from assignment header
var idWithParens = assignment.match(/\(\d+\)$/);
if (!idWithParens) {
popUp(`ERROR: "${assignment}" is not a properly formatted assignment name.` + referral);
$("#comments_file").show();
return;
}
var assignId = idWithParens[0].match(/\d+/)[0];
var comment = row[assignment];
if (!comment || !comment.trim()) {
continue;
}
// extract course id from url
var courseId = window.location.href.split('/')[4];
// build api url
var subUrl = `/api/v1/courses/${courseId}/assignments/${assignId}/submissions/sis_user_id:${student}`;
// build request and canned error message in case it fails
requests.push({
request: {
url: subUrl,
type: "PUT",
data: {"comment[text_comment]": comment},
dataType: "text" },
error: `Failed to post comment for student ${student} and assignment ${assignment} using endpoint ${subUrl}. Response: `
});
}
}
// confirm before proceeding
confirm(
`You are about to post ${requests.length} new comments. This cannot be undone. Are you sure you wish to proceed?`,
function(confirmed) {
if (confirmed) {
// send requests in chunks of 10 every second to avoid rate-limiting
var errors = [];
var completed = 0;
var chunkSize = 10;
function sendChunk(i) {
for (const request of requests.slice(i, i+chunkSize)) {
$.ajax(request.request).fail(function(jqXHR, textStatus, errorThrown) {
errors.push(`${request.error}${jqXHR.status} - ${errorThrown}\n`);
}).always(requestSent);
}
showProgress(i * 100 / requests.length);
if (i + chunkSize < requests.length) {
setTimeout(sendChunk, 1000, i + chunkSize);
}
}
// when each request finishes...
function requestSent() {
completed++;
if (completed >= requests.length) {
// all finished
showProgress(100);
$("#comments_file").show();
if (errors.length > 0) {
popUp(`Import complete. WARNING: ${errors.length} comments failed to import. See errors.txt for details.
`);
saveText(errors, "errors.txt");
} else {
popUp("All comments imported successfully!");
}
}
}
// actually starts the recursion
sendChunk(0);
} else {
// confirmation was dismissed
$("#comments_file").show();
}
});
}
});
});
});
Step 4. Import Comments on Canvas
Go to the Canvas gradebook page and you’ll see a file uploader button for importing comments as shown in the following screenshot. Click on it and import the CSV file you prepared in Step 1.
💡 Common Error
If it does not work, one of the common reasons is because of the name of the ID column. Match the ID column name with the ones specified in the code above. For example, if your CSV file uses the column named “SIS User ID” as the identifier, replace the keyword “ID” in the code to “SIS User ID”.
Deleting existing comments
I’ll start by assuming that you have already finished installing TamperMonkey (i.e., Step 2 in the previous section).
Step 1. You should prepare a CSV file that has the following format
- The very first column needs to be the “ID” column which contains the SIS User ID that is unique for each student whose comments should be deleted. The column name should exactly be “ID” or it will cause an error.
- The name of the second column has to be in the format of
Assignment Name (Assignment ID)
, the assignment where comments should be deleted.
Step 2. Create a new script with TamperMonkey
Click on the TamperMonkey icon on the top bar of your browser and press [Create a new script].
You’ll see an editor where you can write codes. There, please paste the following codes.
// ==UserScript==
// @name Comments Importer
// @namespace https://github.com/UCBoulder
// @description Bulk import assignment comments into the Canvas gradebook.
// @match https://*/courses/*/gradebook
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.1.0/papaparse.min.js
// @run-at document-idle
// @version 1.1.4
// ==/UserScript==
/* globals $ Papa */
// wait until the window jQuery is loaded
function defer(method) {
if (typeof $ !== 'undefined' && typeof $().dialog !== 'undefined') {
method();
}
else {
setTimeout(function() { defer(method); }, 100);
}
}
defer(function() {
'use strict';
// prep jquery info dialog
$("body").append($('<div id="comments_dialog" title="Delete Comments"></div>'));
$("#comments_dialog").dialog({ autoOpen: false });
function popUp(text) {
$("#comments_dialog").html(`<p>${text}</p>`);
$("#comments_dialog").dialog('open');
}
// prep jquery confirm dialog
$("body").append($('<div id="comments_modal" title="Delete Comments"></div>'));
$("#comments_modal").dialog({ modal: true, autoOpen: false });
function confirm(text, callback) {
$("#comments_modal").html(`<p>${text}</p>`);
$("#comments_modal").dialog({
buttons: {
"Confirm": function() {
$(this).dialog("close");
callback(true);
},
"Cancel": function() {
$(this).dialog("close");
callback(false);
}
}
});
$("#comments_modal").dialog('open');
}
// prep jquery progress dialog
$("body").append($('<div id="comments_progress" title="Delete Comments"><p>Deleting comments. Do not navigate from this page.</p><div id="comments_bar"></div></div>'));
$("#comments_progress").dialog({ buttons: {}, autoOpen: false });
function showProgress(amount) {
if (amount === 100) {
$("#comments_progress").dialog("close");
} else {
$("#comments_bar").progressbar({ value: amount });
$("#comments_progress").dialog("open");
}
}
// add choose file button to gradebook
var importDiv = $(`<div style="padding-top:10px"><label for="deleteComments">Delete comments: </label><input type="file" id="deleteComments"/></div>`);
$("div.gradebook-menus").append(importDiv);
// handle when file is selected
$('#deleteComments').change(function(evt) {
$("#deleteComments").hide();
// parse CSV
Papa.parse(evt.target.files[0], {
header: true,
dynamicTyping: false,
complete: function(results) {
$("#deleteComments").val('');
var data = results.data;
if (data.length < 1) {
popUp("ERROR: File should contain a header row and at least one data row.");
$("#deleteComments").show();
return;
}
if (!Object.keys(data[0]).includes("ID")) {
popUp("ERROR: No 'ID' column found.");
$("#deleteComments").show();
return;
}
if (Object.keys(data[0]).length < 2) {
popUp("ERROR: Header row should have a 'ID' column and at least one assignment column.");
$("#deleteComments").show();
return;
}
function httpGet(theUrl) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", theUrl, false ); // false for synchronous request
xmlHttp.send( null );
return xmlHttp.responseText;
}
var studentIds = data.map(d => d.ID);
var idWithParens = Object.keys(data[0])[1].match(/\(\d+\)$/);
var assignId = idWithParens[0].match(/\d+/)[0];
var courseId = window.location.href.split('/')[4];
// build requests
var requests = []
for (let studentId of studentIds) {
let commentIds = [];
try {
let result = httpGet(`/api/v1/courses/${courseId}/assignments/${assignId}/submissions/${studentId}?include=submission_comments`)
if ("submission_comments" in JSON.parse(result)) {
commentIds = JSON.parse(result)["submission_comments"].map(obj => obj.id);
}
} catch (err) {
console.log(err);
}
for (let commentId of commentIds) {
// build api url
var subUrl = `/api/v1/courses/${courseId}/assignments/${assignId}/submissions/${studentId}/comments/${commentId}`;
// build request and canned error message in case it fails
requests.push({
request: {
url: subUrl,
type: "DELETE", // DELETE /api/v1/courses/:course_id/assignments/:assignment_id/submissions/:user_id/comments/:id
data: {},
dataType: "text"
},
error: `Failed to delete comment ${commentId} for student ${studentId} and assignment ${assignId} using endpoint ${subUrl}. Response: `
});
}
}
// confirm before proceeding
confirm(
`You are about to delete ${requests.length} existing comments. This cannot be undone. Are you sure you wish to proceed?`,
function(confirmed) {
if (confirmed) {
// send requests in chunks of 10 every second to avoid rate-limiting
var errors = [];
var completed = 0;
var chunkSize = 10;
function sendChunk(i) {
for (const request of requests.slice(i, i+chunkSize)) {
$.ajax(request.request).fail(function(jqXHR, textStatus, errorThrown) {
errors.push(`${request.error}${jqXHR.status} - ${errorThrown}\n`);
console.log(request.error);
}).always(requestSent);
}
showProgress(i * 100 / requests.length);
if (i + chunkSize < requests.length) {
setTimeout(sendChunk, 1000, i + chunkSize);
}
}
// when each request finishes...
function requestSent() {
completed++;
if (completed >= requests.length) {
// all finished
showProgress(100);
$("#deleteComments").show();
if (errors.length > 0) {
popUp(`Deletion complete. WARNING: ${errors.length} comments failed to delete.`);
} else {
popUp("All comments deleted successfully!");
}
}
}
// actually starts the recursion
sendChunk(0);
} else {
// confirmation was dismissed
$("#deleteComments").show();
}
});
}
});
});
});
Step 3. Delete Comments on Canvas
Go to the Canvas gradebook page and you’ll see a file uploader button for deleting comments. Click on it and upload the CSV file you prepared in Step 1. Then all the comments for the specified students and assignment will be deleted.
References
Canvas Userscripts by UCBoulder https://github.com/UCBoulder/canvas-userscripts
Canvas API documentation https://canvas.instructure.com/doc/api/index.html