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