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