Attempt to resurrect qomet code - need some rewrite
[qomet.git] / public / javascripts / evaluation.js
CommitLineData
e99c53fb
BA
1let socket = null; //monitor answers in real time
2
a3080c33 3if (evaluation.mode == "secure" && !checkWindowSize())
cc7c0f5e 4 document.location.href= "/fullscreen";
e99c53fb 5
cc7c0f5e 6function checkWindowSize()
e99c53fb 7{
cc7c0f5e
BA
8 // NOTE: temporarily accept smartphone (security hole: pretend being a smartphone on desktop browser...)
9 if (navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/))
10 return true;
f03a2ad9
BA
11 // 3 is arbitrary, but a small tolerance is required (e.g. in Firefox)
12 return window.innerWidth >= screen.width-3 && window.innerHeight >= screen.height-3;
20c96143 13}
e99c53fb 14
8a2b3260 15new Vue({
a3080c33 16 el: "#evaluation",
e99c53fb 17 data: {
a3080c33 18 evaluation: evaluation,
8a51dbf7
BA
19 answers: { }, //filled later with answering parameters
20 student: { }, //filled later (name, password)
e99c53fb
BA
21 // Stage 0: unauthenticated (number),
22 // 1: authenticated (got a name, unvalidated)
23 // 2: locked: password set, exam started
24 // 3: completed
25 // 4: show answers
a3080c33
BA
26 remainingTime: evaluation.time, //integer or array
27 stage: evaluation.mode != "open" ? 0 : 1,
f03a2ad9 28 warnMsg: "",
e99c53fb 29 },
e99c53fb
BA
30 computed: {
31 countdown: function() {
a3080c33 32 const remainingTime = evaluation.display == "one" && _.isArray(evaluation.time)
a80c6a3b
BA
33 ? this.remainingTime[this.answers.index]
34 : this.remainingTime;
35 let seconds = remainingTime % 60;
36 let minutes = Math.floor(remainingTime / 60);
e99c53fb
BA
37 return this.padWithZero(minutes) + ":" + this.padWithZero(seconds);
38 },
39 },
f03a2ad9
BA
40 mounted: function() {
41 $(".modal").modal();
a3080c33 42 if (["exam","open"].includes(evaluation.mode))
20c96143 43 return;
435371c7 44 window.addEventListener("blur", () => {
20c96143 45 if (this.stage != 2)
2bada710 46 return;
a3080c33 47 if (evaluation.mode == "secure")
2bada710 48 {
a80c6a3b 49 this.sendAnswer();
2bada710
BA
50 document.location.href= "/noblur";
51 }
20c96143 52 else //"watch" mode
2bada710 53 socket.emit(message.studentBlur, {number:this.student.number});
435371c7 54 }, false);
a3080c33 55 if (evaluation.mode == "watch")
2bada710
BA
56 {
57 window.addEventListener("focus", () => {
45e056cf
BA
58 if (this.stage == 2)
59 socket.emit(message.studentFocus, {number:this.student.number});
2bada710
BA
60 }, false);
61 }
435371c7 62 window.addEventListener("resize", e => {
20c96143 63 if (this.stage != 2)
2bada710 64 return;
a3080c33 65 if (evaluation.mode == "secure")
2bada710 66 {
a80c6a3b 67 this.sendAnswer();
20c96143 68 document.location.href = "/fullscreen";
2bada710 69 }
20c96143 70 else //"watch" mode
2bada710
BA
71 {
72 if (checkWindowSize())
73 socket.emit(message.studentFullscreen, {number:this.student.number});
74 else
75 socket.emit(message.studentResize, {number:this.student.number});
76 }
435371c7 77 }, false);
f03a2ad9 78 },
e99c53fb 79 methods: {
a80c6a3b 80 // In case of AJAX errors (not blur-ing)
71d1ca9c 81 showWarning: function(message) {
f03a2ad9
BA
82 this.warnMsg = message;
83 $("#warning").modal("open");
84 },
e99c53fb
BA
85 padWithZero: function(x) {
86 if (x < 10)
87 return "0" + x;
88 return x;
89 },
90 // stage 0 --> 1
a80c6a3b 91 getStudent: function() {
73609d3b 92 $.ajax("/courses/student", {
e99c53fb
BA
93 method: "GET",
94 data: {
95 number: this.student.number,
a3080c33 96 cid: evaluation.cid,
e99c53fb
BA
97 },
98 dataType: "json",
99 success: s => {
100 if (!!s.errmsg)
71d1ca9c 101 return this.showWarning(s.errmsg);
e99c53fb
BA
102 this.stage = 1;
103 this.student = s.student;
104 Vue.nextTick( () => { Materialize.updateTextFields(); });
e99c53fb
BA
105 },
106 });
107 },
108 // stage 1 --> 0
109 cancelStudent: function() {
110 this.stage = 0;
111 },
112 // stage 1 --> 2 (get all questions, set password)
a3080c33 113 startEvaluation: function() {
a80c6a3b 114 let initializeStage2 = paper => {
e99c53fb 115 $("#leftButton, #rightButton").hide();
e99c53fb 116 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
a80c6a3b
BA
117
118 // if display == "all" getQuestionS
119 // otherwise get first question
120
121
e99c53fb 122 if (!!questions)
a3080c33 123 evaluation.questions = questions;
8a51dbf7 124 this.answers.inputs = [ ];
a3080c33 125 for (let q of evaluation.questions)
71d1ca9c 126 this.answers.inputs.push( _(q.options.length).times( _.constant(false) ) );
f03a2ad9
BA
127 if (!paper)
128 {
a3080c33
BA
129 this.answers.indices = evaluation.fixed
130 ? _.range(evaluation.questions.length)
131 : _.shuffle( _.range(evaluation.questions.length) );
f03a2ad9
BA
132 }
133 else
134 {
135 // Resuming
136 let indices = paper.inputs.map( input => { return input.index; });
a3080c33 137 let remainingIndices = _.difference( _.range(evaluation.questions.length).map(String), indices );
8a51dbf7 138 this.answers.indices = indices.concat( _.shuffle(remainingIndices) );
f03a2ad9 139 }
a80c6a3b
BA
140
141
142
143
144
145
146
a3080c33 147 if (evaluation.time > 0)
a80c6a3b
BA
148 {
149
150// TODO: distinguish total exam time AND question time
151
152 const deltaTime = !!paper ? Date.now() - paper.startTime : 0;
a3080c33 153 this.remainingTime = evaluation.time * 60 - Math.round(deltaTime / 1000);
a80c6a3b
BA
154 this.runTimer();
155 }
156
157
8a51dbf7 158 this.answers.index = !!paper ? paper.inputs.length : 0;
a3080c33 159 this.answers.displayAll = evaluation.display == "all";
3b8117c5 160 this.answers.showSolution = false;
e99c53fb 161 this.stage = 2;
e99c53fb 162 };
a3080c33 163 if (evaluation.mode == "open")
e99c53fb 164 return initializeStage2();
a3080c33 165 $.ajax("/evaluations/start", {
73609d3b 166 method: "PUT",
e99c53fb
BA
167 data: {
168 number: this.student.number,
a3080c33 169 aid: evaluation._id
e99c53fb
BA
170 },
171 dataType: "json",
172 success: s => {
173 if (!!s.errmsg)
71d1ca9c 174 return this.showWarning(s.errmsg);
f03a2ad9
BA
175 if (!!s.paper)
176 {
177 // Resuming: receive stored answers + startTime
178 this.student.password = s.paper.password;
8a51dbf7 179 this.answers.inputs = s.paper.inputs.map( inp => { return inp.input; });
f03a2ad9
BA
180 }
181 else
182 {
183 this.student.password = s.password;
184 // Got password: students answers locked to this page until potential teacher
185 // action (power failure, computer down, ...)
186 }
29c8b391 187 socket = io.connect("/", {
a3080c33 188 query: "aid=" + evaluation._id + "&number=" + this.student.number + "&password=" + this.student.password
e5ec7dea
BA
189 });
190 socket.on(message.allAnswers, this.setAnswers);
f03a2ad9 191 initializeStage2(s.questions, s.paper);
e99c53fb
BA
192 },
193 });
194 },
20c96143
BA
195
196
e99c53fb 197 // stage 2
20c96143 198 runGlobalTimer: function() {
a3080c33 199 if (evaluation.time <= 0)
e99c53fb
BA
200 return;
201 let self = this;
202 setInterval( function() {
203 self.remainingTime--;
71d1ca9c
BA
204 if (self.remainingTime <= 0)
205 {
206 if (self.stage == 2)
a3080c33 207 self.endEvaluation();
e99c53fb 208 clearInterval(this);
71d1ca9c 209 }
e99c53fb
BA
210 }, 1000);
211 },
20c96143 212 runQuestionTimer: function(idx) {
a3080c33 213 if (evaluation.questions[idx].time <= 0)
20c96143
BA
214 return;
215 let self = this; //TODO: question remaining time
216 setInterval( function() {
217 self.remainingTime--;
218 if (self.remainingTime <= 0)
219 {
220 if (self.stage == 2)
a3080c33 221 self.endEvaluation();
20c96143
BA
222 clearInterval(this);
223 }
224 }, 1000);
225 },
226
227//TODO: get question after sending answer
228
435371c7 229 // stage 2
9f4f3259 230 sendOneAnswer: function() {
71d1ca9c 231 const realIndex = this.answers.indices[this.answers.index];
435371c7 232 let gotoNext = () => {
a3080c33
BA
233 if (this.answers.index == evaluation.questions.length - 1)
234 this.endEvaluation();
435371c7 235 else
71d1ca9c
BA
236 this.answers.index++;
237 this.$children[0].$forceUpdate(); //TODO: bad HACK, and shouldn't be required...
435371c7 238 };
a3080c33 239 if (evaluation.mode == "open")
435371c7
BA
240 return gotoNext(); //only local
241 let answerData = {
a3080c33 242 aid: evaluation._id,
435371c7 243 answer: JSON.stringify({
71d1ca9c
BA
244 index: realIndex.toString(),
245 input: this.answers.inputs[realIndex]
435371c7
BA
246 .map( (tf,i) => { return {val:tf,idx:i}; } )
247 .filter( item => { return item.val; })
248 .map( item => { return item.idx; })
249 }),
250 number: this.student.number,
251 password: this.student.password,
252 };
a3080c33 253 $.ajax("/evaluations/answer", {
73609d3b 254 method: "PUT",
435371c7
BA
255 data: answerData,
256 dataType: "json",
257 success: ret => {
258 if (!!ret.errmsg)
71d1ca9c
BA
259 return this.showWarning(ret.errmsg);
260 gotoNext();
435371c7
BA
261 socket.emit(message.newAnswer, answerData);
262 },
263 });
264 },
9f4f3259
BA
265 // TODO: I don't like that + sending should not be definitive in exam mode with display = all
266 sendAnswer: function() {
a3080c33 267 if (evaluation.display == "one")
4a4a6497 268 this.sendOneAnswer();
9f4f3259 269 else
a3080c33 270 evaluation.questions.forEach(this.sendOneAnswer);
9f4f3259 271 },
e99c53fb 272 // stage 2 --> 3 (or 4)
cc7c0f5e 273 // from a message by statements component, or time over
a3080c33 274 endEvaluation: function() {
cc7c0f5e 275 // Set endTime, destroy password
e99c53fb 276 $("#leftButton, #rightButton").show();
a3080c33 277 if (evaluation.mode == "open")
e99c53fb 278 {
e99c53fb 279 this.stage = 4;
3b8117c5 280 this.answers.showSolution = true;
29c8b391 281 this.answers.displayAll = true;
cc7c0f5e
BA
282 return;
283 }
a3080c33 284 $.ajax("/evaluations/end", {
73609d3b 285 method: "PUT",
cc7c0f5e 286 data: {
a3080c33 287 aid: evaluation._id,
cc7c0f5e
BA
288 number: this.student.number,
289 password: this.student.password,
290 },
291 dataType: "json",
292 success: ret => {
293 if (!!ret.errmsg)
71d1ca9c 294 return this.showWarning(ret.errmsg);
cc7c0f5e
BA
295 this.stage = 3;
296 delete this.student["password"]; //unable to send new answers now
cc7c0f5e
BA
297 },
298 });
e99c53fb
BA
299 },
300 // stage 3 --> 4 (on socket message "feedback")
71d1ca9c 301 setAnswers: function(m) {
29c8b391
BA
302 const answers = JSON.parse(m.answers);
303 for (let i=0; i<answers.length; i++)
a3080c33 304 evaluation.questions[i].answer = answers[i];
3b8117c5 305 this.answers.showSolution = true;
29c8b391 306 this.answers.displayAll = true;
e99c53fb 307 this.stage = 4;
29c8b391
BA
308 socket.disconnect();
309 socket = null;
e99c53fb
BA
310 },
311 },
312});