NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Batch Upload Sections // @namespace mw784 // @version 1 // @license MIT // @description Batch Upload Section shells direct in the settings area of a Canvas course site. // @author Matthew Williams // @include https://canvas.newcastle.edu.au/courses/*/settings // @include https://newcastle.test.instructure.com/courses/*/settings // ==/UserScript== (function () { 'use strict'; const uniqueLinkId = 'mw_section_upload'; addSectionButton('Import Course Sections', 'icon-upload'); function addSectionButton(linkText, iconType) { if (!document.getElementById(uniqueLinkId)) { const insBefore = document.querySelector('aside#right-side > div > .import_content'); if (insBefore) { const anchor = document.createElement('a'); anchor.id = uniqueLinkId; anchor.classList.add('Button', 'Button--link', 'Button--link--has-divider', 'Button--course-settings'); const icon = document.createElement('i'); icon.classList.add(iconType); anchor.appendChild(icon); anchor.appendChild(document.createTextNode(`${linkText} `)); anchor.addEventListener('click', openDialog); insBefore.parentNode.insertBefore(anchor, insBefore); } } return; } function createDialog() { var el = document.querySelector('#mw_sections_dialog'); if (!el) { el = document.createElement('div'); el.id = 'mw_sections_dialog'; el.classList.add('ic-Form-control'); var label = document.createElement('label'); label.htmlFor = 'mw_section_text'; label.textContent = 'Section Data'; label.classList.add('ic-Label'); el.appendChild(label); var textarea = document.createElement('textarea'); textarea.setAttribute('rows', '9'); textarea.id = 'mw_section_text'; textarea.classList.add('ic-Input'); textarea.placeholder = `Paste tab-delimited section data from Excel into this textbox, without headers.\n First column should be section name, second column should be section SIS ID (optional), third column should be Student ID (optional). For example: \nC1A \tC1A.NURS1234.2022.S1\tc1111111 C1A \tC1A.NURS1234.2022.S1\tc2222222 C1B \tC1B.NURS1234.2022.S1\tc3333333 O1A \tO1A.NURS1234.2022.S1\tc4444444 O1B \tO1B.NURS1234.2022.S1\tc5555555`; el.appendChild(textarea); var msg = document.createElement('div'); msg.id = 'mw_rubric_msg'; msg.classList.add('ic-flash-warning'); msg.style.display = 'none'; el.appendChild(msg); var parent = document.querySelector('body'); parent.appendChild(el); } } function openDialog() { try { createDialog(); $('#mw_sections_dialog').dialog({ 'title': 'Import Sections and Section Enrolments', 'autoOpen': false, 'buttons': [{ 'text': 'Cancel', 'click': closeDialog }, { 'text': 'Import', 'click': checkDialog, 'class': 'Button Button--primary' }], 'modal': true, 'height': 'auto', 'width': '80%' }); if (!$('#mw_sections_dialog').dialog('isOpen')) { $('#mw_sections_dialog').dialog('open'); } } catch (e) { console.log(e); } } function checkDialog() { var rawtext = document.getElementById('mw_section_text'); var courseIdFlag = checkCourseId(); if (!courseIdFlag) { alert('Unable to determine where to import sections.'); return; } if (rawtext.value && rawtext.value.trim() !== '' && courseIdFlag) { parseDialog(rawtext.value) } else { alert('You must paste your section data into the textbox.'); } } function checkCourseId() { const courseId = getCourseId(); if (!(Number(courseId))) { return false; } else { return true; } } function parseDialog(txt) { var linesOfText = txt.split('\n'); // remove possible newlines at start and end while (linesOfText.at(-1) === '' || linesOfText.at(0) === '') { if (linesOfText.at(-1) === '') { linesOfText.pop() } else if (linesOfText.at(-0) === '') { linesOfText.shift() } } // split each line up by \t // rows becomes an array of objects var rows = []; for (let i = 0; i < linesOfText.length; i++) { rows[i] = { sectionName: linesOfText[i].split('\t')[0], sectionSISId: linesOfText[i].split('\t')[1], studentId: linesOfText[i].split('\t')[2] } // abort if row contains a studentId but no sectionSISId if (!rows[i]['sectionSISId'] && rows[i]['studentId']) { alert(`Student cannot be imported to a section without a SIS ID. (check row ${i + 1})`); return; } // abort if row contains no sectionName or sectionSISId if (!rows[i]['sectionName']) { alert(`Row does not contain section name. (check row ${i + 1})`); return; } } //create arrays of unique sections for posting and unique students for posting const uniqueSections = removeDuplicateSections(rows); const uniqueStudents = removeDuplicateStudents(rows); // remove students from uniqueStudents who aren't already enrolled in Canvas - to avoid enrolling them inadvertently const uniqueStudentsInCourse = removeStudentsNotInCourse(uniqueStudents); processDialog(uniqueSections, uniqueStudentsInCourse); } function removeStudentsNotInCourse(uniqueStudents) { var settings = { "url": `${window.location.origin}/api/v1/courses/${getCourseId()}/enrollments`, "type": "GET", "timeout": 0, "async": false, "headers": { "X-CSRFToken": getCsrfToken() }, } var resp = $.parseJSON($.ajax(settings).responseText); const studentsInCourse = []; for (let i = 0; i < resp.length; i++) { studentsInCourse[i] = resp[i].user.sis_user_id } //compare uniqueStudents with studentsInCourse const uniqueStudentsInCourse = [] for (let i = uniqueStudents.length - 1; i >= 0; i--) { if (studentsInCourse.indexOf(uniqueStudents[i]['studentId']) !== -1) uniqueStudentsInCourse.push(uniqueStudents[i]) } return uniqueStudentsInCourse; } function processDialog(uniqueSections, uniqueStudentsInCourse) { for (let i = 0; i < uniqueSections.length; i++) { var settings = { "url": `${window.location.origin}/api/v1/courses/${courseId}/sections`, "type": "POST", "timeout": 0, "async": false, "data": { 'course_section': { 'name': uniqueSections[i]['sectionName'], 'sis_section_id': uniqueSections[i]['sectionSISId'], } }, "headers": { "X-CSRFToken": getCsrfToken() }, }; postSection(settings); } for (let i = 0; i < uniqueStudentsInCourse.length; i++) { var settings = { "url": `${window.location.origin}/api/v1/sections/sis_section_id:${uniqueStudentsInCourse[i]['sectionSISId']}/enrollments`, "type": "POST", "timeout": 0, "async": false, "data": { 'enrollment': { 'user_id': `sis_user_id:${uniqueStudentsInCourse[i]['studentId']}`, 'enrollment_state': 'active', 'notify': false, } }, "headers": { "X-CSRFToken": getCsrfToken() }, }; postStudent(settings); } $('#mw_sections_dialog').dialog('close'); window.location.reload(true); } function postStudent(settings){ $.ajax(settings).fail(function () { console.log(`Student "${settings['data']['enrollment']['user_id']}" couldn't be imported.`) }); } function postSection(settings) { $.ajax(settings).fail(function () { console.log(`Section "${settings['data']['course_section']['name']}" couldn't be imported. Most likely the SIS ID is already in use.`) }); } function removeDuplicateSections(input2dArray) { var flagArray = []; var outputArray = []; var j = -1; for (var i = 0, l = input2dArray.length; i < l; i++) { if (flagArray[input2dArray[i]['sectionSISId']] !== true) { flagArray[input2dArray[i]['sectionSISId']] = true; outputArray[++j] = { sectionName: input2dArray[i]['sectionName'], sectionSISId: input2dArray[i]['sectionSISId'] } } else if (!input2dArray[i]['sectionSISId'] || input2dArray[i]['sectionSISId'].length === 0) { outputArray[++j] = { sectionName: input2dArray[i]['sectionName'], sectionSISId: '' } } } return outputArray; } function removeDuplicateStudents(input2dArray) { var flagArray = []; var outputArray = []; var j = -1; for (var i = 0, l = input2dArray.length; i < l; i++) { if (flagArray[input2dArray[i]['studentId']] !== true) { flagArray[input2dArray[i]['studentId']] = true; outputArray[++j] = { sectionSISId: input2dArray[i]['sectionSISId'], studentId: input2dArray[i]['studentId'] } } } return outputArray; } function closeDialog() { $(this).dialog('close'); var el = document.getElementById('mw_section_text'); if (el) { el.value = ''; } } function getCsrfToken() { var csrfRegex = new RegExp('^_csrf_token=(.*)$'); var csrf; var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = cookies[i].trim(); var match = csrfRegex.exec(cookie); if (match) { csrf = decodeURIComponent(match[1]); break; } } return csrf; } function getCourseId() { let id = false; const courseRegex = new RegExp('^/courses/([0-9]+)'); const matches = courseRegex.exec(window.location.pathname); if (matches) { id = matches[1]; } return id; } })();