1 // TODO: if display == "all", les envois devraient être non définitifs (possibilité de corriger)
2 // Et, blur sur une (sous-)question devrait envoyer la version courante de la sous-question
4 let socket
= null; //monitor answers in real time
6 if (assessment
.mode
== "secure" && !checkWindowSize())
7 document
.location
.href
= "/fullscreen";
9 function checkWindowSize()
11 // NOTE: temporarily accept smartphone (security hole: pretend being a smartphone on desktop browser...)
12 if (navigator
.userAgent
.match(/(iPhone|iPod|iPad|Android|BlackBerry)/))
14 // 3 is arbitrary, but a small tolerance is required (e.g. in Firefox)
15 return window
.innerWidth
>= screen
.width
-3 && window
.innerHeight
>= screen
.height
-3;
21 assessment: assessment
,
22 inputs: [ ], //student's answers
23 student: { }, //filled later
24 // Stage 0: unauthenticated (number),
25 // 1: authenticated (got a name, unvalidated)
26 // 2: locked: password set, exam started
29 stage: assessment
.mode
!= "open" ? 0 : 1,
30 remainingTime: 0, //global, in seconds
35 props: ['assessment','inputs','student','stage'],
36 // TODO: general render function for nested exercises
37 // TODO: with answer if stage==4 : class "wrong" if ticked AND stage==4 AND received answers
38 // class "right" if stage == 4 AND received answers (background-color: red / green)
39 // There should be a questions navigator below, or next (visible if display=='all')
40 // Full questions tree is rendered, but some parts hidden depending on display settings
43 let questions
= (assessment
.questions
|| [ ]).map( (q
,i
) => {
44 let questionContent
= [ ];
58 let optionsOrder
= _
.range(q
.options
.length
);
60 optionsOrder
= _
.shuffle(optionsOrder
);
62 optionsOrder
.forEach( idx
=> {
69 checked: this.inputs
.length
> 0 && this.inputs
[i
][idx
],
72 id: this.inputId(i
,idx
),
76 change: e
=> { this.inputs
[i
][idx
] = e
.target
.checked
; },
86 innerHTML: q
.options
[idx
],
89 "for": this.inputId(i
,idx
),
100 choiceCorrect: this.stage
== 4 && assessment
.questions
[i
].answer
.includes(idx
),
101 choiceWrong: this.stage
== 4 && this.inputs
[i
][idx
] && !assessment
.questions
[i
].answer
.includes(idx
),
108 questionContent
.push(
124 "hide": this.stage
== 2 && assessment
.display
== 'one' && assessment
.indices
[assessment
.index
] != i
,
137 "waves-effect": true,
143 "margin-left": "auto",
144 "margin-right": "auto",
147 click: () => this.sendAnswer(assessment
.indices
[assessment
.index
]),
164 mounted: function() {
165 if (assessment
.mode
!= "secure")
167 window
.addEventListener("keydown", e
=> {
168 // (Try to) Ignore F11 + F12 (avoid accidental window resize)
169 // NOTE: in Chromium at least, exiting fullscreen mode with F11 cannot be prevented.
170 // Workaround: disable key at higher level. Possible xbindkey config:
174 if ([122,123].includes(e
.keyCode
))
177 window
.addEventListener("blur", () => {
178 this.trySendCurrentAnswer();
179 document
.location
.href
= "/noblur";
181 window
.addEventListener("resize", e
=> {
182 this.trySendCurrentAnswer();
183 document
.location
.href
= "/fullscreen";
187 inputId: function(i
,j
) {
188 return "q" + i
+ "_" + "input" + j
;
190 trySendCurrentAnswer: function() {
192 this.sendAnswer(assessment
.indices
[assessment
.index
]);
195 sendAnswer: function(realIndex
) {
196 let gotoNext
= () => {
197 if (assessment
.index
== assessment
.questions
.length
- 1)
198 this.$emit("gameover");
201 this.$forceUpdate(); //TODO: shouldn't be required
203 if (assessment
.mode
== "open")
204 return gotoNext(); //only local
207 answer: JSON
.stringify({
208 index:realIndex
.toString(),
209 input:this.inputs
[realIndex
]
210 .map( (tf
,i
) => { return {val:tf
,idx:i
}; } )
211 .filter( item
=> { return item
.val
; })
212 .map( item
=> { return item
.idx
; })
214 number: this.student
.number
,
215 password: this.student
.password
,
217 $.ajax("/send/answer", {
223 return this.$emit("warning", ret
.errmsg
);
226 //socket.emit(message.newAnswer, answer);
234 countdown: function() {
235 let seconds
= this.remainingTime
% 60;
236 let minutes
= Math
.floor(this.remainingTime
/ 60);
237 return this.padWithZero(minutes
) + ":" + this.padWithZero(seconds
);
240 mounted: function() {
244 // In case of AJAX errors
245 warning: function(message
) {
246 this.warnMsg
= message
;
247 $("#warning").modal("open");
249 padWithZero: function(x
) {
255 getStudent: function(cb
) {
256 $.ajax("/get/student", {
259 number: this.student
.number
,
265 return this.warning(s
.errmsg
);
267 this.student
= s
.student
;
268 Vue
.nextTick( () => { Materialize
.updateTextFields(); });
275 cancelStudent: function() {
278 // stage 1 --> 2 (get all questions, set password)
279 startAssessment: function() {
280 let initializeStage2
= (questions
,paper
) => {
281 $("#leftButton, #rightButton").hide();
282 if (assessment
.time
> 0)
284 const deltaTime
= !!paper
? Date
.now() - paper
.startTime : 0;
285 this.remainingTime
= assessment
.time
* 60 - Math
.round(deltaTime
/ 1000);
288 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
290 assessment
.questions
= questions
;
291 for (let q
of assessment
.questions
)
292 this.inputs
.push( _(q
.options
.length
).times( _
.constant(false) ) );
295 assessment
.indices
= assessment
.fixed
296 ? _
.range(assessment
.questions
.length
)
297 : _
.shuffle( _
.range(assessment
.questions
.length
) );
302 let indices
= paper
.inputs
.map( input
=> { return input
.index
; });
303 let remainingIndices
= _
.difference(_
.range(assessment
.questions
.length
), indices
);
304 assessment
.indices
= indices
.concat( _
.shuffle(remainingIndices
) );
306 assessment
.index
= !!paper
? paper
.inputs
.length : 0;
308 Vue
.nextTick( () => {
309 // Run Prism + MathJax on questions text
310 $("#statements").find("code[class^=language-]").each( (i
,elem
) => {
311 Prism
.highlightElement(elem
);
313 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"statements"]);
316 if (assessment
.mode
== "open")
317 return initializeStage2();
318 $.ajax("/start/assessment", {
321 number: this.student
.number
,
327 return this.warning(s
.errmsg
);
330 // Resuming: receive stored answers + startTime
331 this.student
.password
= s
.paper
.password
;
332 this.inputs
= s
.paper
.inputs
.map( inp
=> { return inp
.input
; });
336 this.student
.password
= s
.password
;
337 // Got password: students answers locked to this page until potential teacher
338 // action (power failure, computer down, ...)
340 // TODO: password also exchanged by sockets to check identity
341 //socket = io.connect("/" + assessment.name, {
342 // query: "number=" + this.student.number + "&password=" + this.password
344 //socket.on(message.allAnswers, this.setAnswers);
345 //socket.on("disconnect", () => { }); //TODO: notify monitor (highlight red), redirect
346 initializeStage2(s
.questions
, s
.paper
);
351 runTimer: function() {
352 if (assessment
.time
<= 0)
355 setInterval( function() {
356 self
.remainingTime
--;
357 if (self
.remainingTime
<= 0 || self
.stage
>= 4)
358 self
.endAssessment();
362 // stage 2 --> 3 (or 4)
363 // from a message by statements component, or time over
364 endAssessment: function() {
365 // Set endTime, destroy password
366 $("#leftButton, #rightButton").show();
367 if (assessment
.mode
== "open")
372 $.ajax("/end/assessment", {
376 number: this.student
.number
,
377 password: this.student
.password
,
382 return this.warning(ret
.errmsg
);
383 assessment
.conclusion
= ret
.conclusion
;
385 delete this.student
["password"]; //unable to send new answers now
386 //socket.disconnect();
391 // stage 3 --> 4 (on socket message "feedback")
392 setAnswers: function(answers
) {
393 for (let i
=0; i
<answers
.length
; i
++)
394 assessment
.questions
[i
].answer
= answers
[i
];