Kenneth Larsen

JavaScript to Quickly Import a Template Course

Blog Post created by Kenneth Larsen on Mar 18, 2020

Like many of you out there, here at Utah State University, we are struggling to move all of our classes online in an absurdly short period of time in response to COVID-19.

 

To help speed up the process, I have created some JavaScript to facilitate rapidly pulling a template course into an empty course shell and I am sharing it in the hopes that it can alleviate some of the headaches for other institutions.

 

I am providing the code for two contexts, uploading JavaScript to Canvas using the Canvas Theme Editor and creating a browser bookmarklet for those who do not have access to account-level JavaScript in Canvas.


A Note About Permissions

The code that follows will run as the logged-in user and will only work if the user has permission to import course content in Canvas.

The code is also scoped to only show on the front-page of an empty course so it should not be visible to students.

 

Account Level JavaScript

Canvas allows for adding CSS and JavaScript at an account level using the Canvas Theme Editor. For institutions that want to provide this type of functionality for an entire account, here are two options:


Single Template Course

If you have a single template course that you would like to provide an easy way to copy, the following code will create an Insert Base Template button in the right Canvas sidebar:

Insert Base Template Button

Here is the code that you would upload to Canvas. Update the templateCourse id on line 36 to your template course:

// Import Base Template button for the course Home Page
// This function will keep checking the progress url until process is complete or fails
function checkImportProgress(progressUrl) {
     $.get(progressUrl, function(data) {
          // Update the button to show we are checking again
          $('.kl-import-template').html('Checking Progress');
          // Four possible options
          // 'queued', 'running', 'completed', 'failed'
          let completed = false;
          switch(data.workflow_state) {
            case 'completed':
                 completed = true;
              break;
            case 'failed':
                 alert('Import failed');
                 break;
            default:
                 // For 'queued' or 'running'
              setTimeout(function(){ checkImportProgress(progressUrl); }, 5000);
          }
          if (completed) {
               // Reload the course home page
               location.reload();
          } else {
               // Wait a bit and try again
              setTimeout(function(){ checkImportProgress(progressUrl); }, 5000);
          }
          // Provide feedback of the current progress
          setTimeout(function(){
               $('.kl-import-template').html('Template Progress: ' + data.workflow_state);
          }, 1000);
     });
}
$(document).ready(function() {
     // The Canvas course id for your template
     let templateCourse = '593';
     // Only add the button to the home page of courses without any other content in place
     if (ENV.COURSE !== undefined && ENV.COURSE.id !== null && window.location.pathname === '/courses/' + ENV.COURSE.id && $('.ic-EmptyStateList:visible').length > 0) {
          // Add the import button
          let importButton = '<button class="btn btn-primary button-sidebar-wide kl-import-template"><i class="icon-download"></i> Import Base Template</button>';
          $('.kl-import-template').remove();
          $('.course-options').prepend(importButton);
          // Bind action
          $('.kl-import-template').unbind('click').on('click', function () {
               // Prompt user so they have a chance to cancel
               let confirmMessage = confirm("This will copy content into your course. Click OK to proceed.");
               if (confirmMessage == true) {
                    $(this).html('Importing Base Template');
                    // Send import request to the Canvas API
                    $.post('/api/v1/courses/' + ENV.COURSE.id + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': templateCourse}, function(data, textStatus, xhr) {
                         // Begin checking the progress url to see when import is complete
                         checkImportProgress(data.progress_url);
                    });
               }
          });
     }
});

 

Multiple Template Courses

This next variation is for institutions with multiple templates. Instead of a single button, this will add a list of courses with the option to preview and a brief description:

Template list

Here is the code that you would upload to Canvas. Update the templateList that begins on line 4 to include a name, id, and description for each of your template courses:

// Create a list of templates on the home page of a blank course 
$(document).ready(function() {
     // This is the list of your templates (Duplicate as needed)
     let templateList = [
          {
               name: 'Basic Online Template',
               id: '593',
               description: 'Basic 8 week course with readings, assignments, and quizzes'
          },
          {
               name: 'Another Template',
               id: '1234',
               description: 'This is a good course template'
          }
     ];
     // Function to check the Canvas progress url to see if the course has finished importing
     function checkImportProgress(progressUrl, courseID) {
          $.get(progressUrl, function(data) {
               // Give a visual cue that we are going to check again
               $('#kl-import-progress').html('Checking Progress');
               // 'queued', 'running', 'completed', 'failed'
               let completed = false;
               switch(data.workflow_state) {
                 case 'completed':
                      completed = true;
                      $('#kl-import-progress').attr('class', 'alert alert-success');
                   break;
                 case 'failed':
                      $('#kl-import-progress').attr('class', 'alert alert-error').html('Import failed');
                      break;
                 default:
                      // for 'queued' or 'running'
                   setTimeout(function(){ checkImportProgress(progressUrl, courseID); }, 5000);
               }
               if (completed) {
                    // Change from institution visibility to course
                    // If you set the template visibility to 'institution', users can preview before they copy
                    let parms = {
                      'course[is_public_to_auth_users]' : false,
                      'course[is_public]' : false
                    };
                    $.ajax({
                      'url' : '/api/v1/courses/' + courseID,
                      'type' : 'PUT',
                      'data' : parms
                    });
                    // Import is complete, reload the page
                    location.reload();
               } else {
                    // Import isn't finished, wait a bit and try again
                   setTimeout(function(){ checkImportProgress(progressUrl, courseID); }, 5000);
               }
               setTimeout(function(){
                    $('#kl-import-progress').html('Template Progress: ' + data.workflow_state);
               }, 1000);
          });
     }
     // Only add the button to the home page of courses without any other content in place (shows import option)
     if (ENV.COURSE !== undefined && ENV.COURSE.id !== null && window.location.pathname === '/courses/' + ENV.COURSE.id && $('.ic-EmptyStateList:visible').length > 0) {
          // Add a placeholder to the sidebar
          $('#kl-template-list').remove();
          $('.course-options').prepend('<div id="kl-template-list"></div><div id="kl-import-progress"></div>');
          // Add each template to the list
          $.each(templateList, function(index, val) {
               let itemInfo = `<p>
                         <a href="/courses/${val.id}" target="_blank" data-tooltip="left" title="${val.description}" class="Button Button--secondary kl-preview-template" style="padding: 2px 6px;"><i class="icon-eye"></i><span class="screenreader-only">View ${val.name}</span></a>
                         <button class="Button Button--secondary kl-import-template" data-tooltip="top" title="Import ${val.name}" data-courseid="${val.id}" style="padding: 2px 6px;"><i class="icon-download"></i><span class="screenreader-only">Import ${val.name}</span></button>
                         <span class="kl-template-name-${val.id}">${val.name}</span>
                    </p>`;
                $('#kl-template-list').append(itemInfo);
          });
          // When template import is clicked
          $('.kl-import-template').unbind('click').on('click', function () {
               // Give user a chance to cancel
               let confirmMessage = confirm("This will copy content into your course. Click OK to proceed.");
               if (confirmMessage == true) {
                    let templateCourse = $(this).attr('data-courseid');
                    let templateName = $(`.kl-template-name-${templateCourse}`).text();
                    $('#kl-import-progress').addClass('alert alert-info').html(`Importing ${templateName}`);
                    // Send import request to the Canvas API
                    $.post('/api/v1/courses/' + ENV.COURSE.id + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': templateCourse}, function(data, textStatus, xhr) {
                         // Begin checking the progress url to see when import is complete
                         checkImportProgress(data.progress_url, ENV.COURSE.id);
                    });
               }
          });
     }
});

 

JavaScript Bookmarklets (No access to add JavaScript to Canvas)

Next, let's take a look at some options for those who do not have access to add JavaScript to Canvas using the Theme Editor at an account level.


What is a Bookmarklet?

A bookmarklet is similar to create a bookmark in your browser to take you to a webpage. The difference is that instead of opening a webpage, a bookmarklet will run some JavaScript.


How do I create a Bookmarklet?

  1. Create a bookmark in your browser (the same way you would create any bookmark).
  2. Edit the bookmark.
  3. Give it a name to make it easy for you to find.
  4. In the URL field, you are going to add some JavaScript (keep reading to learn what this will look like).


Hard-Coded Template Course

If you would like to add a bookmarklet that will always import the same course, this is the JavaScript code we will use (replace the template_course_id on line 2 with your template course):

// The id of our template course
let template_course_id = '593';
// Only run on the home page of courses without any other content in place (shows import option)
if (ENV.COURSE !== undefined && ENV.COURSE.id !== null && window.location.pathname === '/courses/' + ENV.COURSE.id && $('.ic-EmptyStateList:visible').length > 0) {
     // Give user a chance to cancel
     let confirmMessage = confirm("This will copy content into your course. Click OK to proceed.");
     if (confirmMessage == true) {
          // Add a div for feedback
          $('#modules_homepage_user_create').prepend('<div id="kl-import-progress" class="alert alert-info"></div>');
          // Send request to Canvas
          $.post('/api/v1/courses/' + ENV.COURSE.id + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': template_course_id}, function(data, textStatus, xhr) {
               // Write a response with a link to the course migration page
               $('#kl-import-progress').html('Request submitted. Course copy will take a few minutes. Reload this page periodically or <a href="/courses/'+ENV.COURSE.id+'/content_migrations">view import progress in Canvas</a>');
          });
     }
} else {
     // Will only work on the course front page
     alert('Run this from the course home page of an empty course');
}

In order for this to work as a bookmark, we have to convert it. I like to use MrColes Bookmarklet Creator to convert the code above into what we will add to a bookmark. After converting the code, it will look more like this:

javascript:(function()%7B%2F%2F%20The%20id%20of%20our%20template%20courselet%20template_course_id%20%3D%20'#####'%3B%2F%2F%20Only%20run%20on%20the%20home%20page%20of%20courses%20without%20any%20other%20content%20in%20place%20(shows%20import%20option)if%20(ENV.COURSE%20!%3D%3D%20undefined%20%26%26%20ENV.COURSE.id%20!%3D%3D%20null%20%26%26%20window.location.pathname%20%3D%3D%3D%20'%2Fcourses%2F'%20%2B%20ENV.COURSE.id%20%26%26%20%24('.ic-EmptyStateList%3Avisible').length%20%3E%200)%20%7B%2F%2F%20Give%20user%20a%20chance%20to%20cancellet%20confirmMessage%20%3D%20confirm(%22This%20will%20copy%20content%20into%20your%20course.%20Click%20OK%20to%20proceed.%22)%3Bif%20(confirmMessage%20%3D%3D%20true)%20%7B%2F%2F%20Add%20a%20div%20for%20feedback%24('%23modules_homepage_user_create').prepend('%3Cdiv%20id%3D%22kl-import-progress%22%20class%3D%22alert%20alert-info%22%3E%3C%2Fdiv%3E')%3B%2F%2F%20Send%20request%20to%20Canvas%24.post('%2Fapi%2Fv1%2Fcourses%2F'%20%2B%20ENV.COURSE.id%20%2B%20'%2Fcontent_migrations'%2C%20%7B'migration_type'%3A%20'course_copy_importer'%2C%20'settings%5Bsource_course_id%5D'%3A%20template_course_id%7D%2C%20function(data%2C%20textStatus%2C%20xhr)%20%7B%2F%2F%20Write%20a%20response%20with%20a%20link%20to%20the%20course%20migration%20page%24('%23kl-import-progress').html('Request%20submitted.%20Course%20copy%20will%20take%20a%20few%20minutes.%20Reload%20this%20page%20periodically%20or%20%3Ca%20href%3D%22%2Fcourses%2F'%2BENV.COURSE.id%2B'%2Fcontent_migrations%22%3Eview%20import%20progress%20in%20Canvas%3C%2Fa%3E')%3B%7D)%3B%7D%7D%20else%20%7B%2F%2F%20Will%20only%20work%20on%20the%20course%20front%20pagealert('Run%20this%20from%20the%20course%20home%20page%20of%20an%20empty%20course')%3B%7D%7D)()

 It is a lot harder to read but it will do the job. To make things easier for you to update, find the ##### string and replace it with your template course id. Once you update the course id, this is the code that you will paste in as the URL in your bookmark.


Prompt for Course ID

If you want a little more flexibility for what course you want to copy, this option will ask the user to provide a course ID.

Course ID Prompt

Here is the JavaScript we will use for this one (you don't have to update this one):

// Only run on the home page of courses without any other content in place (shows import option)
if (ENV.COURSE !== undefined && ENV.COURSE.id !== null && window.location.pathname === '/courses/' + ENV.COURSE.id && $('.ic-EmptyStateList:visible').length > 0) {
     // Prompt user for Canvas course ID
     let course = prompt("Please enter the Canvas course ID from which to pull content");
     // If they give a response, use it
     if (course != null) {
          // Placeholder for feedback
          $('#modules_homepage_user_create').prepend('<div id="kl-import-progress" class="alert alert-info"></div>');
          // Remove any spaces
          course = course.trim();
          // Send request to Canvas
          $.post('/api/v1/courses/' + ENV.COURSE.id + '/content_migrations', {'migration_type': 'course_copy_importer', 'settings[source_course_id]': course}, function(data, textStatus, xhr) {
               // Write a response with a link to the course migration page
               $('#kl-import-progress').html('Request submitted. Course copy will take a few minutes. Reload this page periodically or <a href="/courses/'+ENV.COURSE.id+'/content_migrations">view import progress in Canvas</a>');
          });
     }
} else {
     // Will only work on the course front page
     alert('Run this from the course home page of an empty course');
}

And here is that code converted to use for a bookmarklet:

javascript:(function()%7B%2F%2F%20Only%20run%20on%20the%20home%20page%20of%20courses%20without%20any%20other%20content%20in%20place%20(shows%20import%20option)if%20(ENV.COURSE%20!%3D%3D%20undefined%20%26%26%20ENV.COURSE.id%20!%3D%3D%20null%20%26%26%20window.location.pathname%20%3D%3D%3D%20'%2Fcourses%2F'%20%2B%20ENV.COURSE.id%20%26%26%20%24('.ic-EmptyStateList%3Avisible').length%20%3E%200)%20%7B%2F%2F%20Prompt%20user%20for%20Canvas%20course%20IDlet%20course%20%3D%20prompt(%22Please%20enter%20the%20Canvas%20course%20ID%20from%20which%20to%20pull%20content%22)%3B%2F%2F%20If%20they%20give%20a%20response%2C%20use%20itif%20(course%20!%3D%20null)%20%7B%2F%2F%20Placeholder%20for%20feedback%24('%23modules_homepage_user_create').prepend('%3Cdiv%20id%3D%22kl-import-progress%22%20class%3D%22alert%20alert-info%22%3E%3C%2Fdiv%3E')%3B%2F%2F%20Remove%20any%20spacescourse%20%3D%20course.trim()%3B%2F%2F%20Send%20request%20to%20Canvas%24.post('%2Fapi%2Fv1%2Fcourses%2F'%20%2B%20ENV.COURSE.id%20%2B%20'%2Fcontent_migrations'%2C%20%7B'migration_type'%3A%20'course_copy_importer'%2C%20'settings%5Bsource_course_id%5D'%3A%20course%7D%2C%20function(data%2C%20textStatus%2C%20xhr)%20%7B%2F%2F%20Write%20a%20response%20with%20a%20link%20to%20the%20course%20migration%20page%24('%23kl-import-progress').html('Request%20submitted.%20Course%20copy%20will%20take%20a%20few%20minutes.%20Reload%20this%20page%20periodically%20or%20%3Ca%20href%3D%22%2Fcourses%2F'%2BENV.COURSE.id%2B'%2Fcontent_migrations%22%3Eview%20import%20progress%20in%20Canvas%3C%2Fa%3E')%3B%7D)%3B%7D%7D%20else%20%7B%2F%2F%20Will%20only%20work%20on%20the%20course%20front%20pagealert('Run%20this%20from%20the%20course%20home%20page%20of%20an%20empty%20course')%3B%7D%7D)()


Wrap Up

Anyway, I hope that this will be useful to some of you during the present chaos and any future chaos. Feel free to modify, adapt, change, or share that code.

Outcomes