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
9 display: "assessments", //or "students", or "grades" (admin mode)
11 mode: "view", //or "edit" (some assessment)
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
25 group: 1, //for detailed grades tables
26 grades: { }, //computed
29 $('.modal').each( (i
,elem
) => {
30 if (elem
.id
!= "assessmentEdit")
34 $('#assessmentEdit').modal({
36 this.parseAssessment();
38 $("#questionList").find("code[class^=language-]").each( (i
,elem
) => {
39 Prism
.highlightElement(elem
);
41 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"questionList"]);
48 toggleDisplay: function(area
) {
49 if (this.display
== area
)
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
); })
61 uploadTrigger: function() {
65 let file
= (e
.target
.files
|| e
.dataTransfer
.files
)[0];
69 complete: (results
,file
) => {
71 // Post-process: add group/number if missing
73 results
.data
.forEach( d
=> {
78 if (typeof d
.number
!== "string")
79 d
.number
= d
.number
.toString();
82 $.ajax("/import/students", {
86 students: JSON
.stringify(students
),
91 this.course
.students
= students
;
100 addAssessment: function() {
103 // modal, fill code and description
104 let error
= Validator
.checkObject(this.newAssessment
, "Assessment");
108 $('#newAssessment').modal('close');
109 $.ajax("/add/assessment",
113 name: this.newAssessment
.name
,
120 this.newAssessment
["name"] = "";
121 this.assessmentArray
.push(res
);
129 materialOpenModal: function(id
) {
130 $("#" + id
).modal("open");
131 Materialize
.updateTextFields(); //textareas, time field...
133 updateAssessment: function() {
134 $.ajax("/update/assessment", {
136 data: {assessment: JSON
.stringify(this.assessment
)},
141 this.assessmentArray
[this.assessmentIndex
] = this.assessment
;
149 deleteAssessment: function(assessment
) {
152 if (confirm("Delete assessment '" + assessment
.name
+ "' ?"))
154 $.ajax("/remove/assessment",
157 data: { qid: this.assessment
._id
},
161 this.assessmentArray
.splice( this.assessmentArray
.findIndex( item
=> {
162 return item
._id
== assessment
._id
;
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);
177 this.assessment
.activeSet
.push(questionIndex
);
179 setAssessmentText: function() {
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";
187 txt
+= "\n"; //separate questions by new line
189 this.assessmentText
= txt
;
191 parseAssessment: function() {
193 let lines
= this.assessmentText
.split("\n").map( L
=> { return L
.trim(); })
194 lines
.push(""); //easier parsing
195 let emptyQuestion
= () => {
200 active: true, //default
203 let q
= emptyQuestion();
204 lines
.forEach( L
=> {
207 if (['+','-'].includes(L
.charAt(0)))
209 if (L
.charAt(0) == '+')
210 q
.answer
.push(q
.options
.length
);
211 q
.options
.push(L
.slice(1).trim());
213 else if (L
.charAt(0) == '*')
215 // TODO: read current + next lines into q.answer (HTML, 1-elem array)
218 q
.wording
+= L
+ "\n";
222 // Flush current question (if any)
223 if (q
.wording
.length
> 0)
230 this.assessment
.questions
= questions
;
232 actionAssessment: function(index
) {
236 this.assessmentIndex
= index
;
237 this.assessment
= $.extend(true, {}, this.assessmentArray
[index
]);
238 this.setAssessmentText();
240 Vue
.nextTick( () => {
241 $("#questionList").find("code[class^=language-]").each( (i
,elem
) => {
242 Prism
.highlightElement(elem
);
244 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"questionList"]);
247 else //external user: show assessment
248 this.redirect(this.assessmentArray
[index
].name
);
250 redirect: function(assessmentName
) {
251 document
.location
.href
= "/" + initials
+ "/" + course
.code
+ "/" + assessmentName
;
253 setPassword: function() {
254 let hashPwd
= Sha1
.Compute(this.monitorPwd
);
255 let error
= Validator
.checkObject({password:hashPwd
}, "Course");
256 if (error
.length
> 0)
258 $.ajax("/set/password",
262 cid: this.course
._id
,
268 alert("Password saved!");
275 // NOTE: artifact required for Vue v-model to behave well
276 checkBoxFixedId: function(i
) {
277 return "questionFixed" + i
;
279 checkBoxActiveId: function(i
) {
280 return "questionActive" + i
;
283 gradeSettings: function() {
284 $("#gradeSettings").modal("open");
285 Materialize
.updateTextFields(); //total points field in grade settings overlap
287 download: function() {
288 // Download (all) grades as a CSV file
290 this.studentList(0).forEach( s
=> {
293 if (!!this.grades
[s
.number
])
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
]))
299 finalGrade
+= s
[assessmentName
];
302 if (gradesCount
>= 1)
303 finalGrade
/= gradesCount
;
304 s
["final"] = finalGrade
; //TODO: forbid "final" as assessment name
307 data
.push(s
); //number,name,group,assessName1...assessNameN,final
309 let csv
= Papa
.unparse(data
, {
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
319 showDetails: function(group
) {
321 $("#detailedGrades").modal("open");
323 groupList: function() {
325 this.course
.students
.forEach( s
=> {
326 if (s
.group
> maxGrp
)
329 return _
.range(1,maxGrp
+1);
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
];
336 groupId: function(group
, prefix
) {
337 return (prefix
|| "") + "group" + group
;
339 togglePresence: function(number
, index
) {
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)
344 computeGrades: function() {
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)