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