Prepare monitoring using Statements component (early draft stage)
[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 inputs: [ ], //student's answers
20 student: { }, //filled later
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 },
37 mounted: function() {
38 $(".modal").modal();
39 if (assessment.mode != "secure")
40 return;
41 window.addEventListener("keydown", e => {
42 // Ignore F12 (avoid accidental window resize due to devtools)
43 // NOTE: in Chromium at least, fullscreen mode exit with F11 cannot be prevented.
44 // Workaround: disable key at higher level. Possible xbindkey config:
45 // "false"
46 // m:0x10 + c:95
47 // Mod2 + F11
48 if (e.keyCode == 123)
49 e.preventDefault();
50 }, false);
51 window.addEventListener("blur", () => {
52 this.trySendCurrentAnswer();
53 document.location.href= "/noblur";
54 }, false);
55 window.addEventListener("resize", e => {
56 this.trySendCurrentAnswer();
57 document.location.href= "/fullscreen";
58 }, false);
59 },
60 trySendCurrentAnswer: function() {
61 if (this.stage == 2)
62 this.sendAnswer(assessment.indices[assessment.index]);
63 },
64 },
65 methods: {
66 // In case of AJAX errors
67 warning: function(message) {
68 this.warnMsg = message;
69 $("#warning").modal("open");
70 },
71 padWithZero: function(x) {
72 if (x < 10)
73 return "0" + x;
74 return x;
75 },
76 // stage 0 --> 1
77 getStudent: function(cb) {
78 $.ajax("/get/student", {
79 method: "GET",
80 data: {
81 number: this.student.number,
82 cid: assessment.cid,
83 },
84 dataType: "json",
85 success: s => {
86 if (!!s.errmsg)
87 return this.warning(s.errmsg);
88 this.stage = 1;
89 this.student = s.student;
90 Vue.nextTick( () => { Materialize.updateTextFields(); });
91 if (!!cb)
92 cb();
93 },
94 });
95 },
96 // stage 1 --> 0
97 cancelStudent: function() {
98 this.stage = 0;
99 },
100 // stage 1 --> 2 (get all questions, set password)
101 startAssessment: function() {
102 let initializeStage2 = (questions,paper) => {
103 $("#leftButton, #rightButton").hide();
104 if (assessment.time > 0)
105 {
106 const deltaTime = !!paper ? Date.now() - paper.startTime : 0;
107 this.remainingTime = assessment.time * 60 - Math.round(deltaTime / 1000);
108 this.runTimer();
109 }
110 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
111 if (!!questions)
112 assessment.questions = questions;
113 for (let q of assessment.questions)
114 this.inputs.push( _(q.options.length).times( _.constant(false) ) );
115 if (!paper)
116 {
117 assessment.indices = assessment.fixed
118 ? _.range(assessment.questions.length)
119 : _.shuffle( _.range(assessment.questions.length) );
120 }
121 else
122 {
123 // Resuming
124 let indices = paper.inputs.map( input => { return input.index; });
125 let remainingIndices = _.difference( _.range(assessment.questions.length).map(String), indices );
126 assessment.indices = indices.concat( _.shuffle(remainingIndices) );
127 }
128 assessment.index = !!paper ? paper.inputs.length : 0;
129 Vue.nextTick(libsRefresh);
130 this.stage = 2;
131 };
132 if (assessment.mode == "open")
133 return initializeStage2();
134 $.ajax("/start/assessment", {
135 method: "GET",
136 data: {
137 number: this.student.number,
138 aid: assessment._id
139 },
140 dataType: "json",
141 success: s => {
142 if (!!s.errmsg)
143 return this.warning(s.errmsg);
144 if (!!s.paper)
145 {
146 // Resuming: receive stored answers + startTime
147 this.student.password = s.paper.password;
148 this.inputs = s.paper.inputs.map( inp => { return inp.input; });
149 }
150 else
151 {
152 this.student.password = s.password;
153 // Got password: students answers locked to this page until potential teacher
154 // action (power failure, computer down, ...)
155 }
156 socket = io.connect("/" + assessment.name, {
157 query: "number=" + this.student.number + "&password=" + this.password
158 });
159 socket.on(message.allAnswers, this.setAnswers);
160 initializeStage2(s.questions, s.paper);
161 },
162 });
163 },
164 // stage 2
165 runTimer: function() {
166 if (assessment.time <= 0)
167 return;
168 let self = this;
169 setInterval( function() {
170 self.remainingTime--;
171 if (self.remainingTime <= 0 || self.stage >= 4)
172 self.endAssessment();
173 clearInterval(this);
174 }, 1000);
175 },
176 // stage 2
177 // TODO: currentIndex ? click: () => this.sendAnswer(assessment.indices[assessment.index]),
178 // De même, cette condition sur le display d'une question doit remonter (résumée dans 'index' property) :
179 // à faire par ici : "hide": this.stage == 2 && assessment.display == 'one' && assessment.indices[assessment.index] != i,
180 sendAnswer: function(realIndex) {
181 let gotoNext = () => {
182 if (assessment.index == assessment.questions.length - 1)
183 this.$emit("gameover");
184 else
185 assessment.index++;
186 this.$forceUpdate(); //TODO: shouldn't be required
187 };
188 if (assessment.mode == "open")
189 return gotoNext(); //only local
190 let answerData = {
191 aid: assessment._id,
192 answer: JSON.stringify({
193 index:realIndex.toString(),
194 input:this.inputs[realIndex]
195 .map( (tf,i) => { return {val:tf,idx:i}; } )
196 .filter( item => { return item.val; })
197 .map( item => { return item.idx; })
198 }),
199 number: this.student.number,
200 password: this.student.password,
201 };
202 $.ajax("/send/answer", {
203 method: "GET",
204 data: answerData,
205 dataType: "json",
206 success: ret => {
207 if (!!ret.errmsg)
208 return this.$emit("warning", ret.errmsg);
209 else
210 gotoNext();
211 socket.emit(message.newAnswer, answerData);
212 },
213 });
214 },
215 // stage 2 --> 3 (or 4)
216 // from a message by statements component, or time over
217 endAssessment: function() {
218 // Set endTime, destroy password
219 $("#leftButton, #rightButton").show();
220 if (assessment.mode == "open")
221 {
222 this.stage = 4;
223 return;
224 }
225 $.ajax("/end/assessment", {
226 method: "GET",
227 data: {
228 aid: assessment._id,
229 number: this.student.number,
230 password: this.student.password,
231 },
232 dataType: "json",
233 success: ret => {
234 if (!!ret.errmsg)
235 return this.warning(ret.errmsg);
236 assessment.conclusion = ret.conclusion;
237 this.stage = 3;
238 delete this.student["password"]; //unable to send new answers now
239 socket.disconnect();
240 socket = null;
241 },
242 });
243 },
244 // stage 3 --> 4 (on socket message "feedback")
245 setAnswers: function(answers) {
246 for (let i=0; i<answers.length; i++)
247 assessment.questions[i].answer = answers[i];
248 this.stage = 4;
249 },
250 },
251 });