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