05b4d58559042592b9b9efa69fd53e5acdbe9fae
[qomet.git] / public / javascripts / assessment.js
1 let socket = null; //monitor answers in real time
2
3 function checkWindowSize()
4 {
5 if (assessment.mode == "secure")
6 {
7 // NOTE: temporarily accept smartphone (security hole: pretend being a smartphone on desktop browser...)
8 if (navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/))
9 return true;
10 let test = () => {
11 return window.innerWidth < screen.width || window.innerHeight < screen.height;
12 };
13 const returnVal = test;
14 while (!test)
15 alert("Please enter fullscreen mode (F11)");
16 return returnVal;
17 }
18 return true;
19 };
20
21 function libsRefresh()
22 {
23 $("#statements").find("code[class^=language-]").each( (i,elem) => {
24 Prism.highlightElement(elem);
25 });
26 MathJax.Hub.Queue(["Typeset",MathJax.Hub,"statements"]);
27 };
28
29 // TODO: if display == "all", les envois devraient être non définitifs (possibilité de corriger)
30 // Et, blur sur une (sous-)question devrait envoyer la version courante de la sous-question
31
32 let V = new Vue({
33 el: "#assessment",
34 data: {
35 assessment: assessment,
36 inputs: [ ], //student's answers
37 student: { }, //filled later
38 // Stage 0: unauthenticated (number),
39 // 1: authenticated (got a name, unvalidated)
40 // 2: locked: password set, exam started
41 // 3: completed
42 // 4: show answers
43 stage: assessment.mode != "open" ? 0 : 1,
44 remainingTime: 0, //global, in seconds
45 },
46 components: {
47 "statements": {
48 props: ['assessment','inputs','student','stage'],
49 data: function() {
50 return {
51 index: 0, //current question index in assessment.indices
52 };
53 },
54 mounted: function() {
55 if (assessment.mode != "secure")
56 return;
57 $("#warning").modal({
58 complete: () => {
59 this.stage = 2;
60 this.resumeAssessment();
61 },
62 });
63 window.addEventListener("blur", () => {
64 if (this.stage == 2)
65 this.showWarning();
66 }, false);
67 window.addEventListener("resize", e => {
68 if (this.stage == 2 && !checkWindowSize())
69 this.showWarning();
70 }, false);
71 //socket.on("disconnect", () => { }); //TODO: notify monitor (highlight red)
72 },
73 updated: function() {
74 libsRefresh();
75 },
76 // TODO: general render function for nested exercises
77 // TODO: with answer if stage==4 : class "wrong" if ticked AND stage==4 AND received answers
78 // class "right" if stage == 4 AND received answers (background-color: red / green)
79 // There should be a questions navigator below, or next (visible if display=='all')
80 // Full questions tree is rendered, but some parts hidden depending on display settings
81 render(h) {
82 let self = this;
83 let questions = assessment.questions.map( (q,i) => {
84 let questionContent = [ ];
85 questionContent.push(
86 h(
87 "div",
88 {
89 "class": {
90 "wording": true,
91 },
92 domProps: {
93 innerHTML: q.wording,
94 },
95 }
96 )
97 );
98 let optionsOrder = _.range(q.options.length);
99 if (!q.fixed)
100 optionsOrder = _.shuffle(optionsOrder);
101 let optionList = [ ];
102 optionsOrder.forEach( idx => {
103 let option = [ ];
104 option.push(
105 h(
106 "input",
107 {
108 domProps: {
109 checked: this.inputs[i][idx],
110 },
111 attrs: {
112 id: this.inputId(i,idx),
113 type: "checkbox",
114 },
115 on: {
116 change: e => { this.inputs[i][idx] = e.target.checked; },
117 },
118 },
119 )
120 );
121 option.push(
122 h(
123 "label",
124 {
125 domProps: {
126 innerHTML: q.options[idx],
127 },
128 attrs: {
129 "for": this.inputId(i,idx),
130 },
131 }
132 )
133 );
134 optionList.push(
135 h(
136 "div",
137 {
138 "class": {
139 option: true,
140 choiceCorrect: this.stage == 4 && assessment.questions[i].answer.includes(idx),
141 choiceWrong: this.stage == 4 && this.inputs[i][idx] && !assessment.questions[i].answer.includes(idx),
142 },
143 },
144 option
145 )
146 );
147 });
148 questionContent.push(
149 h(
150 "div",
151 {
152 "class": {
153 optionList: true,
154 },
155 },
156 optionList
157 )
158 );
159 return h(
160 "div",
161 {
162 "class": {
163 "question": true,
164 "hide": this.stage == 2 && assessment.display == 'one' && assessment.indices[this.index] != i,
165 },
166 },
167 questionContent
168 );
169 });
170 if (this.stage == 2)
171 {
172 // TODO: one button per question
173 questions.unshift(
174 h(
175 "button",
176 {
177 "class": {
178 "waves-effect": true,
179 "waves-light": true,
180 "btn": true,
181 },
182 on: {
183 click: () => this.sendAnswer(assessment.indices[this.index]),
184 },
185 },
186 "Send"
187 )
188 );
189 }
190 return h(
191 "div",
192 {
193 attrs: {
194 id: "statements",
195 },
196 },
197 questions
198 );
199 },
200 methods: {
201 // HELPERS:
202 inputId: function(i,j) {
203 return "q" + i + "_" + "input" + j;
204 },
205 showWarning: function(action) {
206 this.sendAnswer(assessment.indices[this.index]);
207 this.stage = 32; //fictive stage to hide all elements
208 $("#warning").modal('open');
209 },
210 // stage 2
211 sendAnswer: function(realIndex) {
212 if (this.index == assessment.questions.length - 1)
213 this.$emit("gameover");
214 else
215 this.index++;
216 if (assessment.mode == "open")
217 return; //only local
218 let answerData = {
219 aid: assessment._id,
220 answer: JSON.stringify({
221 index:realIndex.toString(),
222 input:this.inputs[realIndex]
223 .map( (tf,i) => { return {val:tf,idx:i}; } )
224 .filter( item => { return item.val; })
225 .map( item => { return item.idx; })
226 }),
227 number: this.student.number,
228 password: this.student.password,
229 };
230 $.ajax("/send/answer", {
231 method: "GET",
232 data: answerData,
233 dataType: "json",
234 success: ret => {
235 if (!!ret.errmsg)
236 return alert(ret.errmsg);
237 //socket.emit(message.newAnswer, answer);
238 },
239 });
240 },
241 // stage 2 after blur or resize
242 resumeAssessment: function() {
243 checkWindowSize();
244 },
245 },
246 },
247 },
248 mounted: function() {
249 if (assessment.mode == "open")
250 return; //no security needed in open mode
251 window.addEventListener("keydown", e => {
252 // If F12 or ctrl+shift (ways to access devtools)
253 if (e.keyCode == 123 || (e.ctrlKey && e.shiftKey))
254 e.preventDefault();
255 }, false);
256 // Devtools detect based on https://jsfiddle.net/ebhjxfwv/4/
257 let div = document.createElement('div');
258 let devtoolsLoop = setInterval(
259 () => {
260 if (assessment.mode != "open")
261 {
262 console.log(div);
263 console.clear();
264 }
265 },
266 1000
267 );
268 Object.defineProperty(div, "id", {
269 get: () => {
270 clearInterval(devtoolsLoop);
271 if (this.stage == 2)
272 this.endAssessment();
273 document.location.href = "/nodevtools";
274 }
275 });
276 },
277 computed: {
278 countdown: function() {
279 let seconds = this.remainingTime % 60;
280 let minutes = Math.floor(this.remainingTime / 60);
281 return this.padWithZero(minutes) + ":" + this.padWithZero(seconds);
282 },
283 },
284 methods: {
285 // HELPERS:
286 padWithZero: function(x) {
287 if (x < 10)
288 return "0" + x;
289 return x;
290 },
291 // stage 0 --> 1
292 getStudent: function(cb) {
293 $.ajax("/get/student", {
294 method: "GET",
295 data: {
296 number: this.student.number,
297 cid: assessment.cid,
298 },
299 dataType: "json",
300 success: s => {
301 if (!!s.errmsg)
302 return alert(s.errmsg);
303 this.stage = 1;
304 this.student = s.student;
305 Vue.nextTick( () => { Materialize.updateTextFields(); });
306 if (!!cb)
307 cb();
308 },
309 });
310 },
311 // stage 1 --> 0
312 cancelStudent: function() {
313 this.stage = 0;
314 },
315 // stage 1 --> 2 (get all questions, set password)
316 startAssessment: function() {
317 checkWindowSize();
318 let initializeStage2 = questions => {
319 $("#leftButton, #rightButton").hide();
320 if (assessment.time > 0)
321 {
322 this.remainingTime = assessment.time * 60;
323 this.runTimer();
324 }
325 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
326 if (!!questions)
327 assessment.questions = questions;
328 for (let q of assessment.questions)
329 this.inputs.push( _(q.options.length).times( _.constant(false) ) );
330 assessment.indices = assessment.fixed
331 ? _.range(assessment.questions.length)
332 : _.shuffle( _.range(assessment.questions.length) );
333 this.stage = 2;
334 Vue.nextTick( () => { libsRefresh(); });
335 };
336 if (assessment.mode == "open")
337 return initializeStage2();
338 $.ajax("/start/assessment", {
339 method: "GET",
340 data: {
341 number: this.student.number,
342 aid: assessment._id
343 },
344 dataType: "json",
345 success: s => {
346 if (!!s.errmsg)
347 return alert(s.errmsg);
348 this.student.password = s.password;
349 // Got password: students answers locked to this page until potential teacher
350 // action (power failure, computer down, ...)
351 // TODO: password also exchanged by sockets to check identity
352 //socket = io.connect("/" + assessment.name, {
353 // query: "number=" + this.student.number + "&password=" + this.password
354 //});
355 //socket.on(message.allAnswers, this.setAnswers);
356 initializeStage2(s.questions);
357 },
358 });
359 },
360 // stage 2
361 runTimer: function() {
362 if (assessment.time <= 0)
363 return;
364 let self = this;
365 setInterval( function() {
366 self.remainingTime--;
367 if (self.remainingTime <= 0 || self.stage >= 4)
368 self.endAssessment();
369 clearInterval(this);
370 }, 1000);
371 },
372 // stage 2 after disconnect (socket)
373 resumeAssessment: function() {
374 // UNIMPLEMENTED
375 // TODO: get stored answers (papers[number cookie]), inject (inputs), set index+indices
376 },
377 // stage 2 --> 3 (or 4)
378 // from a message by statements component
379 endAssessment: function() {
380 // If time over or cheating: set endTime, destroy password
381 $("#leftButton, #rightButton").show();
382 //this.sendAnswer(...); //TODO: for each non-answered (and non-empty!) index (yet)
383 if (assessment.mode != "open")
384 {
385 $.ajax("/end/assessment", {
386 method: "GET",
387 data: {
388 aid: assessment._id,
389 number: this.student.number,
390 password: this.student.password,
391 },
392 dataType: "json",
393 success: ret => {
394 if (!!ret.errmsg)
395 return alert(ret.errmsg);
396 assessment.conclusion = ret.conclusion;
397 this.stage = 3;
398 delete this.student["password"]; //unable to send new answers now
399 //socket.disconnect();
400 //socket = null;
401 },
402 });
403 }
404 else
405 this.stage = 4;
406 },
407 // stage 3 --> 4 (on socket message "feedback")
408 setAnswers: function(answers) {
409 for (let i=0; i<answers.length; i++)
410 assessment.questions[i].answer = answers[i];
411 this.stage = 4;
412 },
413 },
414 });