First commit
[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 window.addEventListener("keydown", e => {
250 // If F12 or ctrl+shift (ways to access devtools)
251 if (e.keyCode == 123 || (e.ctrlKey && e.shiftKey))
252 e.preventDefault();
253 }, false);
254 // Devtools detect based on https://jsfiddle.net/ebhjxfwv/4/
255 let div = document.createElement('div');
256 let devtoolsLoop = setInterval(
257 () => {
258 if (assessment.mode != "open")
259 {
260 console.log(div);
261 console.clear();
262 }
263 },
264 1000
265 );
266 Object.defineProperty(div, "id", {
267 get: () => {
268 clearInterval(devtoolsLoop);
269 if (assessment.mode != "open")
270 {
271 if (this.stage == 2)
272 this.endAssessment();
273 document.location.href = "/nodevtools";
274 }
275 }
276 });
277 },
278 computed: {
279 countdown: function() {
280 let seconds = this.remainingTime % 60;
281 let minutes = Math.floor(this.remainingTime / 60);
282 return this.padWithZero(minutes) + ":" + this.padWithZero(seconds);
283 },
284 },
285 methods: {
286 // HELPERS:
287 padWithZero: function(x) {
288 if (x < 10)
289 return "0" + x;
290 return x;
291 },
292 // stage 0 --> 1
293 getStudent: function(cb) {
294 $.ajax("/get/student", {
295 method: "GET",
296 data: {
297 number: this.student.number,
298 cid: assessment.cid,
299 },
300 dataType: "json",
301 success: s => {
302 if (!!s.errmsg)
303 return alert(s.errmsg);
304 this.stage = 1;
305 this.student = s.student;
306 Vue.nextTick( () => { Materialize.updateTextFields(); });
307 if (!!cb)
308 cb();
309 },
310 });
311 },
312 // stage 1 --> 0
313 cancelStudent: function() {
314 this.stage = 0;
315 },
316 // stage 1 --> 2 (get all questions, set password)
317 startAssessment: function() {
318 checkWindowSize();
319 let initializeStage2 = questions => {
320 $("#leftButton, #rightButton").hide();
321 if (assessment.time > 0)
322 {
323 this.remainingTime = assessment.time * 60;
324 this.runTimer();
325 }
326 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
327 if (!!questions)
328 assessment.questions = questions;
329 for (let q of assessment.questions)
330 this.inputs.push( _(q.options.length).times( _.constant(false) ) );
331 assessment.indices = assessment.fixed
332 ? _.range(assessment.questions.length)
333 : _.shuffle( _.range(assessment.questions.length) );
334 this.stage = 2;
335 Vue.nextTick( () => { libsRefresh(); });
336 };
337 if (assessment.mode == "open")
338 return initializeStage2();
339 $.ajax("/start/assessment", {
340 method: "GET",
341 data: {
342 number: this.student.number,
343 aid: assessment._id
344 },
345 dataType: "json",
346 success: s => {
347 if (!!s.errmsg)
348 return alert(s.errmsg);
349 this.student.password = s.password;
350 // Got password: students answers locked to this page until potential teacher
351 // action (power failure, computer down, ...)
352 // TODO: password also exchanged by sockets to check identity
353 //socket = io.connect("/" + assessment.name, {
354 // query: "number=" + this.student.number + "&password=" + this.password
355 //});
356 //socket.on(message.allAnswers, this.setAnswers);
357 initializeStage2(s.questions);
358 },
359 });
360 },
361 // stage 2
362 runTimer: function() {
363 if (assessment.time <= 0)
364 return;
365 let self = this;
366 setInterval( function() {
367 self.remainingTime--;
368 if (self.remainingTime <= 0 || self.stage >= 4)
369 self.endAssessment();
370 clearInterval(this);
371 }, 1000);
372 },
373 // stage 2 after disconnect (socket)
374 resumeAssessment: function() {
375 // UNIMPLEMENTED
376 // TODO: get stored answers (papers[number cookie]), inject (inputs), set index+indices
377 },
378 // stage 2 --> 3 (or 4)
379 // from a message by statements component
380 endAssessment: function() {
381 // If time over or cheating: set endTime, destroy password
382 $("#leftButton, #rightButton").show();
383 //this.sendAnswer(...); //TODO: for each non-answered (and non-empty!) index (yet)
384 if (assessment.mode != "open")
385 {
386 $.ajax("/end/assessment", {
387 method: "GET",
388 data: {
389 aid: assessment._id,
390 number: this.student.number,
391 password: this.student.password,
392 },
393 dataType: "json",
394 success: ret => {
395 if (!!ret.errmsg)
396 return alert(ret.errmsg);
397 assessment.conclusion = ret.conclusion;
398 this.stage = 3;
399 delete this.student["password"]; //unable to send new answers now
400 //socket.disconnect();
401 //socket = null;
402 },
403 });
404 }
405 else
406 this.stage = 4;
407 },
408 // stage 3 --> 4 (on socket message "feedback")
409 setAnswers: function(answers) {
410 for (let i=0; i<answers.length; i++)
411 assessment.questions[i].answer = answers[i];
412 this.stage = 4;
413 },
414 },
415 });