Basic monitoring OK (sockets non-functional atm)
authorBenjamin Auder <benjamin.auder@somewhere>
Sun, 4 Feb 2018 23:07:42 +0000 (00:07 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Sun, 4 Feb 2018 23:07:42 +0000 (00:07 +0100)
21 files changed:
README.md
TODO
app.js
config/parameters.js.dist
models/assessment.js
public/javascripts/assessment.js
public/javascripts/components/statements.js
public/javascripts/course.js
public/javascripts/grading.js [new file with mode: 0644]
public/javascripts/monitor.js
public/stylesheets/assessment.css
public/stylesheets/course.css
public/stylesheets/monitor.css
routes/assessments.js
routes/courses.js
routes/pages.js
sockets.js
views/assessment.pug
views/course.pug
views/grading.pug [new file with mode: 0644]
views/monitor.pug

index d1a22b5..845524b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -16,8 +16,7 @@ Individual answers to an exam are monitored in real time, and feedback is sent
 to each participant in the end (answers, computing grade).
 Once a series of exam is over, the teacher can get all grades in CSV format from course page.
 
 to each participant in the end (answers, computing grade).
 Once a series of exam is over, the teacher can get all grades in CSV format from course page.
 
-*Note:* for now the monitoring + socket part is still unimplemented,
-and exams composition is limited to single question exercises.
+*Note:* for now exams composition is limited to single question exercises.
 Automatic grades are also not available.
 
 ## Installation
 Automatic grades are also not available.
 
 ## Installation
diff --git a/TODO b/TODO
index 0d3b5e7..8670fcb 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,5 +1,11 @@
-permettre temps par question : gestion côté serveur, y réfléchir...
-auto grading + finish erdiag + corr tp1
+Replace underscore by lodash (or better: ES6)
+Replace socket.io by Websockets ( https://www.npmjs.com/package/websocket )
+
+time per question (in mode "one question at a time" from server...)
+compute grades after exam (in teacher's view)
+factorize redundant code in course.js, monitor.js and (TOWRITE) grade.js
+  (showing students list + grades or papers)
+
 -----
 
 TODO: format général TXT: (compilé en JSON)
 -----
 
 TODO: format général TXT: (compilé en JSON)
diff --git a/app.js b/app.js
index 5e7fdfe..0fd56f9 100644 (file)
--- a/app.js
+++ b/app.js
@@ -48,8 +48,10 @@ app.use(function(req, res, next) {
 
 // Error handler
 app.use(function(err, req, res, next) {
 
 // Error handler
 app.use(function(err, req, res, next) {
-  // Set locals, only providing error in development
-  res.locals.message = err.message;
+  // Set locals, only providing error in development (TODO: difference req.app and app ?!)
+       res.locals.message = err.message;
+       if (app.get('env') === 'development')
+               console.log(err);
   res.locals.error = req.app.get('env') === 'development' ? err : {};
   res.status(err.status || 500);
   res.render('error');
   res.locals.error = req.app.get('env') === 'development' ? err : {};
   res.status(err.status || 500);
   res.render('error');
index 82ca2ae..66e0bd1 100644 (file)
@@ -6,6 +6,9 @@ Parameters.siteURL = "http://localhost";
 // Lifespan of a (login) cookie
 Parameters.cookieExpire = 183*24*3600*1000; //6 months in milliseconds
 
 // Lifespan of a (login) cookie
 Parameters.cookieExpire = 183*24*3600*1000; //6 months in milliseconds
 
+// Secret string used in monitoring page + review
+Parameters.secret = "ImNotSoSecretChangeMe";
+
 // Characters in a login token, and period of validity (in milliseconds)
 Parameters.token = {
        length: 16,
 // Characters in a login token, and period of validity (in milliseconds)
 Parameters.token = {
        length: 16,
index 9269aee..9ab92ba 100644 (file)
@@ -23,6 +23,18 @@ const AssessmentModel =
                });
        },
 
                });
        },
 
+       checkPassword: function(aid, number, password, cb)
+       {
+               AssessmentEntity.getById(aid, (err,assessment) => {
+                       if (!!err || !assessment)
+                               return cb(err, assessment);
+                       const paperIdx = assessment.papers.findIndex( item => { return item.number == number; });
+                       if (paperIdx === -1)
+                               return cb({errmsg: "Paper not found"}, false);
+                       cb(null, assessment.papers[paperIdx].password == password);
+               });
+       },
+
        add: function(uid, cid, name, cb)
        {
                // 1) Check that course is owned by user of ID uid
        add: function(uid, cid, name, cb)
        {
                // 1) Check that course is owned by user of ID uid
@@ -38,9 +50,9 @@ const AssessmentModel =
 
        update: function(uid, assessment, cb)
        {
 
        update: function(uid, assessment, cb)
        {
-               const qid = ObjectId(assessment._id);
+               const aid = ObjectId(assessment._id);
                // 1) Check that assessment is owned by user of ID uid
                // 1) Check that assessment is owned by user of ID uid
-               AssessmentEntity.getById(qid, (err,assessmentOld) => {
+               AssessmentEntity.getById(aid, (err,assessmentOld) => {
                        if (!!err || !assessmentOld)
                                return cb({errmsg: "Assessment retrieval failure"});
                        CourseEntity.getById(ObjectId(assessmentOld.cid), (err2,course) => {
                        if (!!err || !assessmentOld)
                                return cb({errmsg: "Assessment retrieval failure"});
                        CourseEntity.getById(ObjectId(assessmentOld.cid), (err2,course) => {
@@ -51,7 +63,7 @@ const AssessmentModel =
                                // 2) Replace assessment
                                delete assessment["_id"];
                                assessment.cid = ObjectId(assessment.cid);
                                // 2) Replace assessment
                                delete assessment["_id"];
                                assessment.cid = ObjectId(assessment.cid);
-                               AssessmentEntity.replace(qid, assessment, cb);
+                               AssessmentEntity.replace(aid, assessment, cb);
                        });
                });
        },
                        });
                });
        },
@@ -62,12 +74,14 @@ const AssessmentModel =
                AssessmentEntity.getPaperByNumber(aid, number, (err,paper) => {
                        if (!!err)
                                return cb(err,null);
                AssessmentEntity.getPaperByNumber(aid, number, (err,paper) => {
                        if (!!err)
                                return cb(err,null);
+                       if (!paper && !!password)
+                               return cb({errmsg: "Cannot start a new exam before finishing current"},null);
                        if (!!paper)
                        {
                                if (!password)
                        if (!!paper)
                        {
                                if (!password)
-                                       return cb({errmsg:"Missing password"});
+                                       return cb({errmsg: "Missing password"});
                                if (paper.password != password)
                                if (paper.password != password)
-                                       return cb({errmsg:"Wrong password"});
+                                       return cb({errmsg: "Wrong password"});
                        }
                        AssessmentEntity.getQuestions(aid, (err,questions) => {
                                if (!!err)
                        }
                        AssessmentEntity.getQuestions(aid, (err,questions) => {
                                if (!!err)
index 20ca2cb..09caf12 100644 (file)
@@ -12,7 +12,7 @@ function checkWindowSize()
        return window.innerWidth >= screen.width-3 && window.innerHeight >= screen.height-3;
 };
 
        return window.innerWidth >= screen.width-3 && window.innerHeight >= screen.height-3;
 };
 
-new Vue({
+let V = new Vue({
        el: "#assessment",
        data: {
                assessment: assessment,
        el: "#assessment",
        data: {
                assessment: assessment,
@@ -62,7 +62,7 @@ new Vue({
        },
        methods: {
                // In case of AJAX errors
        },
        methods: {
                // In case of AJAX errors
-               warning: function(message) {
+               showWarning: function(message) {
                        this.warnMsg = message;
                        $("#warning").modal("open");
                },
                        this.warnMsg = message;
                        $("#warning").modal("open");
                },
@@ -73,7 +73,7 @@ new Vue({
                },
                trySendCurrentAnswer: function() {
                        if (this.stage == 2)
                },
                trySendCurrentAnswer: function() {
                        if (this.stage == 2)
-                               this.sendAnswer(assessment.indices[assessment.index]);
+                               this.sendAnswer();
                },
                // stage 0 --> 1
                getStudent: function(cb) {
                },
                // stage 0 --> 1
                getStudent: function(cb) {
@@ -86,7 +86,7 @@ new Vue({
                                dataType: "json",
                                success: s => {
                                        if (!!s.errmsg)
                                dataType: "json",
                                success: s => {
                                        if (!!s.errmsg)
-                                               return this.warning(s.errmsg);
+                                               return this.showWarning(s.errmsg);
                                        this.stage = 1;
                                        this.student = s.student;
                                        Vue.nextTick( () => { Materialize.updateTextFields(); });
                                        this.stage = 1;
                                        this.student = s.student;
                                        Vue.nextTick( () => { Materialize.updateTextFields(); });
@@ -114,7 +114,7 @@ new Vue({
                                        assessment.questions = questions;
                                this.answers.inputs = [ ];
                                for (let q of assessment.questions)
                                        assessment.questions = questions;
                                this.answers.inputs = [ ];
                                for (let q of assessment.questions)
-                                       this.inputs.push( _(q.options.length).times( _.constant(false) ) );
+                                       this.answers.inputs.push( _(q.options.length).times( _.constant(false) ) );
                                if (!paper)
                                {
                                        this.answers.indices = assessment.fixed
                                if (!paper)
                                {
                                        this.answers.indices = assessment.fixed
@@ -129,7 +129,7 @@ new Vue({
                                        this.answers.indices = indices.concat( _.shuffle(remainingIndices) );
                                }
                                this.answers.index = !!paper ? paper.inputs.length : 0;
                                        this.answers.indices = indices.concat( _.shuffle(remainingIndices) );
                                }
                                this.answers.index = !!paper ? paper.inputs.length : 0;
-                               Vue.nextTick(libsRefresh);
+                               Vue.nextTick(statementsLibsRefresh);
                                this.stage = 2;
                        };
                        if (assessment.mode == "open")
                                this.stage = 2;
                        };
                        if (assessment.mode == "open")
@@ -143,7 +143,7 @@ new Vue({
                                dataType: "json",
                                success: s => {
                                        if (!!s.errmsg)
                                dataType: "json",
                                success: s => {
                                        if (!!s.errmsg)
-                                               return this.warning(s.errmsg);
+                                               return this.showWarning(s.errmsg);
                                        if (!!s.paper)
                                        {
                                                // Resuming: receive stored answers + startTime
                                        if (!!s.paper)
                                        {
                                                // Resuming: receive stored answers + startTime
@@ -157,7 +157,7 @@ new Vue({
                                                // action (power failure, computer down, ...)
                                        }
                                        socket = io.connect("/" + assessment.name, {
                                                // action (power failure, computer down, ...)
                                        }
                                        socket = io.connect("/" + assessment.name, {
-                                               query: "number=" + this.student.number + "&password=" + this.password
+                                               query: "aid=" + assessment._id + "&number=" + this.student.number + "&password=" + this.student.password
                                        });
                                        socket.on(message.allAnswers, this.setAnswers);
                                        initializeStage2(s.questions, s.paper);
                                        });
                                        socket.on(message.allAnswers, this.setAnswers);
                                        initializeStage2(s.questions, s.paper);
@@ -171,30 +171,31 @@ new Vue({
                        let self = this;
                        setInterval( function() {
                                self.remainingTime--;
                        let self = this;
                        setInterval( function() {
                                self.remainingTime--;
-                               if (self.remainingTime <= 0 || self.stage >= 4)
-                                       self.endAssessment();
+                               if (self.remainingTime <= 0)
+                               {
+                                       if (self.stage == 2)
+                                               self.endAssessment();
                                        clearInterval(this);
                                        clearInterval(this);
+                               }
                        }, 1000);
                },
                // stage 2
                        }, 1000);
                },
                // stage 2
-               // TODO: currentIndex ? click: () => this.sendAnswer(assessment.indices[assessment.index]),
-               // De même, cette condition sur le display d'une question doit remonter (résumée dans 'index' property) :
-               // à faire par ici : "hide": this.stage == 2 && assessment.display == 'one' && assessment.indices[assessment.index] != i,
-               sendAnswer: function(realIndex) {
+               sendAnswer: function() {
+                       const realIndex = this.answers.indices[this.answers.index];
                        let gotoNext = () => {
                        let gotoNext = () => {
-                               if (assessment.index == assessment.questions.length - 1)
+                               if (this.answers.index == assessment.questions.length - 1)
                                        this.endAssessment();
                                else
                                        this.endAssessment();
                                else
-                                       assessment.index++;
-                               this.$forceUpdate(); //TODO: shouldn't be required
+                                       this.answers.index++;
+                               this.$children[0].$forceUpdate(); //TODO: bad HACK, and shouldn't be required...
                        };
                        if (assessment.mode == "open")
                                return gotoNext(); //only local
                        let answerData = {
                                aid: assessment._id,
                                answer: JSON.stringify({
                        };
                        if (assessment.mode == "open")
                                return gotoNext(); //only local
                        let answerData = {
                                aid: assessment._id,
                                answer: JSON.stringify({
-                                       index:realIndex.toString(),
-                                       input:this.inputs[realIndex]
+                                       index: realIndex.toString(),
+                                       input: this.answers.inputs[realIndex]
                                                .map( (tf,i) => { return {val:tf,idx:i}; } )
                                                .filter( item => { return item.val; })
                                                .map( item => { return item.idx; })
                                                .map( (tf,i) => { return {val:tf,idx:i}; } )
                                                .filter( item => { return item.val; })
                                                .map( item => { return item.idx; })
@@ -208,9 +209,8 @@ new Vue({
                                dataType: "json",
                                success: ret => {
                                        if (!!ret.errmsg)
                                dataType: "json",
                                success: ret => {
                                        if (!!ret.errmsg)
-                                               return this.$emit("warning", ret.errmsg);
-                                       else
-                                               gotoNext();
+                                               return this.showWarning(ret.errmsg);
+                                       gotoNext();
                                        socket.emit(message.newAnswer, answerData);
                                },
                        });
                                        socket.emit(message.newAnswer, answerData);
                                },
                        });
@@ -235,7 +235,7 @@ new Vue({
                                dataType: "json",
                                success: ret => {
                                        if (!!ret.errmsg)
                                dataType: "json",
                                success: ret => {
                                        if (!!ret.errmsg)
-                                               return this.warning(ret.errmsg);
+                                               return this.showWarning(ret.errmsg);
                                        assessment.conclusion = ret.conclusion;
                                        this.stage = 3;
                                        delete this.student["password"]; //unable to send new answers now
                                        assessment.conclusion = ret.conclusion;
                                        this.stage = 3;
                                        delete this.student["password"]; //unable to send new answers now
@@ -245,9 +245,9 @@ new Vue({
                        });
                },
                // stage 3 --> 4 (on socket message "feedback")
                        });
                },
                // stage 3 --> 4 (on socket message "feedback")
-               setAnswers: function(answers) {
-                       for (let i=0; i<answers.length; i++)
-                               assessment.questions[i].answer = answers[i];
+               setAnswers: function(m) {
+                       for (let i=0; i<m.answers.length; i++)
+                               assessment.questions[i].answer = m.answers[i];
                        this.stage = 4;
                },
        },
                        this.stage = 4;
                },
        },
index 6ebea85..5bf6bdb 100644 (file)
@@ -11,7 +11,7 @@ Vue.component("statements", {
        // Full questions tree is rendered, but some parts hidden depending on display settings
        render(h) {
                // TODO: render nothing if answers is empty
        // Full questions tree is rendered, but some parts hidden depending on display settings
        render(h) {
                // TODO: render nothing if answers is empty
-               let domTree = this.questions.map( (q,i) => {
+               let domTree = (this.questions || [ ]).map( (q,i) => {
                        let questionContent = [ ];
                        questionContent.push(
                                h(
                        let questionContent = [ ];
                        questionContent.push(
                                h(
@@ -70,7 +70,7 @@ Vue.component("statements", {
                                                        "class": {
                                                                option: true,
                                                                choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx),
                                                        "class": {
                                                                option: true,
                                                                choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx),
-                                                               choiceWrong: this.answers.showSolution && this.inputs[i][idx] && !q.answer.includes(idx),
+                                                               choiceWrong: this.answers.showSolution && this.answers.inputs[i][idx] && !q.answer.includes(idx),
                                                        },
                                                },
                                                option
                                                        },
                                                },
                                                option
@@ -93,7 +93,7 @@ Vue.component("statements", {
                                {
                                        "class": {
                                                "question": true,
                                {
                                        "class": {
                                                "question": true,
-                                               "hide": !this.answers.displayAll && this.answers.index != i,
+                                               "hide": !this.answers.displayAll && this.answers.indices[this.answers.index] != i,
                                        },
                                },
                                questionContent
                                        },
                                },
                                questionContent
@@ -106,7 +106,7 @@ Vue.component("statements", {
                                        id: "statements",
                                },
                        },
                                        id: "statements",
                                },
                        },
-                       questions
+                       domTree
                );
        },
        updated: function() {
                );
        },
        updated: function() {
index 94e1b84..85172dc 100644 (file)
@@ -340,8 +340,8 @@ window.onload = function() {
                                        return ""; //no grade yet
                                return this.grades[assessmentIndex][studentNumber];
                        },
                                        return ""; //no grade yet
                                return this.grades[assessmentIndex][studentNumber];
                        },
-                       groupId: function(group, hash) {
-                               return (!!hash?"#":"") + "group" + group;
+                       groupId: function(group, prefix) {
+                               return (prefix || "") + "group" + group;
                        },
                        togglePresence: function(number, index) {
                                // UNIMPLEMENTED
                        },
                        togglePresence: function(number, index) {
                                // UNIMPLEMENTED
diff --git a/public/javascripts/grading.js b/public/javascripts/grading.js
new file mode 100644 (file)
index 0000000..5264d83
--- /dev/null
@@ -0,0 +1 @@
+//TODO: similar to monitor / course (need to factor some code)
index 5d5f237..6c08d7a 100644 (file)
@@ -1,18 +1,10 @@
-// TODO: onglets pour chaque groupe + section déroulante questionnaire (chargé avec réponses)
-//   NOM Prenom (par grp, puis alphabétique)
-//   réponse : vert si OK (+ choix), rouge si faux, gris si texte (clic pour voir)
-//   + temps total ?
-//   click sur en-tête de colonne : tri alphabétique, tri décroissant...
-// Affiché si (hash du) mdp du cours est correctement entré
-// Doit reprendre les données en base si refresh (sinon : sockets)
-
 let socket = null; //monitor answers in real time
 
 new Vue({
        el: "#monitor",
        data: {
                password: "", //from password field
 let socket = null; //monitor answers in real time
 
 new Vue({
        el: "#monitor",
        data: {
                password: "", //from password field
-               assessment: null, //obtained after authentication
+               assessment: { }, //obtained after authentication
                // Stage 0: unauthenticated (password),
                //       1: authenticated (password hash validated), start monitoring
                stage: 0,
                // Stage 0: unauthenticated (password),
                //       1: authenticated (password hash validated), start monitoring
                stage: 0,
@@ -22,23 +14,75 @@ new Vue({
                        inputs: [ ],
                        index : -1,
                },
                        inputs: [ ],
                        index : -1,
                },
+               students: [ ], //to know their names
+               display: "assessment", //or student's answers
        },
        methods: {
        },
        methods: {
+               // TODO: redundant code, next 4 funcs already exist in course.js
+               toggleDisplay: function(area) {
+                       if (this.display == area)
+                               this.display = "";
+                       else
+                               this.display = area;
+               },
+               studentList: function(group) {
+                       return this.students
+                               .filter( s => { return group==0 || s.group == group; })
+                               .map( s => { return Object.assign({}, s); }) //not altering initial array
+                               .sort( (a,b) => {
+                                       let res = a.name.localeCompare(b.name);
+                                       if (res == 0)
+                                               res += a.forename.localeCompare(b.forename);
+                                       return res;
+                               });
+               },
+               groupList: function() {
+                       let maxGrp = 1;
+                       this.students.forEach( s => {
+                               if (s.group > maxGrp)
+                                       maxGrp = s.group;
+                       });
+                       return _.range(1,maxGrp+1);
+               },
+               groupId: function(group, prefix) {
+                       return (prefix || "") + "group" + group;
+               },
+               getColor: function(number, qIdx) {
+                       // For the moment, green if correct and red if wrong; grey if unanswered yet
+                       // TODO: in-between color for partially right (especially for multi-questions)
+                       const paperIdx = this.assessment.papers.findIndex( item => { return item.number == number; });
+                       if (paperIdx === -1)
+                               return "grey"; //student didn't start yet
+                       const inputIdx = this.assessment.papers[paperIdx].inputs.findIndex( item => {
+                               const qNum = parseInt(item.index.split(".")[0]); //indexes separated by dots
+                               return qIdx == qNum;
+                       });
+                       if (inputIdx === -1)
+                               return "grey";
+                       if (_.isEqual(this.assessment.papers[paperIdx].inputs[inputIdx].input, this.assessment.questions[qIdx].answer))
+                               return "green";
+                       return "red";
+               },
+               seeDetails: function(number, i) {
+                       // UNIMPLEMENTED: see question details, with current answer(s)
+               },
                // stage 0 --> 1
                startMonitoring: function() {
                        $.ajax("/start/monitoring", {
                                method: "GET",
                                data: {
                // stage 0 --> 1
                startMonitoring: function() {
                        $.ajax("/start/monitoring", {
                                method: "GET",
                                data: {
-                                       password: this.password,
+                                       password: Sha1.Compute(this.password),
                                        aname: examName,
                                        aname: examName,
-                                       cname: courseName,
+                                       ccode: courseCode,
                                        initials: initials,
                                },
                                dataType: "json",
                                success: s => {
                                        if (!!s.errmsg)
                                        initials: initials,
                                },
                                dataType: "json",
                                success: s => {
                                        if (!!s.errmsg)
-                                               return this.warning(s.errmsg);
-                                       this.assessment = JSON.parse(s.assessment);
+                                               return alert(s.errmsg);
+                                       this.assessment = s.assessment;
+                                       this.answers.inputs = s.assessment.questions.map( q => { return q.answer; });
+                                       this.students = s.students;
                                        this.stage = 1;
                                        socket = io.connect("/", {
                                                query: "aid=" + this.assessment._id + "&secret=" + s.secret
                                        this.stage = 1;
                                        socket = io.connect("/", {
                                                query: "aid=" + this.assessment._id + "&secret=" + s.secret
@@ -47,10 +91,27 @@ new Vue({
                                                let paperIdx = this.assessment.papers.findIndex( item => {
                                                        return item.number == m.number;
                                                });
                                                let paperIdx = this.assessment.papers.findIndex( item => {
                                                        return item.number == m.number;
                                                });
-                                               this.assessment.papers[paperIdx].inputs.push(m.input); //answer+index
+                                               if (paperIdx === -1)
+                                               {
+                                                       // First answer
+                                                       paperIdx = this.assessment.papers.length;
+                                                       this.assessment.papers.push({
+                                                               number: m.number,
+                                                               inputs: [ ], //other fields irrelevant here
+                                                       });
+                                               }
+                                               // TODO: notations not coherent (input / answer... when, which ?)
+                                               this.assessment.papers[paperIdx].inputs.push(m.answer); //input+index
                                        });
                                },
                        });
                },
                                        });
                                },
                        });
                },
+               endMonitoring: function() {
+                       // In the end, send answers to students
+                       socket.emit(
+                               message.allAnswers,
+                               { answers: this.assessment.questions.map( q => { return q.answer; }) }
+                       );
+               },
        },
 });
        },
 });
index b8a2409..2143f40 100644 (file)
@@ -53,3 +53,14 @@ button#sendAnswer {
 .timer {
        font-size: 2rem;
 }
 .timer {
        font-size: 2rem;
 }
+
+table.in-question {
+       border: 1px solid black;
+       width: auto;
+       margin: 10px auto;
+}
+
+table.in-question th, table.in-question td {
+       padding: 3px;
+       border-bottom: 1px solid grey;
+}
index 2d61347..c1c4a9b 100644 (file)
@@ -54,3 +54,14 @@ tr.stats {
 .conclusion {
        margin-bottom: 20px;
 }
 .conclusion {
        margin-bottom: 20px;
 }
+
+table.in-question {
+       border: 1px solid black;
+       width: auto;
+       margin: 10px auto;
+}
+
+table.in-question th, table.in-question td {
+       padding: 3px;
+       border-bottom: 1px solid grey;
+}
index 8f414f5..b585800 100644 (file)
@@ -1 +1,41 @@
-/* TODO */
+/* TODO: factor this piece of code from assessment (and course, and here...) */
+.question {
+       margin: 20px 5px;
+       padding: 15px 0;
+}
+.question label {
+       color: black;
+}
+.question .choiceCorrect {
+       background-color: lightgreen;
+}
+.question .choiceWrong {
+       background-color: peachpuff;
+}
+.question .wording {
+       margin-bottom: 10px;
+}
+.question .option {
+       margin-left: 15px;
+}
+.question p {
+       margin-top: 10px;
+}
+.questionInactive {
+       background-color: lightgrey;
+}
+.introduction {
+       padding: 20px 5px;
+}
+.conclusion {
+       padding: 20px 5px;
+}
+table.in-question {
+       border: 1px solid black;
+       width: auto;
+       margin: 10px auto;
+}
+table.in-question th, table.in-question td {
+       padding: 3px;
+       border-bottom: 1px solid grey;
+}
index dc749ed..a107d7e 100644 (file)
@@ -9,8 +9,12 @@ const validator = require("../public/javascripts/utils/validation");
 const ObjectId = require("bson-objectid");
 const sanitizeHtml = require('sanitize-html');
 const sanitizeOpts = {
 const ObjectId = require("bson-objectid");
 const sanitizeHtml = require('sanitize-html');
 const sanitizeOpts = {
-       allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img' ]),
-       allowedAttributes: { code: [ 'class' ] },
+       allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img', 'u' ]),
+       allowedAttributes: {
+               img: [ 'src' ],
+               code: [ 'class' ],
+               table: [ 'class' ],
+       },
 };
 
 router.get("/add/assessment", access.ajax, access.logged, (req,res) => {
 };
 
 router.get("/add/assessment", access.ajax, access.logged, (req,res) => {
@@ -69,6 +73,29 @@ router.get("/start/assessment", access.ajax, (req,res) => {
        });
 });
 
        });
 });
 
+router.get("/start/monitoring", access.ajax, (req,res) => {
+       const password = req.query["password"];
+       const examName = req.query["aname"];
+       const courseCode = req.query["ccode"];
+       const initials = req.query["initials"];
+       // TODO: sanity checks
+       CourseModel.getByRefs(initials, courseCode, (err,course) => {
+               access.checkRequest(res,err,course,"Course not found", () => {
+                       if (password != course.password)
+                               return res.json({errmsg: "Wrong password"});
+                       AssessmentModel.getByRefs(initials, courseCode, examName, (err2,assessment) => {
+                               access.checkRequest(res,err2,assessment,"Assessment not found", () => {
+                                       res.json({
+                                               students: course.students,
+                                               assessment: assessment,
+                                               secret: params.secret,
+                                       });
+                               });
+                       });
+               });
+       });
+});
+
 router.get("/send/answer", access.ajax, (req,res) => {
        let aid = req.query["aid"];
        let number = req.query["number"];
 router.get("/send/answer", access.ajax, (req,res) => {
        let aid = req.query["aid"];
        let number = req.query["number"];
index d221858..e07e243 100644 (file)
@@ -74,4 +74,6 @@ router.get('/remove/course', access.ajax, access.logged, (req,res) => {
        });
 });
 
        });
 });
 
+// TODO: grading page (for at least partially open-questions exams)
+
 module.exports = router;
 module.exports = router;
index 15cf1fd..f6c77a2 100644 (file)
@@ -126,16 +126,17 @@ router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z
        });
 });
 
        });
 });
 
-// Monitor: --> after identification (password), always send password hash with requests
+// Monitor: --> after identification (password), always send secret with requests
 router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z0-9._-]+)/monitor", (req,res) => {
        let initials = req.params["initials"];
        let code = req.params["courseCode"];
        let name = req.params["assessmentName"];
 router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z0-9._-]+)/monitor", (req,res) => {
        let initials = req.params["initials"];
        let code = req.params["courseCode"];
        let name = req.params["assessmentName"];
+       // TODO: if (main) teacher, also send secret, saving one request
        res.render("monitor", {
                title: "monitor assessment " + code + "/" + name,
                initials: initials,
        res.render("monitor", {
                title: "monitor assessment " + code + "/" + name,
                initials: initials,
-               code: code,
-               name: name,
+               courseCode: code,
+               examName: name,
        });
 });
 
        });
 });
 
index eeda127..2ce8ae9 100644 (file)
@@ -1,6 +1,6 @@
 const message = require("./public/javascripts/utils/socketMessages");
 const params = require("./config/parameters");
 const message = require("./public/javascripts/utils/socketMessages");
 const params = require("./config/parameters");
-const AssessmentEntity = require("./entities/assessment");
+const AssessmentModel = require("./models/assessment");
 const ObjectId = require("bson-objectid");
 
 module.exports = function(io)
 const ObjectId = require("bson-objectid");
 
 module.exports = function(io)
@@ -10,34 +10,23 @@ module.exports = function(io)
                socket.join(aid);
                // Student or monitor connexion
                const isTeacher = !!socket.handshake.query.secret && socket.handshake.query.secret == params.secret;
                socket.join(aid);
                // Student or monitor connexion
                const isTeacher = !!socket.handshake.query.secret && socket.handshake.query.secret == params.secret;
+
                if (isTeacher)
                {
                        socket.on(message.newAnswer, m => { //got answer from student
                                socket.emit(message.newAnswer, m);
                        });
                        socket.on(message.allAnswers, m => { //send feedback to student (answers)
                if (isTeacher)
                {
                        socket.on(message.newAnswer, m => { //got answer from student
                                socket.emit(message.newAnswer, m);
                        });
                        socket.on(message.allAnswers, m => { //send feedback to student (answers)
-                               if (!!students[m.number]) //TODO: namespace here... room quiz
-                                       socket.broadcast.to(aid).emit(message.allAnswers, m);
+                               socket.broadcast.to(aid).emit(message.allAnswers, m);
                        });
                }
                else //student
                {
                        const number = socket.handshake.query.number;
                        const password = socket.handshake.query.password;
                        });
                }
                else //student
                {
                        const number = socket.handshake.query.number;
                        const password = socket.handshake.query.password;
-                       AssessmentEntity.checkPassword(ObjectId(aid), number, password, (err,ret) => {
+                       AssessmentModel.checkPassword(ObjectId(aid), number, password, (err,ret) => {
                                if (!!err || !ret)
                                        return; //wrong password, or some unexpected error...
                                if (!!err || !ret)
                                        return; //wrong password, or some unexpected error...
-                               // TODO: Prevent socket connection (just ignore) if student already connected
-//                             io.of('/').in(aid).clients((error, clients) => {
-//                                     if (error)
-//                                             throw error;
-//                                     if (clients.some( c => { return c. .. == number; }))
-//                                             // Problem: we just have a list of socket IDs (not handshakes)
-//                             });
-                               // TODO: next is conditional to "student not already taking the exam"
-                               socket.on(message.allAnswers, () => { //got all answers from teacher
-                                       socket.emit(message.allAnswers, m);
-                               });
                                socket.on("disconnect", () => {
                                        //TODO: notify monitor (grey low opacity background)
                                        //Also send to server: discoTime in assessment.papers ...
                                socket.on("disconnect", () => {
                                        //TODO: notify monitor (grey low opacity background)
                                        //Also send to server: discoTime in assessment.papers ...
index b8ed733..0a7e369 100644 (file)
@@ -39,20 +39,20 @@ block content
                                                        if assessment.mode != "open"
                                                                button.waves-effect.waves-light.btn.on-left(@click="cancelStudent") Cancel
                                                        button.waves-effect.waves-light.btn(@click="startAssessment") Start!
                                                        if assessment.mode != "open"
                                                                button.waves-effect.waves-light.btn.on-left(@click="cancelStudent") Cancel
                                                        button.waves-effect.waves-light.btn(@click="startAssessment") Start!
-                               #stage1_2_4(v-show="stage==1 || stage==2 || stage == 4")
+                               #stage0_1_4(v-show="[0,1,4].includes(stage)")
                                        .card
                                                .introduction(v-html="assessment.introduction")
                                        .card
                                                .introduction(v-html="assessment.introduction")
-                               #stage2_4(v-show="stage==2 || stage==4")
+                               #stage2_4(v-show="[2,4].includes(stage)")
                                        if assessment.time > 0
                                        if assessment.time > 0
-                                               .card
-                                                       .timer.center(v-if="stage==2") {{ countdown }}
+                                               .card(v-if="stage==2")
+                                                       .timer.center {{ countdown }}
                                        .card
                                        .card
-                                               button#sendAsnwer.waves-effect.waves-light.btn(@click="sendAnswer") Send
+                                               button#sendAnswer.waves-effect.waves-light.btn(@click="sendAnswer") Send
                                                statements(:questions="assessment.questions" :answers="answers")
                                #stage3(v-show="stage==3")
                                        .card
                                                .finish Exam completed &#9786; ...don't close the window!
                                                statements(:questions="assessment.questions" :answers="answers")
                                #stage3(v-show="stage==3")
                                        .card
                                                .finish Exam completed &#9786; ...don't close the window!
-                               #stage3_4(v-show="stage==3 || stage==4")
+                               #stage3_4(v-show="[3,4].includes(stage)")
                                        .card
                                                .conclusion(v-html="assessment.conclusion")
 
                                        .card
                                                .conclusion(v-html="assessment.conclusion")
 
@@ -60,5 +60,7 @@ block append javascripts
        script.
                let assessment = !{JSON.stringify(assessment)};
                const monitoring = false;
        script.
                let assessment = !{JSON.stringify(assessment)};
                const monitoring = false;
+       script(src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js")
+       script(src="/javascripts/utils/libsRefresh.js")
        script(src="/javascripts/components/statements.js")
        script(src="/javascripts/assessment.js")
        script(src="/javascripts/components/statements.js")
        script(src="/javascripts/assessment.js")
index 17bad0d..aa412e2 100644 (file)
@@ -144,7 +144,7 @@ block content
                                                        li.tab
                                                                a(href="#group0") All
                                                        li.tab(v-for="group in groupList()")
                                                        li.tab
                                                                a(href="#group0") All
                                                        li.tab(v-for="group in groupList()")
-                                                               a(:href="groupId(group,'hash')") G.{{ group }}
+                                                               a(:href="groupId(group,'#')") G.{{ group }}
                                                table.result(:id="groupId(group)" v-for="group in [0].concat(groupList())" @click="showDetails(group)")
                                                        thead
                                                                tr
                                                table.result(:id="groupId(group)" v-for="group in [0].concat(groupList())" @click="showDetails(group)")
                                                        thead
                                                                tr
diff --git a/views/grading.pug b/views/grading.pug
new file mode 100644 (file)
index 0000000..58dc5e0
--- /dev/null
@@ -0,0 +1,43 @@
+extends withQuestions
+
+block content
+       .container#grading
+               .row
+                       .col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
+                               h4= examName
+                               h4.title(@click="toggleDisplay('grading')") Grading
+                               // TODO: Allow grading per student, per question or sub-question
+                               .card(v-show="display=='grading'")
+                                       ul.tabs.tabs-fixed-width
+                                               li.tab
+                                                       a(href="#group0") All
+                                               li.tab(v-for="group in groupList()")
+                                                       a(:href="groupId(group,'#')") G.{{ group }}
+                                       table(:id="groupId(group)" v-for="group in [0].concat(groupList())")
+                                               thead
+                                                       tr
+                                                               th Forename
+                                                               th Name
+                                                               th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }}
+                                               tbody
+                                                       tr.assessment(v-for="s in studentList(group)")
+                                                               td {{ s.forename }}
+                                                               td {{ s.name }}
+                                                               td(v-for="(q,i) in assessment.questions" :style="{background-color: getColor(number,i)}" @click="seeDetails(number,i)") &nbsp;
+                               h4.title(@click="toggleDisplay('assessment')") Assessment
+                               div(v-show="display=='assessment'")
+                                       .card
+                                               .introduction(v-html="assessment.introduction")
+                                       .card
+                                               statements(:questions="assessment.questions" :answers:"answers")
+                                       .card
+                                               .conclusion(v-html="assessment.conclusion")
+
+block append javascripts
+       script.
+               const examName = "#{examName}";
+               const courseCode = "#{courseName}";
+               const initials = "#{initials}";
+       script(src="/javascripts/components/statements.js")
+       script(src="/javascripts/utils/sha1.js")
+       script(src="/javascripts/grading.js")
index 5a64e89..339ca47 100644 (file)
@@ -1,15 +1,10 @@
 extends withQuestions
 
 extends withQuestions
 
-       //TODO: step 1: ask password (client side, store hash)
-       // step 2: when got hash, send request (with hash) to get monitoring page:
-       //   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)
-
-       // TODO: data = papers (modified after socket messages + retrived at start)
-       // But get examName by server at loading
+block append stylesheets
+       link(rel="stylesheet" href="/stylesheets/monitor.css")
 
 block content
 
 block content
-       .container#assessment
+       .container#monitor
                .row
                        .col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
                                h4= examName
                .row
                        .col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
                                h4= examName
@@ -20,15 +15,43 @@ block content
                                                        input#password(type="password" v-model="password" @keyup.enter="startMonitoring()")
                                                button.waves-effect.waves-light.btn(@click="startMonitoring()") Send
                                #stage1(v-show="stage==1")
                                                        input#password(type="password" v-model="password" @keyup.enter="startMonitoring()")
                                                button.waves-effect.waves-light.btn(@click="startMonitoring()") Send
                                #stage1(v-show="stage==1")
-                                       .card
-                                               .introduction(v-html="assessment.introduction")
-                                       .card
-                                               statements(:questions="assessment.questions" :answers:"answers")
-                                       .card
-                                               .conclusion(v-html="assessment.conclusion")
+                                       button.waves-effect.waves-light.btn(@click="endMonitoring()") Send feedback
+                                       h4.title(@click="toggleDisplay('answers')") Anwers
+                                       // TODO: aussi afficher stats, permettre tri par colonnes
+                                       .card(v-show="display=='answers'")
+                                               ul.tabs.tabs-fixed-width
+                                                       li.tab
+                                                               a(href="#group0") All
+                                                       li.tab(v-for="group in groupList()")
+                                                               a(:href="groupId(group,'#')") G.{{ group }}
+                                               table(:id="groupId(group)" v-for="group in [0].concat(groupList())")
+                                                       thead
+                                                               tr
+                                                                       th Forename
+                                                                       th Name
+                                                                       th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }}
+                                                       tbody
+                                                               tr.assessment(v-for="s in studentList(group)")
+                                                                       td {{ s.forename }}
+                                                                       td {{ s.name }}
+                                                                       td(v-for="(q,i) in assessment.questions" :style="{backgroundColor: getColor(s.number,i)}" @click="seeDetails(s.number,i)") &nbsp;
+                                       h4.title(@click="toggleDisplay('assessment')") Assessment
+                                       div(v-show="display=='assessment'")
+                                               .card
+                                                       .introduction(v-html="assessment.introduction")
+                                               .card
+                                                       statements(:questions="assessment.questions" :answers="answers")
+                                               .card
+                                                       .conclusion(v-html="assessment.conclusion")
 
 block append javascripts
        script.
 
 block append javascripts
        script.
+               const examName = "#{examName}";
+               const courseCode = "#{courseCode}";
+               const initials = "#{initials}";
                const monitoring = true;
                const monitoring = true;
+       script(src="/javascripts/utils/libsRefresh.js")
        script(src="/javascripts/components/statements.js")
        script(src="/javascripts/components/statements.js")
+       script(src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js")
+       script(src="/javascripts/utils/sha1.js")
        script(src="/javascripts/monitor.js")
        script(src="/javascripts/monitor.js")