From a80c6a3b87f75653725f54caca1f24abc556afc7 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 13 Feb 2018 23:32:29 +0100
Subject: [PATCH] thoughts about time, refactor statements component

---
 models/assessment.js                        |   7 +-
 public/javascripts/assessment.js            |  60 +++---
 public/javascripts/components/statements.js | 202 +++++++++++++-------
 3 files changed, 172 insertions(+), 97 deletions(-)

diff --git a/models/assessment.js b/models/assessment.js
index c7cb0fd..fd0133c 100644
--- a/models/assessment.js
+++ b/models/assessment.js
@@ -11,11 +11,11 @@ const AssessmentModel =
 	 *   _id: BSON id
 	 *   cid: course ID
 	 *   name: varchar
-	 *   active: boolean true/false
-	 *   mode: secure | exam | open (decreasing security)
+	 *   active: boolean
+	 *   mode: secure | watch | exam | open (decreasing security)
 	 *   fixed: bool (questions in fixed order; default: false)
 	 *   display: "one" or "all" (generally "all" for open questions, but...)
-	 *   time: 0, //<=0 means "untimed"; otherwise, time in seconds
+	 *   time: 0, global (one vaue) or per question (array of integers)
 	 *   introduction: "",
 	 *   coefficient: number, default 1
 	 *   questions: array of
@@ -26,7 +26,6 @@ const AssessmentModel =
 	 *     answer: array of integers (for quiz) or html text (for paper); striped in exam mode
 	 *     active: boolean, is question in current assessment?
 	 *     points: points for this question (default 1)
-	 *     time: 0 (<=0: untimed)
 	 *   papers : array of
 	 *     number: student number
 	 *     inputs: array of {index,answer[array of integers or html text],startTime}
diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js
index 93200cc..9275879 100644
--- a/public/javascripts/assessment.js
+++ b/public/javascripts/assessment.js
@@ -23,19 +23,19 @@ new Vue({
 		//       2: locked: password set, exam started
 		//       3: completed
 		//       4: show answers
+		remainingTime: assessment.time, //integer or array
 		stage: assessment.mode != "open" ? 0 : 1,
-		remainingTime: 0, //global, in seconds
 		warnMsg: "",
 	},
 	computed: {
 		countdown: function() {
-			let seconds = this.remainingTime % 60;
-			let minutes = Math.floor(this.remainingTime / 60);
+			const remainingTime = assessment.display == "one" && _.isArray(assessment.time)
+				? this.remainingTime[this.answers.index]
+				: this.remainingTime;
+			let seconds = remainingTime % 60;
+			let minutes = Math.floor(remainingTime / 60);
 			return this.padWithZero(minutes) + ":" + this.padWithZero(seconds);
 		},
-		showAnswers: function() {
-			return this.stage == 4;
-		},
 	},
 	mounted: function() {
 		$(".modal").modal();
@@ -46,7 +46,7 @@ new Vue({
 				return;
 			if (assessment.mode == "secure")
 			{
-				this.trySendCurrentAnswer();
+				this.sendAnswer();
 				document.location.href= "/noblur";
 			}
 			else //"watch" mode
@@ -65,7 +65,7 @@ new Vue({
 				return;
 			if (assessment.mode == "secure")
 			{
-				this.trySendCurrentAnswer();
+				this.sendAnswer();
 				document.location.href = "/fullscreen";
 			}
 			else //"watch" mode
@@ -78,7 +78,7 @@ new Vue({
 		}, false);
 	},
 	methods: {
-		// In case of AJAX errors
+		// In case of AJAX errors (not blur-ing)
 		showWarning: function(message) {
 			this.warnMsg = message;
 			$("#warning").modal("open");
@@ -88,12 +88,8 @@ new Vue({
 				return "0" + x;
 			return x;
 		},
-		trySendCurrentAnswer: function() {
-			if (this.stage == 2)
-				this.sendAnswer();
-		},
 		// stage 0 --> 1
-		getStudent: function(cb) {
+		getStudent: function() {
 			$.ajax("/get/student", {
 				method: "GET",
 				data: {
@@ -107,8 +103,6 @@ new Vue({
 					this.stage = 1;
 					this.student = s.student;
 					Vue.nextTick( () => { Materialize.updateTextFields(); });
-					if (!!cb)
-						cb();
 				},
 			});
 		},
@@ -118,18 +112,14 @@ new Vue({
 		},
 		// stage 1 --> 2 (get all questions, set password)
 		startAssessment: function() {
-			let initializeStage2 = (questions,paper) => {
+			let initializeStage2 = paper => {
 				$("#leftButton, #rightButton").hide();
-				if (assessment.time > 0)
-				{
-
-// TODO: distinguish total exam time AND question time
-
-					const deltaTime = !!paper ? Date.now() - paper.startTime : 0;
-					this.remainingTime = assessment.time * 60 - Math.round(deltaTime / 1000);
-					this.runTimer();
-				}
 				// Initialize structured answer(s) based on questions type and nesting (TODO: more general)
+				
+				// if display == "all" getQuestionS
+				// otherwise get first question
+				
+				
 				if (!!questions)
 					assessment.questions = questions;
 				this.answers.inputs = [ ];
@@ -148,6 +138,24 @@ new Vue({
 					let remainingIndices = _.difference( _.range(assessment.questions.length).map(String), indices );
 					this.answers.indices = indices.concat( _.shuffle(remainingIndices) );
 				}
+
+
+
+
+
+
+
+				if (assessment.time > 0)
+				{
+
+// TODO: distinguish total exam time AND question time
+
+					const deltaTime = !!paper ? Date.now() - paper.startTime : 0;
+					this.remainingTime = assessment.time * 60 - Math.round(deltaTime / 1000);
+					this.runTimer();
+				}
+
+
 				this.answers.index = !!paper ? paper.inputs.length : 0;
 				this.answers.displayAll = assessment.display == "all";
 				this.answers.showSolution = false;
diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js
index 0fb5124..900a5e7 100644
--- a/public/javascripts/components/statements.js
+++ b/public/javascripts/components/statements.js
@@ -17,26 +17,37 @@ Imaginary example: (using math.js)
 */
 
 Vue.component("statements", {
-	// 'answers' is an object containing
-	//   'inputs'(array),
-	//   'displayAll'(bool), //TODO: should be in questions!
-	//   'showSolution'(bool),
-	//   'indices': order of appearance
-	//   'index': current integer index (focused question)
-	props: ['questions','answers'],
-	// TODO: general render function for nested exercises
-	// There should be a questions navigator below, or next (visible if display=='all')
+	// 'inputs': array of index (as in questions) + input (text or array of ints)
+	// display: 'all', 'one', 'solution'
+	// iidx: current level-0 integer index (can match a group of questions / inputs)
+	props: ['questions','inputs','display','iidx'],
+	data: function() {
+		return {
+			displayStyle: "compact", //or "all": all on same page
+		};
+	}
 	// 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 questionContent = [ ];
+			questionContent.push(
+				h(
+					"h4",
+					{
+						"class": {
+							"questionIndex": true,
+						}
+					},
+					q.index
+				)
+			);
 			questionContent.push(
 				h(
 					"div",
 					{
 						"class": {
 							wording: true,
+
 						},
 						domProps: {
 							innerHTML: q.wording,
@@ -44,82 +55,141 @@ Vue.component("statements", {
 					}
 				)
 			);
-			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.answers.inputs.length > 0 && this.answers.inputs[i][idx],
-								disabled: monitoring,
-							},
-							attrs: {
-								id: this.inputId(i,idx),
-								type: "checkbox",
-							},
-							on: {
-								change: e => { this.answers.inputs[i][idx] = e.target.checked; },
-							},
-						},
-						[ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
-					)
-				);
-				option.push(
-					h(
-						"label",
-						{
-							domProps: {
-								innerHTML: q.options[idx],
+			if (!!q.options)
+			{
+				// quiz-like question
+				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.answers.inputs.length > 0 && this.answers.inputs[i][idx],
+									disabled: monitoring,
+								},
+								attrs: {
+									id: this.inputId(i,idx),
+									type: "checkbox",
+								},
+								on: {
+									change: e => { this.answers.inputs[i][idx] = e.target.checked; },
+								},
 							},
-							attrs: {
-								"for": this.inputId(i,idx),
+							[ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
+						)
+					);
+					option.push(
+						h(
+							"label",
+							{
+								domProps: {
+									innerHTML: q.options[idx],
+								},
+								attrs: {
+									"for": this.inputId(i,idx),
+								},
+							}
+						)
+					);
+					optionList.push(
+						h(
+							"div",
+							{
+								"class": {
+									option: true,
+									choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx),
+									choiceWrong: this.answers.showSolution && this.answers.inputs[i][idx] && !q.answer.includes(idx),
+								},
 							},
-						}
-					)
-				);
-				optionList.push(
+							option
+						)
+					);
+				});
+				questionContent.push(
 					h(
 						"div",
 						{
 							"class": {
-								option: true,
-								choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx),
-								choiceWrong: this.answers.showSolution && this.answers.inputs[i][idx] && !q.answer.includes(idx),
+								optionList: true,
 							},
 						},
-						option
+						optionList
 					)
 				);
-			});
-			questionContent.push(
-				h(
-					"div",
-					{
-						"class": {
-							optionList: true,
-						},
-					},
-					optionList
-				)
-			);
-			if (this.answers.displayAll && i < this.questions.length-1)
+			}
+			if (this.display == "all" && !this.navigator && i < this.questions.length-1)
 				questionContent.push( h("hr") );
+			const depth = (q.index.match(/\./g) || []).length;
 			return h(
 				"div",
 				{
 					"class": {
 						"question": true,
-						"hide": !this.answers.displayAll && this.answers.indices[this.answers.index] != i,
+						"hide": this.display == "one" && this.iidx != i,
+						"depth" + depth: true,
 					},
 				},
 				questionContent
 			);
 		});
+		const navigator = h(
+			"div",
+			{
+				"class": {
+					"hide": this.displayStyle == "all"
+				},
+			},
+			[
+				h(
+					"button",
+					{
+						"class": {
+							"btn": true,
+						},
+						on: {
+							click: () => {
+								this.index = Math.max(0, this.index - 1);
+							},
+						},
+					},
+					[ h("span", { "class": { "material-icon": true } }, "fast_rewind") ]
+				), //onclick: index = max(0,index-1)
+				h("span",{ },(this.iidx+1).toString()),
+				h(
+					"button",
+					{
+						"class": {
+							"btn": true,
+						},
+						on: {
+							click: () => {
+								this.index = Math.min(this.index+1, this.questions.length-1)
+							},
+						},
+					},
+					[ h("span", { "class": { "material-icon": true } }, "fast_forward") ]
+				)
+			]
+		);
+		domTree.push(navigator);
+		domTree.push(
+			h(
+				"button",
+				{
+					on: {
+						click: () => {
+							this.displayStyle = displayStyle == "compact" ? "all" : "compact";
+						},
+					},
+				},
+				this.displayStyle == "compact" ? "Show all" : "Navigator"
+			)
+		);
 		return h(
 			"div",
 			{
@@ -134,8 +204,6 @@ Vue.component("statements", {
 		statementsLibsRefresh();
 	},
 	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: {
-- 
2.44.0