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