-
Notifications
You must be signed in to change notification settings - Fork 0
/
uploader.js
148 lines (122 loc) · 4.22 KB
/
uploader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Upload a CSV of Grades to Canvas
// CSV: requires a student ID and a grade
var Canvas = require('node-canvas-lms');
var Papa = require('papaparse');
/**
* parseCSV -- returns a mapping of SID:Grade
* TODO: Would student names be helpful for debugging?
* Probably, but makes data passing messy...
*/
function parseCSV(opts, str) {
var csvObj,
result = {
names: {},
scores: {}
},
SID, SCORE, NAME;
// Prevent newlines from turning to Canvas Errors
str = str.trim();
// Options: http://papaparse.com/docs#config
csvObj = Papa.parse(str, {
header: true,
skipEmptyLines: true
});
// Must exactly match the CSV first row!
// TODO: modify names to make optparse format
// TODO: then document
SCORE = 'Total Score' || opts.headers.gradeCol;
NAME = 'Name' || opts.headers.nameCol;
SID = 'SID' || opts.headers.sidCol;
// TODO:
// inspect csv.meta.truncated and csv.errors
csvObj.data.forEach(function (lineData) {
var sid, grade, name;
sid = lineData[SID];
grade = lineData[SCORE];
name = lineData[NAME];
result.names[`${opts['student_id_format']}:${sid}`] = name;
result.scores[`${opts['student_id_format']}:${sid}`] = grade;
});
return result;
}
module.exports = function postGrades (options, data, callback) {
var course, gradesData, url;
course = new Canvas(
options.url,
{ token: options.token }
);
gradesData = parseCSV(options, data);
url = uploadURL(options.course_id, options.assignment_id);
bulkGradeUpload(course, url, gradesData, callback);
};
/**
Return the URL for a bulk upload of grades to a canvas assignment.
*/
function uploadURL(course, assignment) {
return `courses/${course}/assignments/${assignment}/submissions/update_grades`;
}
/**
This posts multiple grades to a single assignment at once.
Grades should be of the form: { sid: grade }
Note, bCourses is whacky and updates grades in an async manner:
**/
function bulkGradeUpload(course, url, data, cb) {
var form = {};
for (sid in data.scores) {
form[`grade_data[${sid}][posted_grade]`] = data.scores[sid];
}
// This returns a canvas "progress" object
course.post(url, {}, form, function(error, resp, body) {
if (body && body.url) {
cb(`URL ${body.url}`);
}
if (error || !body || body.errors) {
cb('Uh, oh! An error occurred');
cb(error || '');
cb(body.errors || 'No error message...');
return;
}
cb('Course Updates Posted');
monitorProgress(course, body.id, cb);
});
};
// Progress Object Docs
// https://bcourses.berkeley.edu/doc/api/progress.html
// the state of the job one of 'queued', 'running', 'completed', 'failed'
function monitorProgress(course, id, callback) {
var timeoutID,
delay = 500,
prevCompletion = null,
prevState = null;
// Use a small delay to prevent killing any servers
// since the progress API leaves no choice other than polling.
setTimeout(function() {
course.get(`progress/${id}/`, {}, function(err, resp, body) {
if (err || !body || body.errors) {
callback('Error!');
return;
}
if (body.workflow_state == 'completed' || body.workflow_state === 'failed') {
// callback('Error! Upload Failed');
callback('Done!');
if (body.message) {
callback(`\t${body.message}`);
}
return;
}
if (body.completion !== prevCompletion || body.workflow_state !== prevState) {
if (body.completion) {
callback(`Progress ${body.completion}%`);
}
if (body.message) {
callback(`\t${body.message}`);
}
prevState = body.state;
prevCompletion = body.completion;
} else {
callback(`Canvas State: ${body.workflow_state}`);
}
monitorProgress(course, id, callback);
});
}, delay);
}