From e5ec7dead171feebb299430f298e3930864e096d Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Tue, 30 Jan 2018 12:00:41 +0100 Subject: [PATCH] early draft of sockets logic for monitoring --- TODO_assessment_template => TODO | 3 + public/javascripts/assessment.js | 24 ++-- public/javascripts/monitor.js | 221 +++++++++++++++++++++++++++++++ sockets.js | 42 +++--- views/monitor.pug | 25 +++- 5 files changed, 282 insertions(+), 33 deletions(-) rename TODO_assessment_template => TODO (91%) diff --git a/TODO_assessment_template b/TODO similarity index 91% rename from TODO_assessment_template rename to TODO index f44a163..3e6b8f7 100644 --- a/TODO_assessment_template +++ b/TODO @@ -1,3 +1,6 @@ +auto grading + finish erdiag + corr tp1 +----- + TODO: format général TXT: (compilé en JSON) 10 (time) diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js index 91e34fc..c014b5a 100644 --- a/public/javascripts/assessment.js +++ b/public/javascripts/assessment.js @@ -43,8 +43,6 @@ new Vue({ "statements": { props: ['assessment','inputs','student','stage'], // TODO: general render function for nested exercises - // TODO: with answer if stage==4 : class "wrong" if ticked AND stage==4 AND received answers - // class "right" if stage == 4 AND received answers (background-color: red / green) // There should be a questions navigator below, or next (visible if display=='all') // Full questions tree is rendered, but some parts hidden depending on display settings render(h) { @@ -174,13 +172,13 @@ new Vue({ if (assessment.mode != "secure") return; window.addEventListener("keydown", e => { - // (Try to) Ignore F11 + F12 (avoid accidental window resize) - // NOTE: in Chromium at least, exiting fullscreen mode with F11 cannot be prevented. + // Ignore F12 (avoid accidental window resize due to devtools) + // NOTE: in Chromium at least, fullscreen mode exit with F11 cannot be prevented. // Workaround: disable key at higher level. Possible xbindkey config: // "false" // m:0x10 + c:95 // Mod2 + F11 - if ([122,123].includes(e.keyCode)) + if (e.keyCode == 123) e.preventDefault(); }, false); window.addEventListener("blur", () => { @@ -235,7 +233,7 @@ new Vue({ return this.$emit("warning", ret.errmsg); else gotoNext(); - //socket.emit(message.newAnswer, answer); + socket.emit(message.newAnswer, answerData); }, }); }, @@ -343,12 +341,10 @@ new Vue({ // Got password: students answers locked to this page until potential teacher // action (power failure, computer down, ...) } - // TODO: password also exchanged by sockets to check identity - //socket = io.connect("/" + assessment.name, { - // query: "number=" + this.student.number + "&password=" + this.password - //}); - //socket.on(message.allAnswers, this.setAnswers); - //socket.on("disconnect", () => { }); //TODO: notify monitor (highlight red), redirect + socket = io.connect("/" + assessment.name, { + query: "number=" + this.student.number + "&password=" + this.password + }); + socket.on(message.allAnswers, this.setAnswers); initializeStage2(s.questions, s.paper); }, }); @@ -389,8 +385,8 @@ new Vue({ assessment.conclusion = ret.conclusion; this.stage = 3; delete this.student["password"]; //unable to send new answers now - //socket.disconnect(); - //socket = null; + socket.disconnect(); + socket = null; }, }); }, diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js index b58461b..1d32d0c 100644 --- a/public/javascripts/monitor.js +++ b/public/javascripts/monitor.js @@ -9,3 +9,224 @@ // Doit reprendre les données en base si refresh (sinon : sockets) // Also buttons "start exam", "end exam" for logged in teacher + +// TODO: réutiliser le component... trouver un moyen + +let socket = null; //monitor answers in real time + +function libsRefresh() +{ + // Run Prism + MathJax on questions text + $("#statements").find("code[class^=language-]").each( (i,elem) => { + Prism.highlightElement(elem); + }); + MathJax.Hub.Queue(["Typeset",MathJax.Hub,"statements"]); +} + +new Vue({ + el: "#monitor", + data: { + password: "", //from password field + assessment: null, //obtained after authentication + // Stage 0: unauthenticated (password), + // 1: authenticated (password hash validated), start monitoring + stage: 0, + }, + components: { + "statements": { + props: ['assessment','inputs','student','stage'], + // TODO: general render function for nested exercises + // There should be a questions navigator below, or next (visible if display=='all') + // Full questions tree is rendered, but some parts hidden depending on display settings + render(h) { + let self = this; + let questions = (assessment.questions || [ ]).map( (q,i) => { + let questionContent = [ ]; + questionContent.push( + h( + "div", + { + "class": { + wording: true, + }, + domProps: { + innerHTML: q.wording, + }, + } + ) + ); + let optionsOrder = _.range(q.options.length); + if (!q.fixed) + optionsOrder = _.shuffle(optionsOrder); + let optionList = [ ]; + optionsOrder.forEach( idx => { + let option = [ ]; + option.push( + h( + "input", + { + domProps: { + checked: this.inputs.length > 0 && this.inputs[i][idx], + }, + attrs: { + id: this.inputId(i,idx), + type: "checkbox", + }, + on: { + change: e => { this.inputs[i][idx] = e.target.checked; }, + }, + }, + ) + ); + option.push( + h( + "label", + { + domProps: { + innerHTML: q.options[idx], + }, + attrs: { + "for": this.inputId(i,idx), + }, + } + ) + ); + optionList.push( + h( + "div", + { + "class": { + option: true, + choiceCorrect: this.stage == 4 && assessment.questions[i].answer.includes(idx), + choiceWrong: this.stage == 4 && this.inputs[i][idx] && !assessment.questions[i].answer.includes(idx), + }, + }, + option + ) + ); + }); + questionContent.push( + h( + "div", + { + "class": { + optionList: true, + }, + }, + optionList + ) + ); + return h( + "div", + { + "class": { + "question": true, + "hide": this.stage == 2 && assessment.display == 'one' && assessment.indices[assessment.index] != i, + }, + }, + questionContent + ); + }); + if (this.stage == 2) + { + questions.unshift( + h( + "button", + { + "class": { + "waves-effect": true, + "waves-light": true, + "btn": true, + }, + style: { + "display": "block", + "margin-left": "auto", + "margin-right": "auto", + }, + on: { + click: () => this.sendAnswer(assessment.indices[assessment.index]), + }, + }, + "Send" + ) + ); + } + return h( + "div", + { + attrs: { + id: "statements", + }, + }, + questions + ); + }, + mounted: function() { + libsRefresh(); + }, + methods: { + inputId: function(i,j) { + return "q" + i + "_" + "input" + j; + }, + }, + }, + }, + methods: { + // stage 0 --> 1 + startMonitoring: function() { + $.ajax("/start/monitoring", { + method: "GET", + data: { + password: this., + aname: examName, + cname: courseName, + }, + dataType: "json", + success: s => { + if (!!s.errmsg) + return this.warning(s.errmsg); + this.stage = 1; + }, + }); + }, + // TODO: 2-level sockets, for prof and monitors + socket = io.connect("/" + assessment.name, { + query: "number=" + this.student.number + "&password=" + this.password + }); + socket.on(message.allAnswers, this.setAnswers); + initializeStage2(s.questions, s.paper); + }, + }); + }, + // stage 2 --> 3 (or 4) + // from a message by statements component, or time over + // TODO: also function startAssessment (for main teacher only) + endAssessment: function() { + // Set endTime, destroy password + $("#leftButton, #rightButton").show(); + if (assessment.mode == "open") + { + this.stage = 4; + return; + } + $.ajax("/end/assessment", { + method: "GET", + data: { + aid: assessment._id, + number: this.student.number, + password: this.student.password, + }, + dataType: "json", + success: ret => { + if (!!ret.errmsg) + return this.warning(ret.errmsg); + assessment.conclusion = ret.conclusion; + this.stage = 3; + delete this.student["password"]; //unable to send new answers now + socket.disconnect(); + socket = null; + }, + }); + }, + }, +}); diff --git a/sockets.js b/sockets.js index 38a9929..1719f9a 100644 --- a/sockets.js +++ b/sockets.js @@ -1,25 +1,27 @@ -var message = require("./public/javascripts/utils/socketMessages.js"); +const message = require("./public/javascripts/utils/socketMessages"); const params = require("./config/parameters"); +const AssessmentEntity = require("./entities/assessment"); +const ObjectId = require("bson-objectid"); // TODO: when teacher connect on monitor, io.of("appropriate namespace").on(connect student) { ... } // --> 2 sockets on monitoring page: one with ns "/" et one dedicated to the exam, triggered after the first // --> The monitoring page should not be closed during exam (otherwise monitors won't receive any more data) -function quizzRoom(socket) { +function examRoom(socket) { let students = { }; + const aid = ObjectId(socket.handshake.query.aid); // Student or monitor stuff const isTeacher = !!socket.handshake.query.secret && socket.handshake.query.secret == params.secret; if (isTeacher) { - // TODO: on student disconnect, too socket.on(message.newAnswer, m => { //got answer from student socket.emit(message.newAnswer, m); }); - socket.on(message.socketFeedback, m => { //send feedback to student (answers) - if (!!students[m.number]) - socket.broadcast.to(students[m.number]).emit(message.newFeedback, { feedback:m.feedback }); + socket.on(message.allAnswers, m => { //send feedback to student (answers) + if (!!students[m.number]) //TODO: namespace here... room quiz + socket.broadcast.to(students[m.number]).emit(message.allAnswers, m); }); socket.on("disconnect", m => { // Reset student array if no more active teacher connections (TODO: condition) @@ -31,17 +33,25 @@ function quizzRoom(socket) { { const number = socket.handshake.query.number; const password = socket.handshake.query.password; - // Prevent socket connection (just ignore) if student already connected - if (!!students[number] && students[number].password != password) - return; - students[number] = { - sid: socket.id, - password: password, - }; - socket.on(message.newFeedback, () => { //got feedback from teacher - socket.emit(message.newFeedback, m); + AssessmentEntity.checkPassword(aid, number, password, (err,ret) => { + if (!!err || !ret) + return; //wrong password, or some unexpected error... + // Prevent socket connection (just ignore) if student already connected + if (!!students[number]) + return; + students[number] = { + sid: socket.id, + password: password, + }; + socket.on(message.allAnswers, () => { //got all answers from teacher + socket.emit(message.allAnswers, m); + }); + socket.on("disconnect", () => { + // .. + //TODO: notify monitor (highlight red), redirect + }); + // NOTE: nothing on disconnect --> teacher disconnect trigger students cleaning }); - // NOTE: nothing on disconnect --> teacher disconnect trigger students cleaning } } diff --git a/views/monitor.pug b/views/monitor.pug index e5dadfe..6c9cf6a 100644 --- a/views/monitor.pug +++ b/views/monitor.pug @@ -5,8 +5,27 @@ extends withQuestions // array with results + quiz details (displayed in another tab) + init socket (with hash too) // buttons "start quiz" and "stop quiz" for teacher only: trigger actions (impacting sockets) - body - p TODO + // TODO: data = papers (modified after socket messages + retrived at start) + // But get examName by server at loading + +block content + .container#assessment + .row + .col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3 + h4= examName + #stage0(v-show="stage==0") + .card + .input-field.inline.on-left + label(for="password") Password + input#password(type="password" v-model="password" @keyup.enter="startMonitoring()") + button.waves-effect.waves-light.btn(@click="startMonitoring()") Send + #stage2(v-show="stage==1") + .card + .introduction(v-html="assessment.introduction") + .card + statements(:assessment="assessment" :student="student" :stage="stage" :inputs="inputs" @gameover="endAssessment" @warning="warning") + .card + .conclusion(v-html="assessment.conclusion") block append javascripts - script. TODO + script(src="/javascripts/monitor.js") -- 2.44.0