From 29c8b391bcdf6ffca53545178e2ad194287a1bdc Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 8 Feb 2018 18:30:46 +0100
Subject: [PATCH] Debug monitoring

---
 bin/www                                    | 21 +++++-----
 entities/assessment.js                     | 45 ++++++++++++++++++++++
 models/assessment.js                       | 12 ++++++
 public/javascripts/assessment.js           | 13 ++++---
 public/javascripts/monitor.js              |  4 +-
 public/javascripts/utils/socketMessages.js |  7 +++-
 public/javascripts/utils/validation.js     |  3 ++
 sockets.js                                 | 23 ++++++-----
 8 files changed, 98 insertions(+), 30 deletions(-)

diff --git a/bin/www b/bin/www
index c964949..eacc6e5 100755
--- a/bin/www
+++ b/bin/www
@@ -4,28 +4,28 @@
  * Module dependencies.
  */
 
-var app = require('../app');
-var http = require('http');
+const app = require('../app');
+const http = require('http');
 
 /**
  * Get port from environment and store in Express.
  */
 
-let port = normalizePort(process.env.PORT || '3000');
+const port = normalizePort(process.env.PORT || '3000');
 app.set('port', port);
 
 /**
  * Create HTTP server.
  */
 
-let server = http.createServer(app);
+const server = http.createServer(app);
 
 /*
  * CRON tasks
  */
 
-var cron = require('node-cron');
-var UserModel = require("../models/user");
+const cron = require('node-cron');
+const UserModel = require("../models/user");
 cron.schedule('0 0 0 * * *', function() {
 	// Remove unlogged users every 24h
 	UserModel.cleanUsersDb();
@@ -38,8 +38,7 @@ cron.schedule('0 0 0 * * *', function() {
 server.listen(port);
 server.on('error', onError);
 server.on('listening', onListening);
-let io = require('socket.io').listen(server); //sockets too
-//https://stackoverflow.com/a/24610678/4640434
+const io = require('socket.io').listen(server);
 
 /*
  * Sockets handling
@@ -53,7 +52,7 @@ require('../sockets')(io);
 
 function normalizePort(val)
 {
-  let port = parseInt(val, 10);
+  const port = parseInt(val, 10);
 
   if (isNaN(port)) // named pipe
     return val;
@@ -99,8 +98,8 @@ function onError(error)
 
 function onListening()
 {
-  let addr = server.address();
-  let bind = typeof addr === 'string'
+  const addr = server.address();
+  const bind = typeof addr === 'string'
     ? 'pipe ' + addr
     : 'port ' + addr.port;
   console.log('Listening on ' + bind);
diff --git a/entities/assessment.js b/entities/assessment.js
index b4e3092..3dc1952 100644
--- a/entities/assessment.js
+++ b/entities/assessment.js
@@ -27,6 +27,8 @@ const AssessmentEntity =
 	 *     number: student number
 	 *     inputs: array of indexed arrays of integers (or html text if not quiz)
 	 *     startTime, endTime
+	 *     discoTime, totalDisco: last disconnect timestamp (if relevant) + total
+	 *     discoCount: total disconnections
 	 *     password: random string identifying student for exam session TEMPORARY
 	 */
 
@@ -134,6 +136,8 @@ const AssessmentEntity =
 				startTime: Date.now(),
 				endTime: undefined,
 				password: password,
+				totalDisco: 0,
+				discoCount: 0,
 				inputs: [ ], //TODO: this is stage 1, stack indexed answers.
 				// then build JSON tree for easier access / correct
 			}}},
@@ -141,6 +145,47 @@ const AssessmentEntity =
 		);
 	},
 
+	// NOTE: no callbacks for 2 next functions, failures are not so important
+	// (because monitored: teachers can see what's going on)
+
+	addDisco: function(aid, number, deltaTime)
+	{
+		db.assessments.update(
+			{
+				_id: aid,
+				"papers.number": number,
+			},
+			{ $inc: {
+				"papers.$.discoCount": 1,
+				"papers.$.totalDisco": deltaTime,
+			} },
+			{ $set: { "papers.$.discoTime": null } }
+		);
+	},
+
+	setDiscoTime: function(aid, number)
+	{
+		db.assessments.update(
+			{
+				_id: aid,
+				"papers.number": number,
+			},
+			{ $set: { "papers.$.discoTime": Date.now() } }
+		);
+	},
+
+	getDiscoTime: function(aid, number, cb)
+	{
+		db.assessments.findOne(
+			{ _id: aid },
+			(err,a) => {
+				if (!!err)
+					return cb(err, null);
+				const idx = a.papers.findIndex( item => { return item.number == number; });
+				cb(null, a.papers[idx].discoTime);
+			}
+		);
+	},
 
 	hasInput: function(aid, number, password, idx, cb)
 	{
diff --git a/models/assessment.js b/models/assessment.js
index 9ab92ba..d5bd3a1 100644
--- a/models/assessment.js
+++ b/models/assessment.js
@@ -115,6 +115,18 @@ const AssessmentModel =
 		});
 	},
 
+	// NOTE: no callbacks for 2 next functions, failures are not so important
+	// (because monitored: teachers can see what's going on)
+
+	newConnection: function(aid, number)
+	{
+		//increment discoCount, reset discoTime to NULL, update totalDisco
+		AssessmentEntity.getDiscoTime(aid, number, (err,discoTime) => {
+			if (!!discoTime)
+				AssessmentEntity.addDisco(aid, number, discoTime - Date.now());
+		});
+	},
+
 	endSession: function(aid, number, password, cb)
 	{
 		AssessmentEntity.endAssessment(aid, number, password, (err,ret) => {
diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js
index aced6f2..ad309d7 100644
--- a/public/javascripts/assessment.js
+++ b/public/javascripts/assessment.js
@@ -157,7 +157,7 @@ let V = new Vue({
 						// Got password: students answers locked to this page until potential teacher
 						// action (power failure, computer down, ...)
 					}
-					socket = io.connect("/" + assessment.name, {
+					socket = io.connect("/", {
 						query: "aid=" + assessment._id + "&number=" + this.student.number + "&password=" + this.student.password
 					});
 					socket.on(message.allAnswers, this.setAnswers);
@@ -232,6 +232,7 @@ let V = new Vue({
 			{
 				this.stage = 4;
 				this.answers.showSolution = true;
+				this.answers.displayAll = true;
 				return;
 			}
 			$.ajax("/end/assessment", {
@@ -248,17 +249,19 @@ let V = new Vue({
 					assessment.conclusion = ret.conclusion;
 					this.stage = 3;
 					delete this.student["password"]; //unable to send new answers now
-					socket.disconnect();
-					socket = null;
 				},
 			});
 		},
 		// stage 3 --> 4 (on socket message "feedback")
 		setAnswers: function(m) {
-			for (let i=0; i<m.answers.length; i++)
-				assessment.questions[i].answer = m.answers[i];
+			const answers = JSON.parse(m.answers);
+			for (let i=0; i<answers.length; i++)
+				assessment.questions[i].answer = answers[i];
 			this.answers.showSolution = true;
+			this.answers.displayAll = true;
 			this.stage = 4;
+			socket.disconnect();
+			socket = null;
 		},
 	},
 });
diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js
index 07a0814..23972a9 100644
--- a/public/javascripts/monitor.js
+++ b/public/javascripts/monitor.js
@@ -105,7 +105,7 @@ new Vue({
 							});
 						}
 						// TODO: notations not coherent (input / answer... when, which ?)
-						this.assessment.papers[paperIdx].inputs.push(m.answer); //input+index
+						this.assessment.papers[paperIdx].inputs.push(JSON.parse(m.answer)); //input+index
 					});
 				},
 			});
@@ -114,7 +114,7 @@ new Vue({
 			// In the end, send answers to students
 			socket.emit(
 				message.allAnswers,
-				{ answers: this.assessment.questions.map( q => { return q.answer; }) }
+				{ answers: JSON.stringify(this.assessment.questions.map( q => { return q.answer; })) }
 			);
 		},
 	},
diff --git a/public/javascripts/utils/socketMessages.js b/public/javascripts/utils/socketMessages.js
index 4ffd05b..7c86fd8 100644
--- a/public/javascripts/utils/socketMessages.js
+++ b/public/javascripts/utils/socketMessages.js
@@ -1,10 +1,13 @@
 // Socket message list, easier life in case of a message renaming
 
 let message = {
-	// send answer (student --> server --> monitor)
+	// Send answer (student --> server --> monitor)
 	newAnswer: "new answer",
-	// receive all answers to an exam (server --> student)
+	// Receive all answers to an exam (monitor --> server --> student)
 	allAnswers: "all answers",
+	// Next 2 to monitor students disconnections
+	studentConnect: "student connect",
+	studentDisconnect: "student disconnect",
 };
 
 try { module.exports = message; } catch (err) {} //for server
diff --git a/public/javascripts/utils/validation.js b/public/javascripts/utils/validation.js
index 560bd5c..aa39ebe 100644
--- a/public/javascripts/utils/validation.js
+++ b/public/javascripts/utils/validation.js
@@ -26,6 +26,9 @@ Validator.Paper = {
 	"inputs": Validator.Input,
 	"startTime": "positiveInteger",
 	"endTime": "positiveInteger",
+	"discoTime": "positiveInteger",
+	"discoCount": "positiveInteger",
+	"totalDisco": "positiveInteger",
 	"password": "password",
 };
 
diff --git a/sockets.js b/sockets.js
index 2ce8ae9..9c230be 100644
--- a/sockets.js
+++ b/sockets.js
@@ -1,23 +1,20 @@
 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)
 {
-	io.of("/").on("connection", socket => {
+	io.of("/").on("connection", socket => { //student or monitor connexion
 		const aid = socket.handshake.query.aid;
-		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.join(aid + "_teacher");
 			socket.on(message.allAnswers, m => { //send feedback to student (answers)
-				socket.broadcast.to(aid).emit(message.allAnswers, m);
+				socket.broadcast.to(aid + "_student").emit(message.allAnswers, m);
 			});
 		}
 		else //student
@@ -27,9 +24,15 @@ module.exports = function(io)
 			AssessmentModel.checkPassword(ObjectId(aid), number, password, (err,ret) => {
 				if (!!err || !ret)
 					return; //wrong password, or some unexpected error...
-				socket.on("disconnect", () => {
-					//TODO: notify monitor (grey low opacity background)
-					//Also send to server: discoTime in assessment.papers ...
+				AssessmentModel.newConnection(ObjectId(aid), number);
+				socket.broadcast.to(aid + "_teacher").emit(message.studentConnect, {number: number});
+				socket.join(aid + "_student");
+				socket.on(message.newAnswer, m => { //got answer from student client
+					socket.broadcast.to(aid + "_teacher").emit(message.newAnswer, m);
+				});
+				socket.on("disconnect", () => { //notify monitor + server
+					AssessmentEntity.setDiscoTime(ObjectId(aid), number);
+					socket.broadcast.to(aid + "_teacher").emit(message.studentDisconnect, {number: number});
 				});
 			});
 		}
-- 
2.44.0