1 let socket
= null; //monitor answers in real time
3 if (assessment
.mode
== "secure" && !checkWindowSize())
4 document
.location
.href
= "/fullscreen";
6 function checkWindowSize()
8 // NOTE: temporarily accept smartphone (security hole: pretend being a smartphone on desktop browser...)
9 if (navigator
.userAgent
.match(/(iPhone|iPod|iPad|Android|BlackBerry)/))
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;
18 assessment: assessment
,
19 answers: { }, //filled later with answering parameters
20 student: { }, //filled later (name, password)
21 // Stage 0: unauthenticated (number),
22 // 1: authenticated (got a name, unvalidated)
23 // 2: locked: password set, exam started
26 stage: assessment
.mode
!= "open" ? 0 : 1,
27 remainingTime: 0, //global, in seconds
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
);
36 showAnswers: function() {
37 return this.stage
== 4;
42 if (assessment
.mode
!= "open")
44 window
.addEventListener("keydown", e
=> {
45 // Ignore F12 (avoid accidental window resize due to devtools)
46 // NOTE: in Chromium at least, fullscreen mode exit with F11 cannot be prevented.
47 // Workaround: disable key at higher level. Possible xbindkey config:
55 window
.addEventListener("blur", () => {
58 if (assessment
.mode
== "secure")
60 this.trySendCurrentAnswer();
61 document
.location
.href
= "/noblur";
63 else if (assessment
.mode
== "exam")
64 socket
.emit(message
.studentBlur
, {number:this.student
.number
});
66 if (assessment
.mode
== "exam")
68 window
.addEventListener("focus", () => {
71 socket
.emit(message
.studentFocus
, {number:this.student
.number
});
74 window
.addEventListener("resize", e
=> {
77 if (assessment
.mode
== "secure")
79 this.trySendCurrentAnswer();
80 document
.location
.href
= "/fullscreen";
82 else if (assessment
.mode
== "exam")
84 if (checkWindowSize())
85 socket
.emit(message
.studentFullscreen
, {number:this.student
.number
});
87 socket
.emit(message
.studentResize
, {number:this.student
.number
});
92 // In case of AJAX errors
93 showWarning: function(message
) {
94 this.warnMsg
= message
;
95 $("#warning").modal("open");
97 padWithZero: function(x
) {
102 trySendCurrentAnswer: function() {
107 getStudent: function(cb
) {
108 $.ajax("/get/student", {
111 number: this.student
.number
,
117 return this.showWarning(s
.errmsg
);
119 this.student
= s
.student
;
120 Vue
.nextTick( () => { Materialize
.updateTextFields(); });
127 cancelStudent: function() {
130 // stage 1 --> 2 (get all questions, set password)
131 startAssessment: function() {
132 let initializeStage2
= (questions
,paper
) => {
133 $("#leftButton, #rightButton").hide();
134 if (assessment
.time
> 0)
137 // TODO: distinguish total exam time AND question time
139 const deltaTime
= !!paper
? Date
.now() - paper
.startTime : 0;
140 this.remainingTime
= assessment
.time
* 60 - Math
.round(deltaTime
/ 1000);
143 // Initialize structured answer(s) based on questions type and nesting (TODO: more general)
145 assessment
.questions
= questions
;
146 this.answers
.inputs
= [ ];
147 for (let q
of assessment
.questions
)
148 this.answers
.inputs
.push( _(q
.options
.length
).times( _
.constant(false) ) );
151 this.answers
.indices
= assessment
.fixed
152 ? _
.range(assessment
.questions
.length
)
153 : _
.shuffle( _
.range(assessment
.questions
.length
) );
158 let indices
= paper
.inputs
.map( input
=> { return input
.index
; });
159 let remainingIndices
= _
.difference( _
.range(assessment
.questions
.length
).map(String
), indices
);
160 this.answers
.indices
= indices
.concat( _
.shuffle(remainingIndices
) );
162 this.answers
.index
= !!paper
? paper
.inputs
.length : 0;
163 this.answers
.displayAll
= assessment
.display
== "all";
164 this.answers
.showSolution
= false;
167 if (assessment
.mode
== "open")
168 return initializeStage2();
169 $.ajax("/start/assessment", {
172 number: this.student
.number
,
178 return this.showWarning(s
.errmsg
);
181 // Resuming: receive stored answers + startTime
182 this.student
.password
= s
.paper
.password
;
183 this.answers
.inputs
= s
.paper
.inputs
.map( inp
=> { return inp
.input
; });
187 this.student
.password
= s
.password
;
188 // Got password: students answers locked to this page until potential teacher
189 // action (power failure, computer down, ...)
191 socket
= io
.connect("/", {
192 query: "aid=" + assessment
._id
+ "&number=" + this.student
.number
+ "&password=" + this.student
.password
194 socket
.on(message
.allAnswers
, this.setAnswers
);
195 initializeStage2(s
.questions
, s
.paper
);
200 runTimer: function() {
201 if (assessment
.time
<= 0)
204 setInterval( function() {
205 self
.remainingTime
--;
206 if (self
.remainingTime
<= 0)
209 self
.endAssessment();
215 sendOneAnswer: function() {
216 const realIndex
= this.answers
.indices
[this.answers
.index
];
217 let gotoNext
= () => {
218 if (this.answers
.index
== assessment
.questions
.length
- 1)
219 this.endAssessment();
221 this.answers
.index
++;
222 this.$children
[0].$forceUpdate(); //TODO: bad HACK, and shouldn't be required...
224 if (assessment
.mode
== "open")
225 return gotoNext(); //only local
228 answer: JSON
.stringify({
229 index: realIndex
.toString(),
230 input: this.answers
.inputs
[realIndex
]
231 .map( (tf
,i
) => { return {val:tf
,idx:i
}; } )
232 .filter( item
=> { return item
.val
; })
233 .map( item
=> { return item
.idx
; })
235 number: this.student
.number
,
236 password: this.student
.password
,
238 $.ajax("/send/answer", {
244 return this.showWarning(ret
.errmsg
);
246 socket
.emit(message
.newAnswer
, answerData
);
250 // TODO: I don't like that + sending should not be definitive in exam mode with display = all
251 sendAnswer: function() {
252 if (assessment
.display
== "one")
253 this.sendOneAnswer();
255 assessment
.questions
.forEach(this.sendOneAnswer
);
257 // stage 2 --> 3 (or 4)
258 // from a message by statements component, or time over
259 endAssessment: function() {
260 // Set endTime, destroy password
261 $("#leftButton, #rightButton").show();
262 if (assessment
.mode
== "open")
265 this.answers
.showSolution
= true;
266 this.answers
.displayAll
= true;
269 $.ajax("/end/assessment", {
273 number: this.student
.number
,
274 password: this.student
.password
,
279 return this.showWarning(ret
.errmsg
);
281 delete this.student
["password"]; //unable to send new answers now
285 // stage 3 --> 4 (on socket message "feedback")
286 setAnswers: function(m
) {
287 const answers
= JSON
.parse(m
.answers
);
288 for (let i
=0; i
<answers
.length
; i
++)
289 assessment
.questions
[i
].answer
= answers
[i
];
290 this.answers
.showSolution
= true;
291 this.answers
.displayAll
= true;