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