early draft of sockets logic for monitoring
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 30 Jan 2018 11:00:41 +0000 (12:00 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 30 Jan 2018 11:00:41 +0000 (12:00 +0100)
TODO [moved from TODO_assessment_template with 91% similarity]
public/javascripts/assessment.js
public/javascripts/monitor.js
sockets.js
views/monitor.pug

similarity index 91%
rename from TODO_assessment_template
rename to TODO
index f44a163..3e6b8f7 100644 (file)
+++ b/TODO
@@ -1,3 +1,6 @@
+auto grading + finish erdiag + corr tp1
+-----
+
 TODO: format général TXT: (compilé en JSON)
 
 10 (time)
index 91e34fc..c014b5a 100644 (file)
@@ -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;
                                },
                        });
                },
index b58461b..1d32d0c 100644 (file)
@@ -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;
+                               },
+                       });
+               },
+       },
+});
index 38a9929..1719f9a 100644 (file)
@@ -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
        }
 }
 
index e5dadfe..6c9cf6a 100644 (file)
@@ -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")