Skip to content

Instantly share code, notes, and snippets.

@acbart
Created March 18, 2020 08:21
Show Gist options
  • Select an option

  • Save acbart/2e6a41f94fc84d52d9fda98e7c4708e4 to your computer and use it in GitHub Desktop.

Select an option

Save acbart/2e6a41f94fc84d52d9fda98e7c4708e4 to your computer and use it in GitHub Desktop.
Canvas Submission Transfer
// 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