Commit | Line | Data |
---|---|---|
e99c53fb BA |
1 | // TODO: YAML format for questions, parsed from text (nested questions) |
2 | // Then yaml parsed to json --> array of indexed questions | |
3 | // Use open mode for question banks: add setting "nbQuestions" to show nbQuestions | |
4 | // at random among active questions | |
5 | ||
6 | window.onload = function() { | |
7 | ||
8 | V = new Vue({ | |
9 | el: '#course', | |
10 | data: { | |
11 | display: "assessments", //or "students", or "grades" (admin mode) | |
12 | course: course, | |
13 | mode: "view", //or "edit" (some assessment) | |
14 | // assessment data: | |
15 | monitorPwd: "", | |
16 | newAssessment: { name: "" }, | |
17 | assessmentArray: assessmentArray, | |
18 | assessmentIndex: 0, //current edited assessment index | |
19 | assessment: { }, //copy of assessment at editing index in array | |
20 | assessmentText: "", //questions in an assessment, in text format | |
21 | // grades data: | |
22 | settings: { | |
23 | totalPoints: 20, | |
24 | halfPoints: false, | |
25 | zeroSum: false, | |
26 | }, | |
27 | group: 1, //for detailed grades tables | |
28 | grades: { }, //computed | |
29 | }, | |
30 | mounted: function() { | |
31 | $('.modal').each( (i,elem) => { | |
32 | if (elem.id != "assessmentEdit") | |
33 | $(elem).modal(); | |
34 | }); | |
35 | $('ul.tabs').tabs(); | |
36 | $('#assessmentEdit').modal({ | |
37 | complete: () => { | |
38 | this.parseAssessment(); | |
39 | Vue.nextTick( () => { | |
40 | $("#questionList").find("code[class^=language-]").each( (i,elem) => { | |
41 | Prism.highlightElement(elem); | |
42 | }); | |
43 | MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]); | |
44 | }); | |
45 | }, | |
46 | }); | |
47 | }, | |
48 | methods: { | |
49 | // GENERAL: | |
50 | toggleDisplay: function(area) { | |
51 | if (this.display == area) | |
52 | this.display = ""; | |
53 | else | |
54 | this.display = area; | |
55 | }, | |
56 | studentList: function(group) { | |
57 | return this.course.students | |
58 | .filter( s => { return group==0 || s.group == group; }) | |
59 | .map( s => { return Object.assign({}, s); }) //not altering initial array | |
b3540dbb | 60 | .sort( (a,b) => { return a.name.localeCompare(b.name); }) |
e99c53fb BA |
61 | }, |
62 | // STUDENTS: | |
63 | uploadTrigger: function() { | |
64 | $("#upload").click(); | |
65 | }, | |
66 | upload: function(e) { | |
67 | let file = (e.target.files || e.dataTransfer.files)[0]; | |
68 | Papa.parse(file, { | |
69 | header: true, | |
70 | skipEmptyLines: true, | |
71 | complete: (results,file) => { | |
72 | let students = [ ]; | |
73 | // Post-process: add group/number if missing | |
74 | let number = 1; | |
75 | results.data.forEach( d => { | |
76 | if (!d.group) | |
77 | d.group = 1; | |
78 | if (!d.number) | |
79 | d.number = number++; | |
80 | if (typeof d.number !== "string") | |
81 | d.number = d.number.toString(); | |
82 | students.push(d); | |
83 | }); | |
84 | $.ajax("/import/students", { | |
85 | method: "POST", | |
86 | data: { | |
87 | cid: this.course._id, | |
88 | students: JSON.stringify(students), | |
89 | }, | |
90 | dataType: "json", | |
91 | success: res => { | |
92 | if (!res.errmsg) | |
93 | this.course.students = students; | |
94 | else | |
95 | alert(res.errmsg); | |
96 | }, | |
97 | }); | |
98 | }, | |
99 | }); | |
100 | }, | |
101 | // ASSESSMENT: | |
102 | addAssessment: function() { | |
103 | if (!admin) | |
104 | return; | |
105 | // modal, fill code and description | |
106 | let error = Validator.checkObject(this.newAssessment, "Assessment"); | |
107 | if (!!error) | |
108 | return alert(error); | |
109 | else | |
110 | $('#newAssessment').modal('close'); | |
111 | $.ajax("/add/assessment", | |
112 | { | |
113 | method: "GET", | |
114 | data: { | |
115 | name: this.newAssessment.name, | |
116 | cid: course._id, | |
117 | }, | |
118 | dataType: "json", | |
119 | success: res => { | |
120 | if (!res.errmsg) | |
121 | { | |
122 | this.newAssessment["name"] = ""; | |
123 | this.assessmentArray.push(res); | |
124 | } | |
125 | else | |
126 | alert(res.errmsg); | |
127 | }, | |
128 | } | |
129 | ); | |
130 | }, | |
131 | materialOpenModal: function(id) { | |
132 | $("#" + id).modal("open"); | |
133 | Materialize.updateTextFields(); //textareas, time field... | |
134 | }, | |
135 | updateAssessment: function() { | |
136 | $.ajax("/update/assessment", { | |
137 | method: "POST", | |
138 | data: {assessment: JSON.stringify(this.assessment)}, | |
139 | dataType: "json", | |
140 | success: res => { | |
141 | if (!res.errmsg) | |
142 | { | |
143 | this.assessmentArray[this.assessmentIndex] = this.assessment; | |
144 | this.mode = "view"; | |
145 | } | |
146 | else | |
147 | alert(res.errmsg); | |
148 | }, | |
149 | }); | |
150 | }, | |
151 | deleteAssessment: function(assessment) { | |
152 | if (!admin) | |
153 | return; | |
154 | if (confirm("Delete assessment '" + assessment.name + "' ?")) | |
155 | { | |
156 | $.ajax("/remove/assessment", | |
157 | { | |
158 | method: "GET", | |
159 | data: { qid: this.assessment._id }, | |
160 | dataType: "json", | |
161 | success: res => { | |
162 | if (!res.errmsg) | |
163 | this.assessmentArray.splice( this.assessmentArray.findIndex( item => { | |
164 | return item._id == assessment._id; | |
165 | }), 1 ); | |
166 | else | |
167 | alert(res.errmsg); | |
168 | }, | |
169 | } | |
170 | ); | |
171 | } | |
172 | }, | |
173 | toggleState: function(questionIndex) { | |
174 | // add or remove from activeSet of current assessment | |
175 | let activeIndex = this.assessment.activeSet.findIndex( item => { return item == questionIndex; }); | |
176 | if (activeIndex >= 0) | |
177 | this.assessment.activeSet.splice(activeIndex, 1); | |
178 | else | |
179 | this.assessment.activeSet.push(questionIndex); | |
180 | }, | |
181 | setAssessmentText: function() { | |
182 | let txt = ""; | |
183 | this.assessment.questions.forEach( q => { | |
184 | txt += q.wording + "\n"; | |
185 | q.options.forEach( (o,i) => { | |
186 | let symbol = q.answer.includes(i) ? "+" : "-"; | |
187 | txt += symbol + " " + o + "\n"; | |
188 | }); | |
189 | txt += "\n"; //separate questions by new line | |
190 | }); | |
191 | this.assessmentText = txt; | |
192 | }, | |
193 | parseAssessment: function() { | |
194 | let questions = [ ]; | |
195 | let lines = this.assessmentText.split("\n").map( L => { return L.trim(); }) | |
196 | lines.push(""); //easier parsing | |
197 | let emptyQuestion = () => { | |
198 | return { | |
199 | wording: "", | |
200 | options: [ ], | |
201 | answer: [ ], | |
202 | active: true, //default | |
203 | }; | |
204 | }; | |
205 | let q = emptyQuestion(); | |
206 | lines.forEach( L => { | |
207 | if (L.length > 0) | |
208 | { | |
209 | if (['+','-'].includes(L.charAt(0))) | |
210 | { | |
211 | if (L.charAt(0) == '+') | |
212 | q.answer.push(q.options.length); | |
213 | q.options.push(L.slice(1).trim()); | |
214 | } | |
215 | else if (L.charAt(0) == '*') | |
216 | { | |
217 | // TODO: read current + next lines into q.answer (HTML, 1-elem array) | |
218 | } | |
219 | else | |
220 | q.wording += L + " "; //space required at line breaks, generally | |
221 | } | |
222 | else | |
223 | { | |
224 | // Flush current question (if any) | |
225 | if (q.wording.length > 0) | |
226 | { | |
227 | questions.push(q); | |
228 | q = emptyQuestion(); | |
229 | } | |
230 | } | |
231 | }); | |
232 | this.assessment.questions = questions; | |
233 | }, | |
234 | actionAssessment: function(index) { | |
235 | if (admin) | |
236 | { | |
237 | // Edit screen | |
238 | this.assessmentIndex = index; | |
239 | this.assessment = $.extend(true, {}, this.assessmentArray[index]); | |
240 | this.setAssessmentText(); | |
241 | this.mode = "edit"; | |
242 | Vue.nextTick( () => { | |
243 | $("#questionList").find("code[class^=language-]").each( (i,elem) => { | |
244 | Prism.highlightElement(elem); | |
245 | }); | |
246 | MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]); | |
247 | }); | |
248 | } | |
249 | else //external user: show assessment | |
250 | this.redirect(this.assessmentArray[index].name); | |
251 | }, | |
252 | redirect: function(assessmentName) { | |
253 | document.location.href = "/" + initials + "/" + course.code + "/" + assessmentName; | |
254 | }, | |
255 | setPassword: function() { | |
256 | let hashPwd = Sha1.Compute(this.monitorPwd); | |
257 | let error = Validator.checkObject({password:hashPwd}, "Course"); | |
258 | if (error.length > 0) | |
259 | return alert(error); | |
260 | $.ajax("/set/password", | |
261 | { | |
262 | method: "GET", | |
263 | data: { | |
264 | cid: this.course._id, | |
265 | pwd: hashPwd, | |
266 | }, | |
267 | dataType: "json", | |
268 | success: res => { | |
269 | if (!res.errmsg) | |
270 | alert("Password saved!"); | |
271 | else | |
272 | alert(res.errmsg); | |
273 | }, | |
274 | } | |
275 | ); | |
276 | }, | |
277 | // NOTE: artifact required for Vue v-model to behave well | |
278 | checkBoxFixedId: function(i) { | |
279 | return "questionFixed" + i; | |
280 | }, | |
281 | checkBoxActiveId: function(i) { | |
282 | return "questionActive" + i; | |
283 | }, | |
284 | // GRADES: | |
285 | gradeSettings: function() { | |
286 | $("#gradeSettings").modal("open"); | |
287 | Materialize.updateTextFields(); //total points field in grade settings overlap | |
288 | }, | |
289 | download: function() { | |
290 | // Download (all) grades as a CSV file | |
291 | let data = [ ]; | |
292 | this.studentList(0).forEach( s => { | |
293 | let finalGrade = 0.; | |
294 | let gradesCount = 0; | |
295 | if (!!this.grades[s.number]) | |
296 | { | |
297 | Object.keys(this.grades[s.number]).forEach( assessmentName => { | |
298 | s[assessmentName] = this.grades[s.number][assessmentName]; | |
299 | if (_.isNumeric(s[assessmentName]) && !isNaN(s[assessmentName])) | |
300 | { | |
301 | finalGrade += s[assessmentName]; | |
302 | gradesCount++; | |
303 | } | |
304 | if (gradesCount >= 1) | |
305 | finalGrade /= gradesCount; | |
306 | s["final"] = finalGrade; //TODO: forbid "final" as assessment name | |
307 | }); | |
308 | } | |
b3540dbb | 309 | data.push(s); //number,name,group,assessName1...assessNameN,final |
e99c53fb BA |
310 | }); |
311 | let csv = Papa.unparse(data, { | |
312 | quotes: true, | |
313 | header: true, | |
314 | }); | |
315 | let downloadAnchor = $("#download"); | |
316 | downloadAnchor.attr("download", this.course.code + "_results.csv"); | |
317 | downloadAnchor.attr("href", "data:text/plain;charset=utf-8," + encodeURIComponent(csv)); | |
318 | this.$refs.download.click() | |
319 | //downloadAnchor.click(); //fails | |
320 | }, | |
321 | showDetails: function(group) { | |
322 | this.group = group; | |
323 | $("#detailedGrades").modal("open"); | |
324 | }, | |
325 | groupList: function() { | |
326 | let maxGrp = 1; | |
327 | this.course.students.forEach( s => { | |
328 | if (s.group > maxGrp) | |
329 | maxGrp = s.group; | |
330 | }); | |
331 | return _.range(1,maxGrp+1); | |
332 | }, | |
333 | grade: function(assessmentIndex, studentNumber) { | |
334 | if (!this.grades[assessmentIndex] || !this.grades[assessmentIndex][studentNumber]) | |
335 | return ""; //no grade yet | |
336 | return this.grades[assessmentIndex][studentNumber]; | |
337 | }, | |
71d1ca9c BA |
338 | groupId: function(group, prefix) { |
339 | return (prefix || "") + "group" + group; | |
e99c53fb BA |
340 | }, |
341 | togglePresence: function(number, index) { | |
342 | // UNIMPLEMENTED | |
343 | // TODO: if no grade (thus automatic 0), toggle "exempt" state on student for current exam | |
344 | // --> automatic update of grades view (just a few number to change) | |
345 | }, | |
346 | computeGrades: function() { | |
347 | // UNIMPLEMENTED | |
348 | // TODO: compute all grades using settings (points, coefficients, bonus/malus...). | |
349 | // If some questions with free answers (open), display answers and ask teacher action. | |
350 | // TODO: need a setting for that too (by student, by exercice, by question) | |
351 | }, | |
352 | }, | |
353 | }); | |
354 | ||
355 | }; |