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 return window
.innerWidth
== screen
.width
&& window
.innerHeight
== screen
.height
;
20 assessment: assessment
,
21 inputs: [ ], //student's answers
22 student: { }, //filled later
23 // Stage 0: unauthenticated (number),
24 // 1: authenticated (got a name, unvalidated)
25 // 2: locked: password set, exam started
28 stage: assessment
.mode
!= "open" ? 0 : 1,
29 remainingTime: 0, //global, in seconds
33 props: ['assessment','inputs','student','stage'],
36 index: 0, //current question index in assessment.indices
39 // TODO: general render function for nested exercises
40 // TODO: with answer if stage==4 : class "wrong" if ticked AND stage==4 AND received answers
41 // class "right" if stage == 4 AND received answers (background-color: red / green)
42 // There should be a questions navigator below, or next (visible if display=='all')
43 // Full questions tree is rendered, but some parts hidden depending on display settings
46 let questions
= assessment
.questions
.map( (q
,i
) => {
47 let questionContent
= [ ];
61 let optionsOrder
= _
.range(q
.options
.length
);
63 optionsOrder
= _
.shuffle(optionsOrder
);
65 optionsOrder
.forEach( idx
=> {
72 checked: this.inputs
[i
][idx
],
75 id: this.inputId(i
,idx
),
79 change: e
=> { this.inputs
[i
][idx
] = e
.target
.checked
; },
89 innerHTML: q
.options
[idx
],
92 "for": this.inputId(i
,idx
),
103 choiceCorrect: this.stage
== 4 && assessment
.questions
[i
].answer
.includes(idx
),
104 choiceWrong: this.stage
== 4 && this.inputs
[i
][idx
] && !assessment
.questions
[i
].answer
.includes(idx
),
111 questionContent
.push(
127 "hide": this.stage
== 2 && assessment
.display
== 'one' && assessment
.indices
[this.index
] != i
,
135 // TODO: one button per question
141 "waves-effect": true,
147 "margin-left": "auto",
148 "margin-right": "auto",
151 click: () => this.sendAnswer(assessment
.indices
[this.index
]),
168 mounted: function() {
169 if (assessment
.mode
!= "secure")
171 window
.addEventListener("keydown", e
=> {
172 // (Try to) Ignore F11 + F12 (avoid accidental window resize)
173 // NOTE: in Chromium at least, exiting fullscreen mode with F11 cannot be prevented.
174 // Workaround: disable key at higher level. Possible xbindkey config:
178 if ([122,123].includes(e
.keyCode
))
181 window
.addEventListener("blur", () => {
182 this.trySendCurrentAnswer();
183 document
.location
.href
= "/noblur";
185 window
.addEventListener("resize", e
=> {
186 this.trySendCurrentAnswer();
187 document
.location
.href
= "/fullscreen";
191 inputId: function(i
,j
) {
192 return "q" + i
+ "_" + "input" + j
;
194 trySendCurrentAnswer: function() {
196 this.sendAnswer(assessment
.indices
[this.index
]);
199 sendAnswer: function(realIndex
) {
200 if (this.index
== assessment
.questions
.length
- 1)
201 this.$emit("gameover");
204 if (assessment
.mode
== "open")
208 answer: JSON
.stringify({
209 index:realIndex
.toString(),
210 input:this.inputs
[realIndex
]
211 .map( (tf
,i
) => { return {val:tf
,idx:i
}; } )
212 .filter( item
=> { return item
.val
; })
213 .map( item
=> { return item
.idx
; })
215 number: this.student
.number
,
216 password: this.student
.password
,
218 $.ajax("/send/answer", {
224 return alert(ret
.errmsg
);
225 //socket.emit(message.newAnswer, answer);
233 countdown: function() {
234 let seconds
= this.remainingTime
% 60;
235 let minutes
= Math
.floor(this.remainingTime
/ 60);
236 return this.padWithZero(minutes
) + ":" + this.padWithZero(seconds
);
240 padWithZero: function(x
) {
246 getStudent: function(cb
) {
247 $.ajax("/get/student", {
250 number: this.student
.number
,
256 return alert(s
.errmsg
);
258 this.student
= s
.student
;
259 Vue
.nextTick( () => { Materialize
.updateTextFields(); });
266 cancelStudent: function() {
269 // stage 1 --> 2 (get all questions, set password)
270 startAssessment: function() {
271 let initializeStage2
= questions
=> {
272 $("#leftButton, #rightButton").hide();
273 if (assessment
.time
> 0)
275 this.remainingTime
= assessment
.time
* 60;
278 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
280 assessment
.questions
= questions
;
281 for (let q
of assessment
.questions
)
282 this.inputs
.push( _(q
.options
.length
).times( _
.constant(false) ) );
283 assessment
.indices
= assessment
.fixed
284 ? _
.range(assessment
.questions
.length
)
285 : _
.shuffle( _
.range(assessment
.questions
.length
) );
287 Vue
.nextTick( () => {
288 // Run Prism + MathJax on questions text
289 $("#statements").find("code[class^=language-]").each( (i
,elem
) => {
290 Prism
.highlightElement(elem
);
292 MathJax
.Hub
.Queue(["Typeset",MathJax
.Hub
,"statements"]);
295 if (assessment
.mode
== "open")
296 return initializeStage2();
297 // TODO: if existing password cookie: get stored answers (papers[number cookie]), inject (inputs), set index+indices
298 // (instead of following ajax call)
299 $.ajax("/start/assessment", {
302 number: this.student
.number
,
308 return alert(s
.errmsg
);
309 this.student
.password
= s
.password
;
310 // Got password: students answers locked to this page until potential teacher
311 // action (power failure, computer down, ...)
312 // TODO: set password cookie
313 // TODO: password also exchanged by sockets to check identity
314 //socket = io.connect("/" + assessment.name, {
315 // query: "number=" + this.student.number + "&password=" + this.password
317 //socket.on(message.allAnswers, this.setAnswers);
318 //socket.on("disconnect", () => { }); //TODO: notify monitor (highlight red), redirect
319 initializeStage2(s
.questions
);
324 runTimer: function() {
325 if (assessment
.time
<= 0)
328 setInterval( function() {
329 self
.remainingTime
--;
330 if (self
.remainingTime
<= 0 || self
.stage
>= 4)
331 self
.endAssessment();
335 // stage 2 --> 3 (or 4)
336 // from a message by statements component, or time over
337 endAssessment: function() {
338 // Set endTime, destroy password
339 $("#leftButton, #rightButton").show();
340 if (assessment
.mode
== "open")
345 $.ajax("/end/assessment", {
349 number: this.student
.number
,
350 password: this.student
.password
,
355 return alert(ret
.errmsg
);
356 assessment
.conclusion
= ret
.conclusion
;
358 delete this.student
["password"]; //unable to send new answers now
359 //socket.disconnect();
364 // stage 3 --> 4 (on socket message "feedback")
365 setAnswers: function(answers
) {
366 for (let i
=0; i
<answers
.length
; i
++)
367 assessment
.questions
[i
].answer
= answers
[i
];