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
60 .sort( (a
,b
) => { return a
.name
.localeCompare(b
.name
); })
63 uploadTrigger: function() {
67 let file
= (e
.target
.files
|| e
.dataTransfer
.files
)[0];
71 complete: (results
,file
) => {
73 // Post-process: add group/number if missing
75 results
.data
.forEach( d
=> {
80 if (typeof d
.number
!== "string")
81 d
.number
= d
.number
.toString();
84 $.ajax("/import/students", {
88 students: JSON
.stringify(students
),
93 this.course
.students
= students
;
102 addAssessment: function() {
105 // modal, fill code and description
106 let error
= Validator
.checkObject(this.newAssessment
, "Assessment");
110 $('#newAssessment').modal('close');
111 $.ajax("/add/assessment",
115 name: this.newAssessment
.name
,
122 this.newAssessment
["name"] = "";
123 this.assessmentArray
.push(res
);
131 materialOpenModal: function(id
) {
132 $("#" + id
).modal("open");
133 Materialize
.updateTextFields(); //textareas, time field...
135 updateAssessment: function() {
136 $.ajax("/update/assessment", {
138 data: {assessment: JSON
.stringify(this.assessment
)},
143 this.assessmentArray
[this.assessmentIndex
] = this.assessment
;
151 deleteAssessment: function(assessment
) {
154 if (confirm("Delete assessment '" + assessment
.name
+ "' ?"))
156 $.ajax("/remove/assessment",
159 data: { qid: this.assessment
._id
},
163 this.assessmentArray
.splice( this.assessmentArray
.findIndex( item
=> {
164 return item
._id
== assessment
._id
;
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);
179 this.assessment
.activeSet
.push(questionIndex
);
181 setAssessmentText: function() {
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";
189 txt
+= "\n"; //separate questions by new line
191 this.assessmentText
= txt
;
193 parseAssessment: function() {
195 let lines
= this.assessmentText
.split("\n").map( L
=> { return L
.trim(); })
196 lines
.push(""); //easier parsing
197 let emptyQuestion
= () => {
202 active: true, //default
205 let q
= emptyQuestion();
206 lines
.forEach( L
=> {
209 if (['+','-'].includes(L
.charAt(0)))
211 if (L
.charAt(0) == '+')
212 q
.answer
.push(q
.options
.length
);
213 q
.options
.push(L
.slice(1).trim());
215 else if (L
.charAt(0) == '*')
217 // TODO: read current + next lines into q.answer (HTML, 1-elem array)
220 q
.wording
+= L
+ " "; //space required at line breaks, generally
224 // Flush current question (if any)
225 if (q
.wording
.length
> 0)
232 this.assessment
.questions
= questions
;
234 actionAssessment: function(index
) {
238 this.assessmentIndex
= index
;
239 this.assessment
= $.extend(true, {}, this.assessmentArray
[index
]);
240 this.setAssessmentText();
242 Vue
.nextTick( () => {
243 $("#questionList").find("code[class^=language-]").each( (i
,elem
) => {
244 Prism
.highlightElement(elem
);
246 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"questionList"]);
249 else //external user: show assessment
250 this.redirect(this.assessmentArray
[index
].name
);
252 redirect: function(assessmentName
) {
253 document
.location
.href
= "/" + initials
+ "/" + course
.code
+ "/" + assessmentName
;
255 setPassword: function() {
256 let hashPwd
= Sha1
.Compute(this.monitorPwd
);
257 let error
= Validator
.checkObject({password:hashPwd
}, "Course");
258 if (error
.length
> 0)
260 $.ajax("/set/password",
264 cid: this.course
._id
,
270 alert("Password saved!");
277 // NOTE: artifact required for Vue v-model to behave well
278 checkBoxFixedId: function(i
) {
279 return "questionFixed" + i
;
281 checkBoxActiveId: function(i
) {
282 return "questionActive" + i
;
285 gradeSettings: function() {
286 $("#gradeSettings").modal("open");
287 Materialize
.updateTextFields(); //total points field in grade settings overlap
289 download: function() {
290 // Download (all) grades as a CSV file
292 this.studentList(0).forEach( s
=> {
295 if (!!this.grades
[s
.number
])
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
]))
301 finalGrade
+= s
[assessmentName
];
304 if (gradesCount
>= 1)
305 finalGrade
/= gradesCount
;
306 s
["final"] = finalGrade
; //TODO: forbid "final" as assessment name
309 data
.push(s
); //number,name,group,assessName1...assessNameN,final
311 let csv
= Papa
.unparse(data
, {
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
321 showDetails: function(group
) {
323 $("#detailedGrades").modal("open");
325 groupList: function() {
327 this.course
.students
.forEach( s
=> {
328 if (s
.group
> maxGrp
)
331 return _
.range(1,maxGrp
+1);
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
];
338 groupId: function(group
, prefix
) {
339 return (prefix
|| "") + "group" + group
;
341 togglePresence: function(number
, index
) {
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)
346 computeGrades: function() {
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)