Course import tool: include completed courses by default

korina_figueroa
Community Participant
10
2537

In the 2019-04-20 release notes, one of the bug fixes was: “The Copy a Canvas Course option uses active term dates to display available courses in the drop-dow... Recently, it came to our attention in Emerson College’s Instructional Technology Group, that this bug fix had the side effect of removing past courses from this drop-down list unless the “Include completed courses” box is checked.

The list of courses in the "Select a course" dropdown for copying a Canvas course now changes when "Include completed courses" is checked.

Since we’d all gotten used to past courses appearing in the list whether or not this box was checked, the change caused us to assume that the drop-down was broken. Based on the comments by chriscas,  @rmurchshafer ,  @Chris_Hofer ‌, and  @millerjm ‌ in the release notes thread, we aren't the only ones who ignored this checkbox until now.

Almost all of the times our faculty use the course copy tool, it’s to copy from a past semester to the current one. To prevent confusion due to the new functionality, we decided to force the “Include completed courses” to be box checked by default.

Demonstration that choosing "Copy a Canvas Course" now results in the "Include completed courses" checkbox being checked by default.

Here’s the code I used to make this happen. I’m happy to help others get this working in their custom js files too!

Edited to add: Check the comments for more efficient and concise code for this. I'm leaving the original version here for the thought process breakdown.

I started by writing a helper function to do the actual work of checking the box:

 /*
* Check the "Include completed courses" box on course import screen.
* NOTE: If the checkbox ID changes in future versions of Canvas, this
* code will need to be adjusted as well.
*/


function checkCompletedCourses() {
var completedBox = document.getElementById("include_completed_courses");

if ((typeof completedBox !== 'undefined') && (completedBox !== null)) {
// Set the checkbox value
completedBox.checked = true;
// Trigger the change event as if the box was being clicked by the user
completedBox.click();
}
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Inside the document ready function in our custom js code file, I already had a variable for running code only on specific pages. I added an additional regular expression to check for the Import Content page in a course.

var currentCoursePath = window.location.pathname;
var importPattern = /(\/courses\/[0-9]+\/content_migrations)$/i;‍‍‍‍‍‍

Since the “Include completed courses” checkbox doesn’t exist until the “Copy a Canvas Course” option is selected, I set up a MutationObserver to monitor the div that this checkbox gets added to.

if (importPattern.test(currentCoursePath)) {
var importBoxObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
checkCompletedCourses();
});
});

importBoxObserver.observe(document.getElementById("converter"), {
childList: true
});
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

So far this is working for us and we’re hoping it’ll prevent extra pre-semester stress once faculty are back on campus for the Fall.

10 Comments
robotcars
Community Champion

 @korina_figueroa 

Nice work. Always a fan of a good mutation hack. I wanted to share a few things I've learned doing some of these.

  • I recommend wrapping independent code blocks in their own IIFE | MDN for scope in your Global JS. This prevents the contents of that IIFE from affecting other code, variables etc, within your own file as well as Canvas.
  • Since the IIFE is invoked immediately, you don't need to put this in document ready.
  • Moving up the window.location.pathname test prevents further execution when not matched, and not declaring those variables shortens the file a little bit for your users.
  • The regex for the test, doesn't need to case insensitive, and we can shorten it up a bit.
  • The mutations do not need to be iterated, they are already complete (in the DOM) by the time you reach the inside of the code block for new MutationObserver(). You can essentially do whatever truthy condition you want, like the value of the select, and then execute checkCompletedCourses(). It's faster too. I was doing this too until  @James ‌ showed me it wasn't necessary.

(function () {
  if (/^\/courses\/\d+\/content_migrations$/.test(window.location.pathname)) {

    const checkCompletedCourses = () => {
      let completedBox = document.getElementById('include_completed_courses');
      if ((typeof completedBox !== 'undefined') && (completedBox !== null)) {
        // Set the checkbox value
        completedBox.checked = true;
        // Trigger the change event as if the box was being clicked by the user
        completedBox.click();
      }
    };

    const importBoxObserver = new MutationObserver(function (mtx) {
      if (document.getElementById('chooseMigrationConverter').value == 'course_copy_importer') {
        checkCompletedCourses();
      }
    });

    importBoxObserver.observe(document.getElementById('converter'), {
      childList: true
    });
  }
})();‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
James
Community Champion

I'm also glad to see people using mutation observers.

I'm always a little wary of attaching them to items that aren't delivered with the HTML of the page as it may be a timing issue and the observer gets missed. That may be based upon my issues with more complex pages that were slow to load and it's okay. I'm just always wary of it and so I try to remember to do a check to make sure the item I'm watching exists before I try to observe it. Otherwise, you get an error and all code after that in the same source file doesn't execute. To confirm, I put two IIFEs back-to-back in the browser's console and put an undefined reference in the first one and the second one didn't execute.

I know I'm paranoid and like to check for something being defined, but I double checked the spec for getElementById and it returns null if there is no matching element so we shouldn't have to check for it being defined. Since null counts as false, I've seen a lot of people use the shortened: if(completedBox) { }. 

If you're not using the mtx in the observer function do you even want to pass it in? I think this was a carry-over from when we were iterating through each mutation.

robotcars
Community Champion

More good points. Although without seeing it, I'm not quite understanding your back-to-back IIFE?

I ran out of time and left my comment a bit early Friday. I was trying to figure out how to make it recheck the box if the user bounces around the other options. Oddly the code seems to indicate that it's still checked and set... but I see otherwise. First day of summer school, so little crazy today.

Here's where I was troubleshooting... peppering in a few of your comments.

314474_copy-box.gif

(function () {
  if (/^\/courses\/\d+\/content_migrations$/.test(window.location.pathname)) {
    const converter = document.getElementById('converter');
    if (!converter) return false;

    const importBoxObserver = new MutationObserver(() => {
      console.log(document.getElementById('chooseMigrationConverter').value)
      if (document.getElementById('chooseMigrationConverter').value == 'course_copy_importer') {
        console.log(document.getElementById('include_completed_courses').value)
        let completedBox = document.getElementById('include_completed_courses');
        if (completedBox) {
          console.log('set')
          completedBox.checked = true;
          completedBox.click();
        }
      }
    });

    importBoxObserver.observe(converter, {
      childList: true
    });
  }
})();‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
James
Community Champion

The back-to-back IIFE was an IIFE followed immediately by another IIFE.

I was checking whether errors in code aborted everything in the source file or just everything within that IIFE. If your custom JavaScript throws an error, then everything after that stops working. What I was testing was whether it would be limited to the source file or limited to the IIFE. It looks like (in Chrome at least) that it's anywhere within the file.

I pasted two IIFEs back-to-back in the console and the second one didn't execute when the first one failed. Using an IIFE isn't protection against checking for error situations. I don't know that anyone suggested it was, I just wanted to double check.

James
Community Champion

The value of the checkbox isn't useful, it's the value returned if the box is checked. However, logging the completedBox itself showed something interesting.

Line 1 is the first time I chose it and line 2 is after selecting something else and then reselecting it. Line 2 has an additional checked="checked" at the end.

<input type="checkbox" value="1" id="include_completed_courses" name="include_completed_courses">
<input type="checkbox" value="1" id="include_completed_courses" name="include_completed_courses" checked="checked">

I'm sitting in the dentist office waiting for my daughter and it's almost lunch time so I imagine I don't have long. I get the full list and don't have that search box unless as masquerade as someone.  Is it an issue of the checkbox not being checked (visibly) but the list still has the completed courses or is it not checked and the concluded courses aren't shown?

robotcars
Community Champion

I had noticed that too, and even added it manually in the element/dom. I guess I'm expecting completedBox.click(); to trigger that each time...

so we need to visually check the box for the user...

I've upset the flow of things by not having hourly differentials for summer school. Users have  expectations!

robotcars
Community Champion

Checking the box if it's not checked seems to satisfy both requirements, application and user know what's desired/happening.

if (document.getElementById('chooseMigrationConverter').value == 'course_copy_importer') {
  let completedBox = document.getElementById('include_completed_courses');
  if (!completedBox.checked) {
    completedBox.checked = true;
    completedBox.click();
  }
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Would have to test if we need to clear checked, when not selected, but that's should only matter if Canvas botched it's handling of params for the feature.

korina_figueroa
Community Participant
Author

Thanks for the tips (and the interesting thread)! This was the first time I've encountered Mutation Observers, so I definitely appreciate the feedback!

I had noticed some inconsistencies with the box not appearing checked when switching between the different import types, but didn't have a chance to dig this deeply into it. Seems like if the user chooses "Copy a Canvas Course", then a different option, then back to "Copy a Canvas Course", then the box still appears checked and completed courses are still included. If the user selects a different option first, then goes to "Copy a Canvas Course", then the box may appear checked or not (and the list may include completed courses or not) depending on which option is selected first and whether the page is refreshed between selecting different options.

Seems related to the fact that most options have a div as the first child of the converter div and everything else is a child of that div. Blackboard Vista/CE(etc) has a form as converter's child, so switching to that one then to "Copy a Canvas Course" does result in the box being checked correctly. Does the childList option for the observer only check for the direct children of the specified element? Would using the subtree option instead / in addition to be better since the checkbox is a few levels down the tree?

James
Community Champion

The childList is direct children unless you also use subtree.

I made the mistake of adding subtree earlier today while testing and had to go to the task manager to end Chrome as it got stuck in an endless loop. This was before Robert added checking to see if it was already checked before forcing the .click()

korina_figueroa
Community Participant
Author

I circled back around to this after the June 22nd release broke this and some other customizations we were using. Based on  @James ‌' comment in a recent thread on the Javascript loading issues, the code below is what I'm using now to check the box. Seems to be working, but happy to hear if there are more improvements to make!

if (/^\/courses\/\d+\/content_migrations$/.test(window.location.pathname)) {
// Only define & execute the below function if user is on the "Import Content" screen

function checkImportBox(mutations, observer) {
   //console.log('Begin checkImportBox');
   const converter = document.getElementById('converter');
   //console.log(converter);

   if (!converter && typeof observer === 'undefined') {
     const obs = new MutationObserver(checkImportBox);
     obs.observe(document.body, {
       'childList' : true
     });
   }

   if (converter) {
     if (typeof observer !== 'undefined') {
       observer.disconnect();
     }
     const importBoxObserver = new MutationObserver(() => {
       //console.log(document.getElementById('chooseMigrationConverter').value)
       if (document.getElementById('chooseMigrationConverter').value == 'course_copy_importer') {
         //console.log(document.getElementById('include_completed_courses').value)
         let completedBox = document.getElementById('include_completed_courses');
         if (!completedBox.checked) {
           completedBox.checked = true;
           completedBox.click();
         }
       }
     });

     importBoxObserver.observe(converter, {
       childList: true
     });
   }
} // END - checkImportBox definition

checkImportBox();
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍