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
6 window
.onload = function() {
11 display: "assessments", //or "students", or "grades" (admin mode)
13 mode: "view", //or "edit" (some assessment)
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
27 group: 1, //for detailed grades tables
28 grades: { }, //computed
31 $('.modal').each( (i
,elem
) => {
32 if (elem
.id
!= "assessmentEdit")
36 $('#assessmentEdit').modal({
38 this.parseAssessment();
40 $("#questionList").find("code[class^=language-]").each( (i
,elem
) => {
41 Prism
.highlightElement(elem
);
43 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"questionList"]);
50 toggleDisplay: function(area
) {
51 if (this.display
== area
)
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
61 let res
= a
.name
.localeCompare(b
.name
);
63 res
+= a
.forename
.localeCompare(b
.forename
);
68 uploadTrigger: function() {
72 let file
= (e
.target
.files
|| e
.dataTransfer
.files
)[0];
76 complete: (results
,file
) => {
78 // Post-process: add group/number if missing
80 results
.data
.forEach( d
=> {
85 if (typeof d
.number
!== "string")
86 d
.number
= d
.number
.toString();
89 $.ajax("/import/students", {
93 students: JSON
.stringify(students
),
98 this.course
.students
= students
;
107 addAssessment: function() {
110 // modal, fill code and description
111 let error
= Validator
.checkObject(this.newAssessment
, "Assessment");
115 $('#newAssessment').modal('close');
116 $.ajax("/add/assessment",
120 name: this.newAssessment
.name
,
127 this.newAssessment
["name"] = "";
128 this.assessmentArray
.push(res
);
136 materialOpenModal: function(id
) {
137 $("#" + id
).modal("open");
138 Materialize
.updateTextFields(); //textareas, time field...
140 updateAssessment: function() {
141 $.ajax("/update/assessment", {
143 data: {assessment: JSON
.stringify(this.assessment
)},
148 this.assessmentArray
[this.assessmentIndex
] = this.assessment
;
156 deleteAssessment: function(assessment
) {
159 if (confirm("Delete assessment '" + assessment
.name
+ "' ?"))
161 $.ajax("/remove/assessment",
164 data: { qid: this.assessment
._id
},
168 this.assessmentArray
.splice( this.assessmentArray
.findIndex( item
=> {
169 return item
._id
== assessment
._id
;
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);
184 this.assessment
.activeSet
.push(questionIndex
);
186 setAssessmentText: function() {
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";
194 txt
+= "\n"; //separate questions by new line
196 this.assessmentText
= txt
;
198 parseAssessment: function() {
200 let lines
= this.assessmentText
.split("\n").map( L
=> { return L
.trim(); })
201 lines
.push(""); //easier parsing
202 let emptyQuestion
= () => {
207 active: true, //default
210 let q
= emptyQuestion();
211 lines
.forEach( L
=> {
214 if (['+','-'].includes(L
.charAt(0)))
216 if (L
.charAt(0) == '+')
217 q
.answer
.push(q
.options
.length
);
218 q
.options
.push(L
.slice(1).trim());
220 else if (L
.charAt(0) == '*')
222 // TODO: read current + next lines into q.answer (HTML, 1-elem array)
225 q
.wording
+= L
+ " "; //space required at line breaks, generally
229 // Flush current question (if any)
230 if (q
.wording
.length
> 0)
237 this.assessment
.questions
= questions
;
239 actionAssessment: function(index
) {
243 this.assessmentIndex
= index
;
244 this.assessment
= $.extend(true, {}, this.assessmentArray
[index
]);
245 this.setAssessmentText();
247 Vue
.nextTick( () => {
248 $("#questionList").find("code[class^=language-]").each( (i
,elem
) => {
249 Prism
.highlightElement(elem
);
251 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"questionList"]);
254 else //external user: show assessment
255 this.redirect(this.assessmentArray
[index
].name
);
257 redirect: function(assessmentName
) {
258 document
.location
.href
= "/" + initials
+ "/" + course
.code
+ "/" + assessmentName
;
260 setPassword: function() {
261 let hashPwd
= Sha1
.Compute(this.monitorPwd
);
262 let error
= Validator
.checkObject({password:hashPwd
}, "Course");
263 if (error
.length
> 0)
265 $.ajax("/set/password",
269 cid: this.course
._id
,
275 alert("Password saved!");
282 // NOTE: artifact required for Vue v-model to behave well
283 checkBoxFixedId: function(i
) {
284 return "questionFixed" + i
;
286 checkBoxActiveId: function(i
) {
287 return "questionActive" + i
;
290 gradeSettings: function() {
291 $("#gradeSettings").modal("open");
292 Materialize
.updateTextFields(); //total points field in grade settings overlap
294 download: function() {
295 // Download (all) grades as a CSV file
297 this.studentList(0).forEach( s
=> {
300 if (!!this.grades
[s
.number
])
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
]))
306 finalGrade
+= s
[assessmentName
];
309 if (gradesCount
>= 1)
310 finalGrade
/= gradesCount
;
311 s
["final"] = finalGrade
; //TODO: forbid "final" as assessment name
314 data
.push(s
); //number,forename,name,group,assessName1...assessNameN,final
316 let csv
= Papa
.unparse(data
, {
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
326 showDetails: function(group
) {
328 $("#detailedGrades").modal("open");
330 groupList: function() {
332 this.course
.students
.forEach( s
=> {
333 if (s
.group
> maxGrp
)
336 return _
.range(1,maxGrp
+1);
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
];
343 groupId: function(group
, hash
) {
344 return (!!hash
?"#":"") + "group" + group
;
346 togglePresence: function(number
, index
) {
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)
351 computeGrades: function() {
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)