From 435371c7ba4b60790953115b9ebed68a047bb0a3 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 30 Jan 2018 22:04:05 +0100
Subject: [PATCH] Prepare monitoring using Statements component (early draft
 stage)

---
 public/javascripts/assessment.js            | 277 +++++---------------
 public/javascripts/components/statements.js | 115 ++++++++
 public/javascripts/monitor.js               | 152 -----------
 public/javascripts/utils/libsRefresh.js     |   8 +
 views/assessment.pug                        |   5 +-
 views/monitor.pug                           |   3 +
 6 files changed, 194 insertions(+), 366 deletions(-)
 create mode 100644 public/javascripts/components/statements.js
 create mode 100644 public/javascripts/utils/libsRefresh.js

diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js
index c014b5a..5019840 100644
--- a/public/javascripts/assessment.js
+++ b/public/javascripts/assessment.js
@@ -1,6 +1,3 @@
-// TODO: if display == "all", les envois devraient être non définitifs (possibilité de corriger)
-// Et, blur sur une (sous-)question devrait envoyer la version courante de la sous-question
-
 let socket = null; //monitor answers in real time
 
 if (assessment.mode == "secure" && !checkWindowSize())
@@ -15,15 +12,6 @@ function checkWindowSize()
 	return window.innerWidth >= screen.width-3 && window.innerHeight >= screen.height-3;
 };
 
-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: "#assessment",
 	data: {
@@ -39,207 +27,6 @@ new Vue({
 		remainingTime: 0, //global, in seconds
 		warnMsg: "",
 	},
-	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() {
-				if (assessment.mode != "secure")
-					return;
-				window.addEventListener("keydown", e => {
-					// 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 (e.keyCode == 123)
-						e.preventDefault();
-				}, false);
-				window.addEventListener("blur", () => {
-					this.trySendCurrentAnswer();
-					document.location.href= "/noblur";
-				}, false);
-				window.addEventListener("resize", e => {
-					this.trySendCurrentAnswer();
-					document.location.href= "/fullscreen";
-				}, false);
-			},
-			updated: function() {
-				libsRefresh(); //TODO: shouldn't be required: "MathJax" strings on start and assign them to assessment.questions. ...
-			},
-			methods: {
-				inputId: function(i,j) {
-					return "q" + i + "_" + "input" + j;
-				},
-				trySendCurrentAnswer: function() {
-					if (this.stage == 2)
-						this.sendAnswer(assessment.indices[assessment.index]);
-				},
-				// stage 2
-				sendAnswer: function(realIndex) {
-					let gotoNext = () => {
-						if (assessment.index == assessment.questions.length - 1)
-							this.$emit("gameover");
-						else
-							assessment.index++;
-						this.$forceUpdate(); //TODO: 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]
-								.map( (tf,i) => { return {val:tf,idx:i}; } )
-								.filter( item => { return item.val; })
-								.map( item => { return item.idx; })
-						}),
-						number: this.student.number,
-						password: this.student.password,
-					};
-					$.ajax("/send/answer", {
-						method: "GET",
-						data: answerData,
-						dataType: "json",
-						success: ret => {
-							if (!!ret.errmsg)
-								return this.$emit("warning", ret.errmsg);
-							else
-								gotoNext();
-							socket.emit(message.newAnswer, answerData);
-						},
-					});
-				},
-			},
-		},
-	},
 	computed: {
 		countdown: function() {
 			let seconds = this.remainingTime % 60;
@@ -249,6 +36,31 @@ new Vue({
 	},
 	mounted: function() {
 		$(".modal").modal();
+		if (assessment.mode != "secure")
+			return;
+		window.addEventListener("keydown", e => {
+			// 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 (e.keyCode == 123)
+				e.preventDefault();
+		}, false);
+		window.addEventListener("blur", () => {
+			this.trySendCurrentAnswer();
+			document.location.href= "/noblur";
+		}, false);
+		window.addEventListener("resize", e => {
+			this.trySendCurrentAnswer();
+			document.location.href= "/fullscreen";
+		}, false);
+	},
+		trySendCurrentAnswer: function() {
+			if (this.stage == 2)
+				this.sendAnswer(assessment.indices[assessment.index]);
+		},
 	},
 	methods: {
 		// In case of AJAX errors
@@ -361,6 +173,45 @@ new Vue({
 					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) {
+			let gotoNext = () => {
+				if (assessment.index == assessment.questions.length - 1)
+					this.$emit("gameover");
+				else
+					assessment.index++;
+				this.$forceUpdate(); //TODO: 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]
+						.map( (tf,i) => { return {val:tf,idx:i}; } )
+						.filter( item => { return item.val; })
+						.map( item => { return item.idx; })
+				}),
+				number: this.student.number,
+				password: this.student.password,
+			};
+			$.ajax("/send/answer", {
+				method: "GET",
+				data: answerData,
+				dataType: "json",
+				success: ret => {
+					if (!!ret.errmsg)
+						return this.$emit("warning", ret.errmsg);
+					else
+						gotoNext();
+					socket.emit(message.newAnswer, answerData);
+				},
+			});
+		},
 		// stage 2 --> 3 (or 4)
 		// from a message by statements component, or time over
 		endAssessment: function() {
diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js
new file mode 100644
index 0000000..d93a97f
--- /dev/null
+++ b/public/javascripts/components/statements.js
@@ -0,0 +1,115 @@
+Vue.component("statements", {
+	props: ['questions','inputs','showAnswers','index'], // index=-1 : show all, otherwise show current question
+	// 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 domTree = this.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],
+								disabled: monitoring,
+							},
+							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: showAnswers && this.questions[i].answer.includes(idx),
+								choiceWrong: showAnswers && this.inputs[i][idx] && !questions[i].answer.includes(idx),
+							},
+						},
+						option
+					)
+				);
+			});
+			questionContent.push(
+				h(
+					"div",
+					{
+						"class": {
+							optionList: true,
+						},
+					},
+					optionList
+				)
+			);
+			return h(
+				"div",
+				{
+					"class": {
+						"question": true,
+						"hide": index >= 0 && index != i,
+					},
+				},
+				questionContent
+			);
+		});
+		return h(
+			"div",
+			{
+				attrs: {
+					id: "statements",
+				},
+			},
+			questions
+		);
+	},
+	updated: function() {
+		// TODO: next line shouldn't be required: questions wordings + answer + options
+		// are processed earlier; their content should be updated at this time.
+		statementsLibsRefresh();
+	},
+	methods: {
+		inputId: function(i,j) {
+			return "q" + i + "_" + "input" + j;
+		},
+	},
+});
diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js
index 1d32d0c..9f43ce0 100644
--- a/public/javascripts/monitor.js
+++ b/public/javascripts/monitor.js
@@ -1,5 +1,3 @@
-// UNIMPLEMENTED
-
 // 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)
@@ -10,19 +8,8 @@
 
 // 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: {
@@ -32,145 +19,6 @@ new Vue({
 		//       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() {
diff --git a/public/javascripts/utils/libsRefresh.js b/public/javascripts/utils/libsRefresh.js
new file mode 100644
index 0000000..4bdb7a0
--- /dev/null
+++ b/public/javascripts/utils/libsRefresh.js
@@ -0,0 +1,8 @@
+function statementsLibsRefresh()
+{
+	// Run Prism + MathJax on questions text
+	$("#statements").find("code[class^=language-]").each( (i,elem) => {
+		Prism.highlightElement(elem);
+	});
+	MathJax.Hub.Queue(["Typeset",MathJax.Hub,"statements"]);
+}
diff --git a/views/assessment.pug b/views/assessment.pug
index c5395d6..94b87ae 100644
--- a/views/assessment.pug
+++ b/views/assessment.pug
@@ -47,7 +47,8 @@ block content
 						.card
 							.timer.center(v-if="stage==2") {{ countdown }}
 					.card
-						statements(:assessment="assessment" :student="student" :stage="stage" :inputs="inputs" @gameover="endAssessment" @warning="warning")
+						button.waves-effect.waves-light.btn(style="display:block;margin:0 auto" @click="sendAnswer") Send
+						statements(:questions="assessment.questions" :showAnswers="showAnswers" :index="index" :inputs="inputs" @gameover="endAssessment")
 				#stage3(v-show="stage==3")
 					.card
 						.finish Exam completed &#9786; ...don't close the window!
@@ -58,4 +59,6 @@ block content
 block append javascripts
 	script.
 		let assessment = !{JSON.stringify(assessment)};
+		const monitoring = false;
+	script(src="/javascripts/components/statements.js")
 	script(src="/javascripts/assessment.js")
diff --git a/views/monitor.pug b/views/monitor.pug
index 6c9cf6a..f4f2e3c 100644
--- a/views/monitor.pug
+++ b/views/monitor.pug
@@ -28,4 +28,7 @@ block content
 						.conclusion(v-html="assessment.conclusion")
 
 block append javascripts
+	script.
+		const monitoring = true;
+	script(src="/javascripts/components/statements.js")
 	script(src="/javascripts/monitor.js")
-- 
2.44.0