From e5ec7dead171feebb299430f298e3930864e096d Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 30 Jan 2018 12:00:41 +0100
Subject: [PATCH] early draft of sockets logic for monitoring

---
 TODO_assessment_template => TODO |   3 +
 public/javascripts/assessment.js |  24 ++--
 public/javascripts/monitor.js    | 221 +++++++++++++++++++++++++++++++
 sockets.js                       |  42 +++---
 views/monitor.pug                |  25 +++-
 5 files changed, 282 insertions(+), 33 deletions(-)
 rename TODO_assessment_template => TODO (91%)

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