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 // There should be a questions navigator below, or next (visible if display=='all')
47 // Full questions tree is rendered, but some parts hidden depending on display settings
50 let questions
= (assessment
.questions
|| [ ]).map( (q
,i
) => {
51 let questionContent
= [ ];
65 let optionsOrder
= _
.range(q
.options
.length
);
67 optionsOrder
= _
.shuffle(optionsOrder
);
69 optionsOrder
.forEach( idx
=> {
76 checked: this.inputs
.length
> 0 && this.inputs
[i
][idx
],
79 id: this.inputId(i
,idx
),
83 change: e
=> { this.inputs
[i
][idx
] = e
.target
.checked
; },
93 innerHTML: q
.options
[idx
],
96 "for": this.inputId(i
,idx
),
107 choiceCorrect: this.stage
== 4 && assessment
.questions
[i
].answer
.includes(idx
),
108 choiceWrong: this.stage
== 4 && this.inputs
[i
][idx
] && !assessment
.questions
[i
].answer
.includes(idx
),
115 questionContent
.push(
131 "hide": this.stage
== 2 && assessment
.display
== 'one' && assessment
.indices
[assessment
.index
] != i
,
144 "waves-effect": true,
150 "margin-left": "auto",
151 "margin-right": "auto",
154 click: () => this.sendAnswer(assessment
.indices
[assessment
.index
]),
171 mounted: function() {
172 if (assessment
.mode
!= "secure")
174 window
.addEventListener("keydown", e
=> {
175 // Ignore F12 (avoid accidental window resize due to devtools)
176 // NOTE: in Chromium at least, fullscreen mode exit with F11 cannot be prevented.
177 // Workaround: disable key at higher level. Possible xbindkey config:
181 if (e
.keyCode
== 123)
184 window
.addEventListener("blur", () => {
185 this.trySendCurrentAnswer();
186 document
.location
.href
= "/noblur";
188 window
.addEventListener("resize", e
=> {
189 this.trySendCurrentAnswer();
190 document
.location
.href
= "/fullscreen";
193 updated: function() {
194 libsRefresh(); //TODO: shouldn't be required: "MathJax" strings on start and assign them to assessment.questions. ...
197 inputId: function(i
,j
) {
198 return "q" + i
+ "_" + "input" + j
;
200 trySendCurrentAnswer: function() {
202 this.sendAnswer(assessment
.indices
[assessment
.index
]);
205 sendAnswer: function(realIndex
) {
206 let gotoNext
= () => {
207 if (assessment
.index
== assessment
.questions
.length
- 1)
208 this.$emit("gameover");
211 this.$forceUpdate(); //TODO: shouldn't be required
213 if (assessment
.mode
== "open")
214 return gotoNext(); //only local
217 answer: JSON
.stringify({
218 index:realIndex
.toString(),
219 input:this.inputs
[realIndex
]
220 .map( (tf
,i
) => { return {val:tf
,idx:i
}; } )
221 .filter( item
=> { return item
.val
; })
222 .map( item
=> { return item
.idx
; })
224 number: this.student
.number
,
225 password: this.student
.password
,
227 $.ajax("/send/answer", {
233 return this.$emit("warning", ret
.errmsg
);
236 socket
.emit(message
.newAnswer
, answerData
);
244 countdown: function() {
245 let seconds
= this.remainingTime
% 60;
246 let minutes
= Math
.floor(this.remainingTime
/ 60);
247 return this.padWithZero(minutes
) + ":" + this.padWithZero(seconds
);
250 mounted: function() {
254 // In case of AJAX errors
255 warning: function(message
) {
256 this.warnMsg
= message
;
257 $("#warning").modal("open");
259 padWithZero: function(x
) {
265 getStudent: function(cb
) {
266 $.ajax("/get/student", {
269 number: this.student
.number
,
275 return this.warning(s
.errmsg
);
277 this.student
= s
.student
;
278 Vue
.nextTick( () => { Materialize
.updateTextFields(); });
285 cancelStudent: function() {
288 // stage 1 --> 2 (get all questions, set password)
289 startAssessment: function() {
290 let initializeStage2
= (questions
,paper
) => {
291 $("#leftButton, #rightButton").hide();
292 if (assessment
.time
> 0)
294 const deltaTime
= !!paper
? Date
.now() - paper
.startTime : 0;
295 this.remainingTime
= assessment
.time
* 60 - Math
.round(deltaTime
/ 1000);
298 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
300 assessment
.questions
= questions
;
301 for (let q
of assessment
.questions
)
302 this.inputs
.push( _(q
.options
.length
).times( _
.constant(false) ) );
305 assessment
.indices
= assessment
.fixed
306 ? _
.range(assessment
.questions
.length
)
307 : _
.shuffle( _
.range(assessment
.questions
.length
) );
312 let indices
= paper
.inputs
.map( input
=> { return input
.index
; });
313 let remainingIndices
= _
.difference( _
.range(assessment
.questions
.length
).map(String
), indices
);
314 assessment
.indices
= indices
.concat( _
.shuffle(remainingIndices
) );
316 assessment
.index
= !!paper
? paper
.inputs
.length : 0;
317 Vue
.nextTick(libsRefresh
);
320 if (assessment
.mode
== "open")
321 return initializeStage2();
322 $.ajax("/start/assessment", {
325 number: this.student
.number
,
331 return this.warning(s
.errmsg
);
334 // Resuming: receive stored answers + startTime
335 this.student
.password
= s
.paper
.password
;
336 this.inputs
= s
.paper
.inputs
.map( inp
=> { return inp
.input
; });
340 this.student
.password
= s
.password
;
341 // Got password: students answers locked to this page until potential teacher
342 // action (power failure, computer down, ...)
344 socket
= io
.connect("/" + assessment
.name
, {
345 query: "number=" + this.student
.number
+ "&password=" + this.password
347 socket
.on(message
.allAnswers
, this.setAnswers
);
348 initializeStage2(s
.questions
, s
.paper
);
353 runTimer: function() {
354 if (assessment
.time
<= 0)
357 setInterval( function() {
358 self
.remainingTime
--;
359 if (self
.remainingTime
<= 0 || self
.stage
>= 4)
360 self
.endAssessment();
364 // stage 2 --> 3 (or 4)
365 // from a message by statements component, or time over
366 endAssessment: function() {
367 // Set endTime, destroy password
368 $("#leftButton, #rightButton").show();
369 if (assessment
.mode
== "open")
374 $.ajax("/end/assessment", {
378 number: this.student
.number
,
379 password: this.student
.password
,
384 return this.warning(ret
.errmsg
);
385 assessment
.conclusion
= ret
.conclusion
;
387 delete this.student
["password"]; //unable to send new answers now
393 // stage 3 --> 4 (on socket message "feedback")
394 setAnswers: function(answers
) {
395 for (let i
=0; i
<answers
.length
; i
++)
396 assessment
.questions
[i
].answer
= answers
[i
];