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;
18 function libsRefresh()
20 // Run Prism + MathJax on questions text
21 $("#statements").find("code[class^=language-]").each( (i
,elem
) => {
22 Prism
.highlightElement(elem
);
24 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"statements"]);
30 assessment: assessment
,
31 inputs: [ ], //student's answers
32 student: { }, //filled later
33 // Stage 0: unauthenticated (number),
34 // 1: authenticated (got a name, unvalidated)
35 // 2: locked: password set, exam started
38 stage: assessment
.mode
!= "open" ? 0 : 1,
39 remainingTime: 0, //global, in seconds
44 props: ['assessment','inputs','student','stage'],
45 // TODO: general render function for nested exercises
46 // TODO: with answer if stage==4 : class "wrong" if ticked AND stage==4 AND received answers
47 // class "right" if stage == 4 AND received answers (background-color: red / green)
48 // There should be a questions navigator below, or next (visible if display=='all')
49 // Full questions tree is rendered, but some parts hidden depending on display settings
52 let questions
= (assessment
.questions
|| [ ]).map( (q
,i
) => {
53 let questionContent
= [ ];
67 let optionsOrder
= _
.range(q
.options
.length
);
69 optionsOrder
= _
.shuffle(optionsOrder
);
71 optionsOrder
.forEach( idx
=> {
78 checked: this.inputs
.length
> 0 && this.inputs
[i
][idx
],
81 id: this.inputId(i
,idx
),
85 change: e
=> { this.inputs
[i
][idx
] = e
.target
.checked
; },
95 innerHTML: q
.options
[idx
],
98 "for": this.inputId(i
,idx
),
109 choiceCorrect: this.stage
== 4 && assessment
.questions
[i
].answer
.includes(idx
),
110 choiceWrong: this.stage
== 4 && this.inputs
[i
][idx
] && !assessment
.questions
[i
].answer
.includes(idx
),
117 questionContent
.push(
133 "hide": this.stage
== 2 && assessment
.display
== 'one' && assessment
.indices
[assessment
.index
] != i
,
146 "waves-effect": true,
152 "margin-left": "auto",
153 "margin-right": "auto",
156 click: () => this.sendAnswer(assessment
.indices
[assessment
.index
]),
173 mounted: function() {
174 if (assessment
.mode
!= "secure")
176 window
.addEventListener("keydown", e
=> {
177 // (Try to) Ignore F11 + F12 (avoid accidental window resize)
178 // NOTE: in Chromium at least, exiting fullscreen mode with F11 cannot be prevented.
179 // Workaround: disable key at higher level. Possible xbindkey config:
183 if ([122,123].includes(e
.keyCode
))
186 window
.addEventListener("blur", () => {
187 this.trySendCurrentAnswer();
188 document
.location
.href
= "/noblur";
190 window
.addEventListener("resize", e
=> {
191 this.trySendCurrentAnswer();
192 document
.location
.href
= "/fullscreen";
195 updated: function() {
196 libsRefresh(); //TODO: shouldn't be required: "MathJax" strings on start and assign them to assessment.questions. ...
199 inputId: function(i
,j
) {
200 return "q" + i
+ "_" + "input" + j
;
202 trySendCurrentAnswer: function() {
204 this.sendAnswer(assessment
.indices
[assessment
.index
]);
207 sendAnswer: function(realIndex
) {
208 let gotoNext
= () => {
209 if (assessment
.index
== assessment
.questions
.length
- 1)
210 this.$emit("gameover");
213 this.$forceUpdate(); //TODO: shouldn't be required
215 if (assessment
.mode
== "open")
216 return gotoNext(); //only local
219 answer: JSON
.stringify({
220 index:realIndex
.toString(),
221 input:this.inputs
[realIndex
]
222 .map( (tf
,i
) => { return {val:tf
,idx:i
}; } )
223 .filter( item
=> { return item
.val
; })
224 .map( item
=> { return item
.idx
; })
226 number: this.student
.number
,
227 password: this.student
.password
,
229 $.ajax("/send/answer", {
235 return this.$emit("warning", ret
.errmsg
);
238 //socket.emit(message.newAnswer, answer);
246 countdown: function() {
247 let seconds
= this.remainingTime
% 60;
248 let minutes
= Math
.floor(this.remainingTime
/ 60);
249 return this.padWithZero(minutes
) + ":" + this.padWithZero(seconds
);
252 mounted: function() {
256 // In case of AJAX errors
257 warning: function(message
) {
258 this.warnMsg
= message
;
259 $("#warning").modal("open");
261 padWithZero: function(x
) {
267 getStudent: function(cb
) {
268 $.ajax("/get/student", {
271 number: this.student
.number
,
277 return this.warning(s
.errmsg
);
279 this.student
= s
.student
;
280 Vue
.nextTick( () => { Materialize
.updateTextFields(); });
287 cancelStudent: function() {
290 // stage 1 --> 2 (get all questions, set password)
291 startAssessment: function() {
292 let initializeStage2
= (questions
,paper
) => {
293 $("#leftButton, #rightButton").hide();
294 if (assessment
.time
> 0)
296 const deltaTime
= !!paper
? Date
.now() - paper
.startTime : 0;
297 this.remainingTime
= assessment
.time
* 60 - Math
.round(deltaTime
/ 1000);
300 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
302 assessment
.questions
= questions
;
303 for (let q
of assessment
.questions
)
304 this.inputs
.push( _(q
.options
.length
).times( _
.constant(false) ) );
307 assessment
.indices
= assessment
.fixed
308 ? _
.range(assessment
.questions
.length
)
309 : _
.shuffle( _
.range(assessment
.questions
.length
) );
314 let indices
= paper
.inputs
.map( input
=> { return input
.index
; });
315 let remainingIndices
= _
.difference( _
.range(assessment
.questions
.length
).map(String
), indices
);
316 assessment
.indices
= indices
.concat( _
.shuffle(remainingIndices
) );
318 assessment
.index
= !!paper
? paper
.inputs
.length : 0;
319 Vue
.nextTick(libsRefresh
);
322 if (assessment
.mode
== "open")
323 return initializeStage2();
324 $.ajax("/start/assessment", {
327 number: this.student
.number
,
333 return this.warning(s
.errmsg
);
336 // Resuming: receive stored answers + startTime
337 this.student
.password
= s
.paper
.password
;
338 this.inputs
= s
.paper
.inputs
.map( inp
=> { return inp
.input
; });
342 this.student
.password
= s
.password
;
343 // Got password: students answers locked to this page until potential teacher
344 // action (power failure, computer down, ...)
346 // TODO: password also exchanged by sockets to check identity
347 //socket = io.connect("/" + assessment.name, {
348 // query: "number=" + this.student.number + "&password=" + this.password
350 //socket.on(message.allAnswers, this.setAnswers);
351 //socket.on("disconnect", () => { }); //TODO: notify monitor (highlight red), redirect
352 initializeStage2(s
.questions
, s
.paper
);
357 runTimer: function() {
358 if (assessment
.time
<= 0)
361 setInterval( function() {
362 self
.remainingTime
--;
363 if (self
.remainingTime
<= 0 || self
.stage
>= 4)
364 self
.endAssessment();
368 // stage 2 --> 3 (or 4)
369 // from a message by statements component, or time over
370 endAssessment: function() {
371 // Set endTime, destroy password
372 $("#leftButton, #rightButton").show();
373 if (assessment
.mode
== "open")
378 $.ajax("/end/assessment", {
382 number: this.student
.number
,
383 password: this.student
.password
,
388 return this.warning(ret
.errmsg
);
389 assessment
.conclusion
= ret
.conclusion
;
391 delete this.student
["password"]; //unable to send new answers now
392 //socket.disconnect();
397 // stage 3 --> 4 (on socket message "feedback")
398 setAnswers: function(answers
) {
399 for (let i
=0; i
<answers
.length
; i
++)
400 assessment
.questions
[i
].answer
= answers
[i
];