From 71d1ca9c594b64d959c608a2abbff926480abad5 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 5 Feb 2018 00:07:42 +0100
Subject: [PATCH] Basic monitoring OK (sockets non-functional atm)

---
 README.md                                   |  3 +-
 TODO                                        | 10 ++-
 app.js                                      |  6 +-
 config/parameters.js.dist                   |  3 +
 models/assessment.js                        | 24 ++++--
 public/javascripts/assessment.js            | 52 ++++++------
 public/javascripts/components/statements.js |  8 +-
 public/javascripts/course.js                |  4 +-
 public/javascripts/grading.js               |  1 +
 public/javascripts/monitor.js               | 89 +++++++++++++++++----
 public/stylesheets/assessment.css           | 11 +++
 public/stylesheets/course.css               | 11 +++
 public/stylesheets/monitor.css              | 42 +++++++++-
 routes/assessments.js                       | 31 ++++++-
 routes/courses.js                           |  2 +
 routes/pages.js                             |  7 +-
 sockets.js                                  | 19 +----
 views/assessment.pug                        | 14 ++--
 views/course.pug                            |  2 +-
 views/grading.pug                           | 43 ++++++++++
 views/monitor.pug                           | 51 ++++++++----
 21 files changed, 334 insertions(+), 99 deletions(-)
 create mode 100644 public/javascripts/grading.js
 create mode 100644 views/grading.pug

diff --git a/README.md b/README.md
index d1a22b5..845524b 100644
--- 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.
 
-*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
diff --git a/TODO b/TODO
index 0d3b5e7..8670fcb 100644
--- 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)
diff --git a/app.js b/app.js
index 5e7fdfe..0fd56f9 100644
--- 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) {
-  // 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');
diff --git a/config/parameters.js.dist b/config/parameters.js.dist
index 82ca2ae..66e0bd1 100644
--- a/config/parameters.js.dist
+++ b/config/parameters.js.dist
@@ -6,6 +6,9 @@ Parameters.siteURL = "http://localhost";
 // 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,
diff --git a/models/assessment.js b/models/assessment.js
index 9269aee..9ab92ba 100644
--- a/models/assessment.js
+++ b/models/assessment.js
@@ -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
@@ -38,9 +50,9 @@ const AssessmentModel =
 
 	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
-		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) => {
@@ -51,7 +63,7 @@ const AssessmentModel =
 				// 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);
+			if (!paper && !!password)
+				return cb({errmsg: "Cannot start a new exam before finishing current"},null);
 			if (!!paper)
 			{
 				if (!password)
-					return cb({errmsg:"Missing password"});
+					return cb({errmsg: "Missing password"});
 				if (paper.password != password)
-					return cb({errmsg:"Wrong password"});
+					return cb({errmsg: "Wrong password"});
 			}
 			AssessmentEntity.getQuestions(aid, (err,questions) => {
 				if (!!err)
diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js
index 20ca2cb..09caf12 100644
--- a/public/javascripts/assessment.js
+++ b/public/javascripts/assessment.js
@@ -12,7 +12,7 @@ function checkWindowSize()
 	return window.innerWidth >= screen.width-3 && window.innerHeight >= screen.height-3;
 };
 
-new Vue({
+let V = new Vue({
 	el: "#assessment",
 	data: {
 		assessment: assessment,
@@ -62,7 +62,7 @@ new Vue({
 	},
 	methods: {
 		// In case of AJAX errors
-		warning: function(message) {
+		showWarning: function(message) {
 			this.warnMsg = message;
 			$("#warning").modal("open");
 		},
@@ -73,7 +73,7 @@ new Vue({
 		},
 		trySendCurrentAnswer: function() {
 			if (this.stage == 2)
-				this.sendAnswer(assessment.indices[assessment.index]);
+				this.sendAnswer();
 		},
 		// stage 0 --> 1
 		getStudent: function(cb) {
@@ -86,7 +86,7 @@ new Vue({
 				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(); });
@@ -114,7 +114,7 @@ new Vue({
 					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
@@ -129,7 +129,7 @@ new Vue({
 					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")
@@ -143,7 +143,7 @@ new Vue({
 				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
@@ -157,7 +157,7 @@ new Vue({
 						// 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);
@@ -171,30 +171,31 @@ new Vue({
 			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);
+				}
 			}, 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 = () => {
-				if (assessment.index == assessment.questions.length - 1)
+				if (this.answers.index == assessment.questions.length - 1)
 					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({
-					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; })
@@ -208,9 +209,8 @@ new Vue({
 				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);
 				},
 			});
@@ -235,7 +235,7 @@ new Vue({
 				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
@@ -245,9 +245,9 @@ new Vue({
 			});
 		},
 		// 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;
 		},
 	},
diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js
index 6ebea85..5bf6bdb 100644
--- a/public/javascripts/components/statements.js
+++ b/public/javascripts/components/statements.js
@@ -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
-		let domTree = this.questions.map( (q,i) => {
+		let domTree = (this.questions || [ ]).map( (q,i) => {
 			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),
-								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
@@ -93,7 +93,7 @@ Vue.component("statements", {
 				{
 					"class": {
 						"question": true,
-						"hide": !this.answers.displayAll && this.answers.index != i,
+						"hide": !this.answers.displayAll && this.answers.indices[this.answers.index] != i,
 					},
 				},
 				questionContent
@@ -106,7 +106,7 @@ Vue.component("statements", {
 					id: "statements",
 				},
 			},
-			questions
+			domTree
 		);
 	},
 	updated: function() {
diff --git a/public/javascripts/course.js b/public/javascripts/course.js
index 94e1b84..85172dc 100644
--- a/public/javascripts/course.js
+++ b/public/javascripts/course.js
@@ -340,8 +340,8 @@ window.onload = function() {
 					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
diff --git a/public/javascripts/grading.js b/public/javascripts/grading.js
new file mode 100644
index 0000000..5264d83
--- /dev/null
+++ b/public/javascripts/grading.js
@@ -0,0 +1 @@
+//TODO: similar to monitor / course (need to factor some code)
diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js
index 5d5f237..6c08d7a 100644
--- a/public/javascripts/monitor.js
+++ b/public/javascripts/monitor.js
@@ -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
-		assessment: null, //obtained after authentication
+		assessment: { }, //obtained after authentication
 		// Stage 0: unauthenticated (password),
 		//       1: authenticated (password hash validated), start monitoring
 		stage: 0,
@@ -22,23 +14,75 @@ new Vue({
 			inputs: [ ],
 			index : -1,
 		},
+		students: [ ], //to know their names
+		display: "assessment", //or student's answers
 	},
 	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: {
-					password: this.password,
+					password: Sha1.Compute(this.password),
 					aname: examName,
-					cname: courseName,
+					ccode: courseCode,
 					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
@@ -47,10 +91,27 @@ new Vue({
 						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; }) }
+			);
+		},
 	},
 });
diff --git a/public/stylesheets/assessment.css b/public/stylesheets/assessment.css
index b8a2409..2143f40 100644
--- a/public/stylesheets/assessment.css
+++ b/public/stylesheets/assessment.css
@@ -53,3 +53,14 @@ button#sendAnswer {
 .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;
+}
diff --git a/public/stylesheets/course.css b/public/stylesheets/course.css
index 2d61347..c1c4a9b 100644
--- a/public/stylesheets/course.css
+++ b/public/stylesheets/course.css
@@ -54,3 +54,14 @@ tr.stats {
 .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;
+}
diff --git a/public/stylesheets/monitor.css b/public/stylesheets/monitor.css
index 8f414f5..b585800 100644
--- a/public/stylesheets/monitor.css
+++ b/public/stylesheets/monitor.css
@@ -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;
+}
diff --git a/routes/assessments.js b/routes/assessments.js
index dc749ed..a107d7e 100644
--- a/routes/assessments.js
+++ b/routes/assessments.js
@@ -9,8 +9,12 @@ const validator = require("../public/javascripts/utils/validation");
 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) => {
@@ -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"];
diff --git a/routes/courses.js b/routes/courses.js
index d221858..e07e243 100644
--- a/routes/courses.js
+++ b/routes/courses.js
@@ -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;
diff --git a/routes/pages.js b/routes/pages.js
index 15cf1fd..f6c77a2 100644
--- a/routes/pages.js
+++ b/routes/pages.js
@@ -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"];
+	// TODO: if (main) teacher, also send secret, saving one request
 	res.render("monitor", {
 		title: "monitor assessment " + code + "/" + name,
 		initials: initials,
-		code: code,
-		name: name,
+		courseCode: code,
+		examName: name,
 	});
 });
 
diff --git a/sockets.js b/sockets.js
index eeda127..2ce8ae9 100644
--- a/sockets.js
+++ b/sockets.js
@@ -1,6 +1,6 @@
 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)
@@ -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;
+
 		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;
-			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...
-				// 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 ...
diff --git a/views/assessment.pug b/views/assessment.pug
index b8ed733..0a7e369 100644
--- a/views/assessment.pug
+++ b/views/assessment.pug
@@ -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!
-				#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")
-				#stage2_4(v-show="stage==2 || stage==4")
+				#stage2_4(v-show="[2,4].includes(stage)")
 					if assessment.time > 0
-						.card
-							.timer.center(v-if="stage==2") {{ countdown }}
+						.card(v-if="stage==2")
+							.timer.center {{ countdown }}
 					.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!
-				#stage3_4(v-show="stage==3 || stage==4")
+				#stage3_4(v-show="[3,4].includes(stage)")
 					.card
 						.conclusion(v-html="assessment.conclusion")
 
@@ -60,5 +60,7 @@ block append javascripts
 	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")
diff --git a/views/course.pug b/views/course.pug
index 17bad0d..aa412e2 100644
--- a/views/course.pug
+++ b/views/course.pug
@@ -144,7 +144,7 @@ block content
 							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
diff --git a/views/grading.pug b/views/grading.pug
new file mode 100644
index 0000000..58dc5e0
--- /dev/null
+++ b/views/grading.pug
@@ -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")
diff --git a/views/monitor.pug b/views/monitor.pug
index 5a64e89..339ca47 100644
--- a/views/monitor.pug
+++ b/views/monitor.pug
@@ -1,15 +1,10 @@
 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
-	.container#assessment
+	.container#monitor
 		.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")
-					.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.
+		const examName = "#{examName}";
+		const courseCode = "#{courseCode}";
+		const initials = "#{initials}";
 		const monitoring = true;
+	script(src="/javascripts/utils/libsRefresh.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")
-- 
2.44.0