'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 (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);
62 },
63 methods: {
64 // In case of AJAX errors
65 warning: function(message) {
66 this.warnMsg = message;
67 $("#warning").modal("open");
68 },
69 padWithZero: function(x) {
70 if (x < 10)
71 return "0" + x;
72 return x;
73 },
74 trySendCurrentAnswer: function() {
75 if (this.stage == 2)
76 this.sendAnswer(assessment.indices[assessment.index]);
77 },
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)
89 return this.warning(s.errmsg);
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() {
104 let initializeStage2 = (questions,paper) => {
105 $("#leftButton, #rightButton").hide();
106 if (assessment.time > 0)
107 {
108 const deltaTime = !!paper ? Date.now() - paper.startTime : 0;
109 this.remainingTime = assessment.time * 60 - Math.round(deltaTime / 1000);
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;
115 this.answers.inputs = [ ];
116 for (let q of assessment.questions)
117 this.inputs.push( _(q.options.length).times( _.constant(false) ) );
118 if (!paper)
119 {
120 this.answers.indices = assessment.fixed
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; });
128 let remainingIndices = _.difference( _.range(assessment.questions.length).map(String), indices );
129 this.answers.indices = indices.concat( _.shuffle(remainingIndices) );
130 }
131 this.answers.index = !!paper ? paper.inputs.length : 0;
132 Vue.nextTick(libsRefresh);
133 this.stage = 2;
134 };
135 if (assessment.mode == "open")
136 return initializeStage2();
137 $.ajax("/start/assessment", {
138 method: "GET",
139 data: {
140 number: this.student.number,
141 aid: assessment._id
142 },
143 dataType: "json",
144 success: s => {
145 if (!!s.errmsg)
146 return this.warning(s.errmsg);
147 if (!!s.paper)
148 {
149 // Resuming: receive stored answers + startTime
150 this.student.password = s.paper.password;
151 this.answers.inputs = s.paper.inputs.map( inp => { return inp.input; });
152 }
153 else
154 {
155 this.student.password = s.password;
156 // Got password: students answers locked to this page until potential teacher
157 // action (power failure, computer down, ...)
158 }
159 socket = io.connect("/" + assessment.name, {
160 query: "number=" + this.student.number + "&password=" + this.password
161 });
162 socket.on(message.allAnswers, this.setAnswers);
163 initializeStage2(s.questions, s.paper);
164 },
165 });
166 },
167 // stage 2
168 runTimer: function() {
169 if (assessment.time <= 0)
170 return;
171 let self = this;
172 setInterval( function() {
173 self.remainingTime--;
174 if (self.remainingTime <= 0 || self.stage >= 4)
175 self.endAssessment();
176 clearInterval(this);
177 }, 1000);
178 },
179 // stage 2
180 // TODO: currentIndex ? click: () => this.sendAnswer(assessment.indices[assessment.index]),
181 // De même, cette condition sur le display d'une question doit remonter (résumée dans 'index' property) :
182 // à faire par ici : "hide": this.stage == 2 && assessment.display == 'one' && assessment.indices[assessment.index] != i,
183 sendAnswer: function(realIndex) {
184 let gotoNext = () => {
185 if (assessment.index == assessment.questions.length - 1)
186 this.endAssessment();
187 else
188 assessment.index++;
189 this.$forceUpdate(); //TODO: shouldn't be required
190 };
191 if (assessment.mode == "open")
192 return gotoNext(); //only local
193 let answerData = {
194 aid: assessment._id,
195 answer: JSON.stringify({
196 index:realIndex.toString(),
197 input:this.inputs[realIndex]
198 .map( (tf,i) => { return {val:tf,idx:i}; } )
199 .filter( item => { return item.val; })
200 .map( item => { return item.idx; })
201 }),
202 number: this.student.number,
203 password: this.student.password,
204 };
205 $.ajax("/send/answer", {
206 method: "GET",
207 data: answerData,
208 dataType: "json",
209 success: ret => {
210 if (!!ret.errmsg)
211 return this.$emit("warning", ret.errmsg);
212 else
213 gotoNext();
214 socket.emit(message.newAnswer, answerData);
215 },
216 });
217 },
218 // stage 2 --> 3 (or 4)
219 // from a message by statements component, or time over
220 endAssessment: function() {
221 // Set endTime, destroy password
222 $("#leftButton, #rightButton").show();
223 if (assessment.mode == "open")
224 {
225 this.stage = 4;
226 return;
227 }
228 $.ajax("/end/assessment", {
229 method: "GET",
230 data: {
231 aid: assessment._id,
232 number: this.student.number,
233 password: this.student.password,
234 },
235 dataType: "json",
236 success: ret => {
237 if (!!ret.errmsg)
238 return this.warning(ret.errmsg);
239 assessment.conclusion = ret.conclusion;
240 this.stage = 3;
241 delete this.student["password"]; //unable to send new answers now
242 socket.disconnect();
243 socket = null;
244 },
245 });
246 },
247 // stage 3 --> 4 (on socket message "feedback")
248 setAnswers: function(answers) {
249 for (let i=0; i<answers.length; i++)
250 assessment.questions[i].answer = answers[i];
251 this.stage = 4;
252 },
253 },
254 });