ec56309e7aaf8aeb40283ee7418785ecfdeb5a10
[qomet.git] / public / javascripts / assessment.js
1 let socket = null; //monitor answers in real time
2
3 if (assessment.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: "#assessment",
17 data: {
18 assessment: assessment,
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 stage: assessment.mode != "open" ? 0 : 1,
27 remainingTime: 0, //global, in seconds
28 warnMsg: "",
29 },
30 computed: {
31 countdown: function() {
32 let seconds = this.remainingTime % 60;
33 let minutes = Math.floor(this.remainingTime / 60);
34 return this.padWithZero(minutes) + ":" + this.padWithZero(seconds);
35 },
36 showAnswers: function() {
37 return this.stage == 4;
38 },
39 },
40 mounted: function() {
41 $(".modal").modal();
42 if (assessment.mode != "open")
43 {
44 window.addEventListener("keydown", e => {
45 // Ignore F12 (avoid accidental window resize due to devtools)
46 // NOTE: in Chromium at least, fullscreen mode exit with F11 cannot be prevented.
47 // Workaround: disable key at higher level. Possible xbindkey config:
48 // "false"
49 // m:0x10 + c:95
50 // Mod2 + F11
51 if (e.keyCode == 123)
52 e.preventDefault();
53 }, false);
54 }
55 window.addEventListener("blur", () => {
56 if (!socket)
57 return;
58 if (assessment.mode == "secure")
59 {
60 this.trySendCurrentAnswer();
61 document.location.href= "/noblur";
62 }
63 else if (assessment.mode == "exam")
64 socket.emit(message.studentBlur, {number:this.student.number});
65 }, false);
66 if (assessment.mode == "exam")
67 {
68 window.addEventListener("focus", () => {
69 if (!socket)
70 return;
71 socket.emit(message.studentFocus, {number:this.student.number});
72 }, false);
73 }
74 window.addEventListener("resize", e => {
75 if (!socket)
76 return;
77 if (assessment.mode == "secure")
78 {
79 this.trySendCurrentAnswer();
80 document.location.href= "/fullscreen";
81 }
82 else if (assessment.mode == "exam")
83 {
84 if (checkWindowSize())
85 socket.emit(message.studentFullscreen, {number:this.student.number});
86 else
87 socket.emit(message.studentResize, {number:this.student.number});
88 }
89 }, false);
90 },
91 methods: {
92 // In case of AJAX errors
93 showWarning: function(message) {
94 this.warnMsg = message;
95 $("#warning").modal("open");
96 },
97 padWithZero: function(x) {
98 if (x < 10)
99 return "0" + x;
100 return x;
101 },
102 trySendCurrentAnswer: function() {
103 if (this.stage == 2)
104 this.sendAnswer();
105 },
106 // stage 0 --> 1
107 getStudent: function(cb) {
108 $.ajax("/get/student", {
109 method: "GET",
110 data: {
111 number: this.student.number,
112 cid: assessment.cid,
113 },
114 dataType: "json",
115 success: s => {
116 if (!!s.errmsg)
117 return this.showWarning(s.errmsg);
118 this.stage = 1;
119 this.student = s.student;
120 Vue.nextTick( () => { Materialize.updateTextFields(); });
121 if (!!cb)
122 cb();
123 },
124 });
125 },
126 // stage 1 --> 0
127 cancelStudent: function() {
128 this.stage = 0;
129 },
130 // stage 1 --> 2 (get all questions, set password)
131 startAssessment: function() {
132 let initializeStage2 = (questions,paper) => {
133 $("#leftButton, #rightButton").hide();
134 if (assessment.time > 0)
135 {
136
137 // TODO: distinguish total exam time AND question time
138
139 const deltaTime = !!paper ? Date.now() - paper.startTime : 0;
140 this.remainingTime = assessment.time * 60 - Math.round(deltaTime / 1000);
141 this.runTimer();
142 }
143 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
144 if (!!questions)
145 assessment.questions = questions;
146 this.answers.inputs = [ ];
147 for (let q of assessment.questions)
148 this.answers.inputs.push( _(q.options.length).times( _.constant(false) ) );
149 if (!paper)
150 {
151 this.answers.indices = assessment.fixed
152 ? _.range(assessment.questions.length)
153 : _.shuffle( _.range(assessment.questions.length) );
154 }
155 else
156 {
157 // Resuming
158 let indices = paper.inputs.map( input => { return input.index; });
159 let remainingIndices = _.difference( _.range(assessment.questions.length).map(String), indices );
160 this.answers.indices = indices.concat( _.shuffle(remainingIndices) );
161 }
162 this.answers.index = !!paper ? paper.inputs.length : 0;
163 this.answers.displayAll = assessment.display == "all";
164 this.answers.showSolution = false;
165 this.stage = 2;
166 };
167 if (assessment.mode == "open")
168 return initializeStage2();
169 $.ajax("/start/assessment", {
170 method: "GET",
171 data: {
172 number: this.student.number,
173 aid: assessment._id
174 },
175 dataType: "json",
176 success: s => {
177 if (!!s.errmsg)
178 return this.showWarning(s.errmsg);
179 if (!!s.paper)
180 {
181 // Resuming: receive stored answers + startTime
182 this.student.password = s.paper.password;
183 this.answers.inputs = s.paper.inputs.map( inp => { return inp.input; });
184 }
185 else
186 {
187 this.student.password = s.password;
188 // Got password: students answers locked to this page until potential teacher
189 // action (power failure, computer down, ...)
190 }
191 socket = io.connect("/", {
192 query: "aid=" + assessment._id + "&number=" + this.student.number + "&password=" + this.student.password
193 });
194 socket.on(message.allAnswers, this.setAnswers);
195 initializeStage2(s.questions, s.paper);
196 },
197 });
198 },
199 // stage 2
200 runTimer: function() {
201 if (assessment.time <= 0)
202 return;
203 let self = this;
204 setInterval( function() {
205 self.remainingTime--;
206 if (self.remainingTime <= 0)
207 {
208 if (self.stage == 2)
209 self.endAssessment();
210 clearInterval(this);
211 }
212 }, 1000);
213 },
214 // stage 2
215 sendOneAnswer: function() {
216 const realIndex = this.answers.indices[this.answers.index];
217 let gotoNext = () => {
218 if (this.answers.index == assessment.questions.length - 1)
219 this.endAssessment();
220 else
221 this.answers.index++;
222 this.$children[0].$forceUpdate(); //TODO: bad HACK, and shouldn't be required...
223 };
224 if (assessment.mode == "open")
225 return gotoNext(); //only local
226 let answerData = {
227 aid: assessment._id,
228 answer: JSON.stringify({
229 index: realIndex.toString(),
230 input: this.answers.inputs[realIndex]
231 .map( (tf,i) => { return {val:tf,idx:i}; } )
232 .filter( item => { return item.val; })
233 .map( item => { return item.idx; })
234 }),
235 number: this.student.number,
236 password: this.student.password,
237 };
238 $.ajax("/send/answer", {
239 method: "GET",
240 data: answerData,
241 dataType: "json",
242 success: ret => {
243 if (!!ret.errmsg)
244 return this.showWarning(ret.errmsg);
245 gotoNext();
246 socket.emit(message.newAnswer, answerData);
247 },
248 });
249 },
250 // TODO: I don't like that + sending should not be definitive in exam mode with display = all
251 sendAnswer: function() {
252 if (assessment.display == "one")
253 this.sendOneAnswer();
254 else
255 assessment.questions.forEach(this.sendOneAnswer);
256 },
257 // stage 2 --> 3 (or 4)
258 // from a message by statements component, or time over
259 endAssessment: function() {
260 // Set endTime, destroy password
261 $("#leftButton, #rightButton").show();
262 if (assessment.mode == "open")
263 {
264 this.stage = 4;
265 this.answers.showSolution = true;
266 this.answers.displayAll = true;
267 return;
268 }
269 $.ajax("/end/assessment", {
270 method: "GET",
271 data: {
272 aid: assessment._id,
273 number: this.student.number,
274 password: this.student.password,
275 },
276 dataType: "json",
277 success: ret => {
278 if (!!ret.errmsg)
279 return this.showWarning(ret.errmsg);
280 this.stage = 3;
281 delete this.student["password"]; //unable to send new answers now
282 },
283 });
284 },
285 // stage 3 --> 4 (on socket message "feedback")
286 setAnswers: function(m) {
287 const answers = JSON.parse(m.answers);
288 for (let i=0; i<answers.length; i++)
289 assessment.questions[i].answer = answers[i];
290 this.answers.showSolution = true;
291 this.answers.displayAll = true;
292 this.stage = 4;
293 socket.disconnect();
294 socket = null;
295 },
296 },
297 });