Created
March 18, 2020 08:21
-
-
Save acbart/2e6a41f94fc84d52d9fda98e7c4708e4 to your computer and use it in GitHub Desktop.
Canvas Submission Transfer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Get Canvas information | |
| var user = ENV.current_user; | |
| var roles = ENV.current_user_roles; | |
| var course = ENV.context_asset_string; | |
| var courseId = course.split("_")[1]; | |
| var base = ENV.DEEP_LINKING_POST_MESSAGE_ORIGIN; | |
| // user.display_name: 'Austin Bart' | |
| // user.avatar_image_url: 'https://...' | |
| // user.id: 123213s | |
| // roles: ['user', 'teacher'] | |
| // course: "course_1442292" | |
| // courseId: "1442292" | |
| // Get this course | |
| // Get all other courses | |
| // Construct a mapping of assignments | |
| // <x> => <y> | |
| // Get the list of students common in both courses | |
| // Transfer submissions | |
| // https://stackoverflow.com/questions/8735792/how-to-parse-link-header-from-github-api | |
| var linkParser = (linkHeader) => { | |
| let re = /,[\s]*<(.*?)>;[\s]*rel="next"/g; | |
| let result = re.exec(linkHeader); | |
| if (result == null) { | |
| return null; | |
| } | |
| return result[1]; | |
| } | |
| function getAll(verb, url, options, callback, statusUpdate) { | |
| var everything = []; | |
| function get_next(data, status, xhr) { | |
| everything.push(...data); | |
| let links = xhr.getResponseHeader('link'); | |
| let next = linkParser(links); | |
| if (next != null) { | |
| verb(next, options, get_next); | |
| if (statusUpdate !== undefined) { | |
| statusUpdate(everything); | |
| } | |
| } else { | |
| callback(everything); | |
| } | |
| } | |
| return verb(url, options, get_next) | |
| } | |
| function prettifyCourse(course) { | |
| return course.course_code + " - " + course.name; | |
| } | |
| var baseApiUrl = base+"/api/v1/courses/" | |
| function loadPreTransferInfo(source, target) { | |
| $("#wi-report").html("Beginning initial downloads."); | |
| // Handle assignment loading: | |
| let errors = []; | |
| getAll($.get, baseApiUrl+source+"/assignments", {'per_page': 100}, function (sourceAssignments) { | |
| getAll($.get, baseApiUrl+target+"/assignments", {'per_page': 100}, function (targetAssignments) { | |
| let targetAssignmentMap = targetAssignments.reduce( (map, a) => { | |
| if (a.name in map) { | |
| console.error("ISSUE: Duplicate name detected within target course."); | |
| errors.push("Duplicate assignment name in target course: "+a.name); | |
| } | |
| map[a.name] = a; | |
| return map; | |
| }, {}); | |
| let assignmentIds = {}; | |
| let assignmentList = sourceAssignments.filter(a=>a.published).map(assignment => { | |
| let targetAssignment = targetAssignmentMap[assignment.name]; | |
| if (targetAssignment === undefined) { | |
| errors.push(`Assignment from source course not present in target course: <a href='${assignment.html_url}' target=_blank>${assignment.name}</a>`); | |
| return null; | |
| } else if (!targetAssignment.published) { | |
| errors.push(`Published assignment from source course is not published in target course: <a href='${targetAssignment.html_url}' target=_blank>${targetAssignment.name}</a>`); | |
| return null; | |
| } else { | |
| assignmentIds[assignment.id] = {source: assignment, target: targetAssignment}; | |
| return `<li><a target=_blank href='${assignment.html_url}'>${assignment.name}</a></li>`; | |
| } | |
| }).filter(a => a !== null).join("\n"); | |
| // Handle student loading: | |
| getAll($.get, baseApiUrl+source+"/users", {'enrollment_type[]': 'student', 'per_page': 100}, | |
| function (sourceStudents) { | |
| getAll($.get, baseApiUrl+target+"/users", {'enrollment_type[]': 'student', 'per_page': 100}, | |
| function(targetStudents) { | |
| let targetStudentMap = targetStudents.reduce( (map, s) => { | |
| map[s.id] = s; | |
| return map; | |
| }, {}) | |
| let studentIds = []; | |
| let studentList = sourceStudents.map(student => { | |
| let targetStudent = targetStudentMap[student.id]; | |
| if (targetStudent !== undefined) { | |
| studentIds.push(student); | |
| return `<li>${student.name}</li>`; | |
| } else { | |
| errors.push(`Student from source course not present in target course: ${student.sortable_name} (Email: ${student.email}, SIS: ${student.sis_user_id})`); | |
| return null; | |
| } | |
| }).filter(s => s !== null).join("\n"); | |
| finishPreTransferReport(source, target, assignmentIds, assignmentList, studentIds, studentList, errors) | |
| }, | |
| function(everythingSoFar) { | |
| $("#wi-report").html(`Loading target students, downloaded ${everythingSoFar.length} so far, please wait.`); | |
| }); | |
| }, function(everythingSoFar) { | |
| $("#wi-report").html(`Loading source students, downloaded ${everythingSoFar.length} so far, please wait.`); | |
| }); | |
| }, function(everythingSoFar) { | |
| $("#wi-report").html(`Loading target assignments, downloaded ${everythingSoFar.length} so far, please wait.`); | |
| }); | |
| }, function(everythingSoFar) { | |
| $("#wi-report").html(`Loading source assignments, downloaded ${everythingSoFar.length} so far, please wait.`); | |
| }); | |
| } | |
| function finishPreTransferReport(source, target, assignmentIds, assignmentList, studentIds, studentList, errors) { | |
| // Handle error reporting: | |
| let errorReport = ""; | |
| if (errors.length > 0) { | |
| errorReport = "<span>Errors exist:</span>"+errors.map(e => `<li>${e}</li>`).join("\n"); | |
| } | |
| // Make "Start" button: | |
| let startButton = $("<a href='#' class='btn btn-success' role='button'>Begin this transfer?</a>"); | |
| startButton.click(function() { | |
| beginSubmissionTransfer(source, target, assignmentIds, studentIds); | |
| }); | |
| // Show report: | |
| $("#wi-report").html( | |
| `<span>Transferable assignments (${Object.keys(assignmentIds).length}):</span> | |
| <ul>${assignmentList}</ul> | |
| <span>Transferable students (${studentIds.length}):</span> | |
| <ul>${studentList}</ul> | |
| ${errorReport}<br>`).append(startButton); | |
| } | |
| function beginSubmissionTransfer(source, target, assignments, students) { | |
| $("#tr-status").html("Beginning transfer. Make take a while!"); | |
| // Download all the old stuff | |
| getAll($.get, baseApiUrl+source+"/students/submissions", { | |
| 'student_ids[]': students.map(s=>s.id), | |
| 'assignment_ids[]': Object.keys(assignments), | |
| 'include[]': ['assignment', 'user'], | |
| 'per_page': 100 | |
| }, function (sourceSubmissions) { | |
| $("#tr-status").html(`Uploading target submissions. There are ${sourceSubmissions.length} to go.`); | |
| let completed = 0; | |
| let allTransfers = []; | |
| sourceSubmissions.map(submission => { | |
| if (submission.workflow_state !== "unsubmitted") { | |
| let transfers = transferSubmission(target, assignments[submission.assignment.id].target.id, submission); | |
| allTransfers.push.apply(allTransfers, transfers); | |
| } | |
| completed += 1; | |
| $("#tr-status").html(`Uploading target submissions. Started ${completed}/${sourceSubmissions.length} so far.`); | |
| }); | |
| Promise.all(allTransfers).then(() => { | |
| $("#tr-status").html(`Uploaded all target submissions!`); | |
| }); | |
| console.log(sourceSubmissions); | |
| }, function(everythingSoFar) { | |
| $("#tr-status").html(`Loading source submissions, downloaded ${everythingSoFar.length} so far, please wait. Make take a while!`); | |
| }); | |
| // Upload as new stuff | |
| } | |
| function transferSubmission(target, targetAssignment, submission) { | |
| // TODO: Handle file upload | |
| let files = []; | |
| if (submission.attachments) { | |
| files = submission.attachments.map(f => f.id); | |
| } | |
| // TODO: Handle grading rubric | |
| // Upload submission | |
| let dataSubmit = $.post(baseApiUrl+target+"/assignments/"+targetAssignment+"/submissions", | |
| { | |
| 'submission[submission_type]': submission.submission_type, | |
| 'submission[body]': submission.body, | |
| 'submission[url]': submission.url, | |
| 'submission[user_id]': submission.user_id, | |
| 'submission[submitted_at]': submission.submitted_at, | |
| 'submission[file_ids][]': files | |
| }, d => { | |
| $("#tr-report").append(`<li>Submitted ${submission.assignment.name} for ${submission.user.sortable_name}</li>`); | |
| }); | |
| // Upload grade | |
| if (submission.workflow_state === "graded") { | |
| let dataGrade = $.ajax({ | |
| type: "PUT", | |
| url: baseApiUrl+target+"/assignments/"+targetAssignment+"/submissions/"+submission.user_id, | |
| data: { | |
| 'submission[posted_grade]': submission.grade, | |
| 'submission[excuse]': submission.excused, | |
| }, | |
| success: d => { | |
| $("#tr-report").append(`<li>Graded ${submission.assignment.name} for ${submission.user.sortable_name}</li>`); | |
| } | |
| }); | |
| return [dataSubmit, dataGrade]; | |
| } else { | |
| return [dataSubmit]; | |
| } | |
| } | |
| function makeCourseSelector(courses) { | |
| let selector = $('<select id="wi-course-selector" name="wi-course-selector">'); | |
| selector.append("<option></option>"); | |
| var seenIds = []; | |
| $.each(courses, function(i, course) { | |
| if (!seenIds.includes(course.id)) { | |
| selector.append( | |
| $('<option></option>').val(course.id) | |
| .html(prettifyCourse(course)) | |
| ); | |
| seenIds.push(course.id); | |
| } | |
| }); | |
| selector.change(function(data, value) { | |
| console.log(data, selector.val()); | |
| loadPreTransferInfo(selector.val(), courseId); | |
| //loadSubmissions(selector.val()); | |
| }) | |
| return selector; | |
| } | |
| function getCourses() { | |
| getAll($.get, base+"/api/v1/courses/", {'per_page': 100}, function (data) { | |
| loadDialog(data); | |
| }); | |
| } | |
| function startDialog(title) { | |
| $("#dialog").dialog({ | |
| autoOpen: false, | |
| show: "blind", | |
| hide: "explode", | |
| width: '80%', | |
| height: document.documentElement.clientHeight-100 | |
| }); | |
| $( "#dialog" ).dialog("open"); | |
| if ($('#dialog').length == 0) { | |
| $(document.body).append('<div title="'+title+ | |
| '" id="dialog"></div>'); | |
| } | |
| $('#dialog').html("<span>Loading courses, please wait.</span>"); | |
| } | |
| function loadDialog(data) { | |
| let thisCourse = data.filter(course => course.id == courseId)[0]; | |
| let message = $("<div></div>"); | |
| message.append("<label for='wi-course-selector'>Transfer submissions from:</label>"); | |
| message.append(makeCourseSelector(data)); | |
| message.append("<br>"); | |
| message.append("<span>To: </span>"); | |
| message.append("<span>"+prettifyCourse(thisCourse)+"</span>"); | |
| message.append($("<div id='wi-report'></div>")); | |
| message.append($("<div id='tr-status'></div>")); | |
| message.append($("<ol id='tr-report'></ol>")); | |
| $('#dialog').html(message); | |
| } | |
| startDialog("Transfer Submissions"); | |
| getCourses(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment