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

Example CSV File
  • 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.

  1. On Canvas gradebook page, press [Actions] - [Export Entire Gradebook]
  2. Delete all columns except for the ID column and assignment column that you wish to post comments for
  3. Delete any filler rows between the header row and the list of students (e.g., the “Points Possible” row)
  4. 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

Example CSV File
  • 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