From 73609d3bc662cf4c8a21746c5d1ad736ea0eecbd Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 14 Feb 2018 12:49:30 +0100
Subject: [PATCH] 'update'

---
 TODO                                        |   2 +
 models/assessment.js                        |   1 +
 public/javascripts/assessment.js            |  14 +-
 public/javascripts/components/statements.js | 208 +++++++++++---------
 public/javascripts/course.js                |  20 +-
 public/javascripts/courseList.js            |   8 +-
 public/javascripts/login.js                 |   7 +-
 public/javascripts/monitor.js               |   2 +-
 public/stylesheets/statements.css           |   8 +
 routes/assessments.js                       |  34 ++--
 routes/courses.js                           |  20 +-
 routes/users.js                             |  27 ++-
 12 files changed, 196 insertions(+), 155 deletions(-)
 create mode 100644 TODO

diff --git a/TODO b/TODO
new file mode 100644
index 0000000..0d7d98e
--- /dev/null
+++ b/TODO
@@ -0,0 +1,2 @@
+views: course, monitor, grade
+js: monitor (see details), assessment, course, grade
diff --git a/models/assessment.js b/models/assessment.js
index fd0133c..de3d2d7 100644
--- a/models/assessment.js
+++ b/models/assessment.js
@@ -26,6 +26,7 @@ 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)
+	 *     param: parameter (if applicable)
 	 *   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 9275879..2a451e4 100644
--- a/public/javascripts/assessment.js
+++ b/public/javascripts/assessment.js
@@ -90,7 +90,7 @@ new Vue({
 		},
 		// stage 0 --> 1
 		getStudent: function() {
-			$.ajax("/get/student", {
+			$.ajax("/courses/student", {
 				method: "GET",
 				data: {
 					number: this.student.number,
@@ -163,8 +163,8 @@ new Vue({
 			};
 			if (assessment.mode == "open")
 				return initializeStage2();
-			$.ajax("/start/assessment", {
-				method: "GET",
+			$.ajax("/assessments/start", {
+				method: "PUT",
 				data: {
 					number: this.student.number,
 					aid: assessment._id
@@ -251,8 +251,8 @@ new Vue({
 				number: this.student.number,
 				password: this.student.password,
 			};
-			$.ajax("/send/answer", {
-				method: "GET",
+			$.ajax("/assessments/answer", {
+				method: "PUT",
 				data: answerData,
 				dataType: "json",
 				success: ret => {
@@ -282,8 +282,8 @@ new Vue({
 				this.answers.displayAll = true;
 				return;
 			}
-			$.ajax("/end/assessment", {
-				method: "GET",
+			$.ajax("/assessments/end", {
+				method: "PUT",
 				data: {
 					aid: assessment._id,
 					number: this.student.number,
diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js
index 900a5e7..64057f9 100644
--- a/public/javascripts/components/statements.js
+++ b/public/javascripts/components/statements.js
@@ -14,10 +14,14 @@ Imaginary example: (using math.js)
 	<div>Calculer le déterminant de 
 	$$\begin{matrix}7 & x\\y & -3\end{matrix}$$</div>
 	* ...
+
+--> input of type text (number, or vector, or matrix e.g. in R syntax)
+--> parameter stored in question.param (TODO)
+
 */
 
 Vue.component("statements", {
-	// 'inputs': array of index (as in questions) + input (text or array of ints)
+	// 'inputs': object with key = question index and value = text or boolean array
 	// display: 'all', 'one', 'solution'
 	// iidx: current level-0 integer index (can match a group of questions / inputs)
 	props: ['questions','inputs','display','iidx'],
@@ -28,113 +32,139 @@ Vue.component("statements", {
 	}
 	// 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(
-					"h4",
-					{
-						"class": {
-							"questionIndex": true,
-						}
-					},
-					q.index
-				)
-			);
-			questionContent.push(
-				h(
-					"div",
-					{
-						"class": {
-							wording: true,
-
-						},
-						domProps: {
-							innerHTML: q.wording,
+		// Prepare questions groups, ordered
+		let questions = this.questions || [ ]
+		let questionGroups = _.groupBy(questions, q => {
+			const dotPos = q.index.indexOf(".");
+			return dotPos === -1 ? q.index : q.index.substring(0,dotPos);
+		});
+		let domTree = questionGroups.map( (qg,i) => {
+			// Re-order questions 1.1.1 then 1.1.2 then...
+			const orderedQg = qg.sort( (a,b) => {
+				let aParts = a.split('.').map(Number);
+				let bParts = b.split('.').map(Number);
+				const La = aParts.length, Lb = bParts.length;
+				for (let i=0; i<Math.min(La,Lb); i++)
+				{
+					if (aParts[i] != bParts[i])
+						return aParts[i] - bParts[i];
+				}
+				return La - Lb; //the longer should appear after
+			});
+			let qgDom = orderedQg.map( q => {
+				let questionContent = [ ];
+				questionContent.push(
+					h(
+						"h4",
+						{
+							"class": {
+								"questionIndex": true,
+							}
 						},
-					}
-				)
-			);
-			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; },
-								},
+						q.index
+					)
+				);
+				questionContent.push(
+					h(
+						"div",
+						{
+							"class": {
+								wording: true,
 							},
-							[ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
-						)
-					);
-					option.push(
-						h(
-							"label",
-							{
-								domProps: {
-									innerHTML: q.options[idx],
+							domProps: {
+								innerHTML: q.wording,
+							},
+						}
+					)
+				);
+				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.inputs[q.index][idx],
+										disabled: monitoring,
+									},
+									attrs: {
+										id: this.inputId(q.index,idx),
+										type: "checkbox",
+									},
+									on: {
+										change: e => { this.inputs[q.index][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(q.index,idx),
+									},
+								}
+							)
+						);
+						optionList.push(
+							h(
+								"div",
+								{
+									"class": {
+										option: true,
+										choiceCorrect: this.display == "solution" && q.answer.includes(idx),
+										choiceWrong: this.display == "solution" && this.inputs[q.index][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,
-							},
+				}
+				const depth = (q.index.match(/\./g) || []).length;
+				return h(
+					"div",
+					{
+						"class": {
+							"question": true,
+							"depth" + depth: true,
 						},
-						optionList
-					)
+					},
+					questionContent
 				);
-			}
-			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,
+						"questionGroup": true,
 						"hide": this.display == "one" && this.iidx != i,
-						"depth" + depth: true,
 					},
-				},
-				questionContent
+				}
+				qgDom
 			);
 		});
 		const navigator = h(
@@ -158,7 +188,7 @@ Vue.component("statements", {
 						},
 					},
 					[ h("span", { "class": { "material-icon": true } }, "fast_rewind") ]
-				), //onclick: index = max(0,index-1)
+				),
 				h("span",{ },(this.iidx+1).toString()),
 				h(
 					"button",
diff --git a/public/javascripts/course.js b/public/javascripts/course.js
index 0e6a169..ab17fe3 100644
--- a/public/javascripts/course.js
+++ b/public/javascripts/course.js
@@ -91,8 +91,8 @@ new Vue({
 							d.number = d.number.toString();
 						students.push(d);
 					});
-					$.ajax("/import/students", {
-						method: "POST",
+					$.ajax("/courses/student-list", {
+						method: "PUT",
 						data: {
 							cid: this.course._id,
 							students: JSON.stringify(students),
@@ -118,9 +118,9 @@ new Vue({
 				return alert(error);
 			else
 				$('#newAssessment').modal('close');
-			$.ajax("/add/assessment",
+			$.ajax("/assessments",
 				{
-					method: "GET",
+					method: "POST",
 					data: {
 						name: this.newAssessment.name,
 						cid: course._id,
@@ -143,8 +143,8 @@ new Vue({
 			Materialize.updateTextFields(); //textareas, time field...
 		},
 		updateAssessment: function() {
-			$.ajax("/update/assessment", {
-				method: "POST",
+			$.ajax("/assessments", {
+				method: "PUT",
 				data: {assessment: JSON.stringify(this.assessment)},
 				dataType: "json",
 				success: res => {
@@ -163,9 +163,9 @@ new Vue({
 				return;
 			if (confirm("Delete assessment '" + assessment.name + "' ?"))
 			{
-				$.ajax("/remove/assessment",
+				$.ajax("/assessments",
 					{
-						method: "GET",
+						method: "DELETE",
 						data: { qid: this.assessment._id },
 						dataType: "json",
 						success: res => {
@@ -267,9 +267,9 @@ new Vue({
 			let error = Validator.checkObject({password:hashPwd}, "Course");
 			if (error.length > 0)
 				return alert(error);
-			$.ajax("/set/password",
+			$.ajax("/courses/password",
 				{
-					method: "GET",
+					method: "PUT",
 					data: {
 						cid: this.course._id,
 						pwd: hashPwd,
diff --git a/public/javascripts/courseList.js b/public/javascripts/courseList.js
index 020af55..d90e201 100644
--- a/public/javascripts/courseList.js
+++ b/public/javascripts/courseList.js
@@ -23,9 +23,9 @@ new Vue({
 				return alert(error);
 			else
 				$('#newCourse').modal('close');
-			$.ajax("/add/course",
+			$.ajax("/courses",
 				{
-					method: "GET",
+					method: "POST",
 					data: this.newCourse,
 					dataType: "json",
 					success: res => {
@@ -45,9 +45,9 @@ new Vue({
 			if (!admin)
 				return;
 			if (confirm("Delete course '" + course.code + "' ?"))
-				$.ajax("/remove/course",
+				$.ajax("/courses",
 					{
-						method: "GET",
+						method: "DELETE",
 						data: { cid: course._id },
 						dataType: "json",
 						success: res => {
diff --git a/public/javascripts/login.js b/public/javascripts/login.js
index 8752ba8..d5cd3ac 100644
--- a/public/javascripts/login.js
+++ b/public/javascripts/login.js
@@ -8,6 +8,11 @@ const ajaxUrl = {
 	"register": "/register",
 };
 
+const ajaxMethod = {
+	"login": "PUT",
+	"register": "POST",
+};
+
 const infos = {
 	"login": "Connection token sent. Check your emails!",
 	"register": "Registration complete! Please check your emails.",
@@ -58,7 +63,7 @@ new Vue({
 			showMsg($dialog, "process", "Processing... Please wait");
 			$.ajax(ajaxUrl[this.stage],
 				{
-					method: "GET",
+					method: ajaxMethod[this.stage],
 					data:
 					{
 						email: encodeURIComponent(this.user.email),
diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js
index 6ee6a41..29ce858 100644
--- a/public/javascripts/monitor.js
+++ b/public/javascripts/monitor.js
@@ -82,7 +82,7 @@ new Vue({
 		},
 		// stage 0 --> 1
 		startMonitoring: function() {
-			$.ajax("/start/monitoring", {
+			$.ajax("/assessments/monitor", {
 				method: "GET",
 				data: {
 					password: Sha1.Compute(this.password),
diff --git a/public/stylesheets/statements.css b/public/stylesheets/statements.css
index 169e67d..5ff0113 100644
--- a/public/stylesheets/statements.css
+++ b/public/stylesheets/statements.css
@@ -3,11 +3,19 @@ button.sendAnswer {
 	margin: 0 auto;
 }
 
+.questionGroup {
+	/* TODO */
+}
+
 .question {
 	margin: 20px 5px;
 	padding: 15px 0;
 }
 
+.question:not(:last-child) {
+	border-bottom: 1px solid black;
+}
+
 .question label {
 	color: black;
 }
diff --git a/routes/assessments.js b/routes/assessments.js
index d9f83ea..03e483e 100644
--- a/routes/assessments.js
+++ b/routes/assessments.js
@@ -17,9 +17,9 @@ const sanitizeOpts = {
 	},
 };
 
-router.get("/add/assessment", access.ajax, access.logged, (req,res) => {
-	const name = req.query["name"];
-	const cid = req.query["cid"];
+router.post("/assessments", access.ajax, access.logged, (req,res) => {
+	const name = req.body["name"];
+	const cid = req.body["cid"];
 	let error = validator({cid:cid, name:name}, "Assessment");
 	if (error.length > 0)
 		return res.json({errmsg:error});
@@ -30,7 +30,7 @@ router.get("/add/assessment", access.ajax, access.logged, (req,res) => {
 	});
 });
 
-router.post("/update/assessment", access.ajax, access.logged, (req,res) => {
+router.put("/assessments", access.ajax, access.logged, (req,res) => {
 	const assessment = JSON.parse(req.body["assessment"]);
 	let error = validator(assessment, "Assessment");
 	if (error.length > 0)
@@ -50,9 +50,9 @@ router.post("/update/assessment", access.ajax, access.logged, (req,res) => {
 });
 
 // Generate and set student password, return it
-router.get("/start/assessment", access.ajax, (req,res) => {
-	let number = req.query["number"];
-	let aid = req.query["aid"];
+router.put("/assessments/start", access.ajax, (req,res) => {
+	let number = req.body["number"];
+	let aid = req.body["aid"];
 	let password = req.cookies["password"]; //potentially from cookies, resuming
 	let error = validator({ _id:aid, papers:[{number:number,password:password || "samplePwd"}] }, "Assessment");
 	if (error.length > 0)
@@ -72,7 +72,7 @@ router.get("/start/assessment", access.ajax, (req,res) => {
 	});
 });
 
-router.get("/start/monitoring", access.ajax, (req,res) => {
+router.get("/assessments/monitor", access.ajax, (req,res) => {
 	const password = req.query["password"];
 	const examName = req.query["aname"];
 	const courseCode = req.query["ccode"];
@@ -95,11 +95,11 @@ router.get("/start/monitoring", access.ajax, (req,res) => {
 	});
 });
 
-router.get("/send/answer", access.ajax, (req,res) => {
-	let aid = req.query["aid"];
-	let number = req.query["number"];
-	let password = req.query["password"];
-	let input = JSON.parse(req.query["answer"]);
+router.put("/assessments/answer", access.ajax, (req,res) => {
+	let aid = req.body["aid"];
+	let number = req.body["number"];
+	let password = req.body["password"];
+	let input = JSON.parse(req.body["answer"]);
 	let error = validator({ _id:aid, papers:[{number:number,password:password,inputs:[input]}] }, "Assessment");
 	if (error.length > 0)
 		return res.json({errmsg:error});
@@ -110,10 +110,10 @@ router.get("/send/answer", access.ajax, (req,res) => {
 	});
 });
 
-router.get("/end/assessment", access.ajax, (req,res) => {
-	let aid = req.query["aid"];
-	let number = req.query["number"];
-	let password = req.query["password"];
+router.put("/assessments/end", access.ajax, (req,res) => {
+	let aid = req.body["aid"];
+	let number = req.body["number"];
+	let password = req.body["password"];
 	let error = validator({ _id:aid, papers:[{number:number,password:password}] }, "Assessment");
 	if (error.length > 0)
 		return res.json({errmsg:error});
diff --git a/routes/courses.js b/routes/courses.js
index 89b38b3..5ca6ad4 100644
--- a/routes/courses.js
+++ b/routes/courses.js
@@ -5,9 +5,9 @@ const sanitizeHtml = require('sanitize-html');
 const ObjectId = require("bson-objectid");
 const CourseModel = require("../models/course");
 
-router.get('/add/course', access.ajax, access.logged, (req,res) => {
-	let code = req.query["code"];
-	let description = sanitizeHtml(req.query["description"]);
+router.post('/courses', access.ajax, access.logged, (req,res) => {
+	let code = req.body["code"];
+	let description = sanitizeHtml(req.body["description"]);
 	let error = validator({code:code}, "Course");
 	if (error.length > 0)
 		return res.json({errmsg:error});
@@ -18,9 +18,9 @@ router.get('/add/course', access.ajax, access.logged, (req,res) => {
 	});
 });
 
-router.get("/set/password", access.ajax, access.logged, (req,res) => {
-	let cid = req.query["cid"];
-	let pwd = req.query["pwd"];
+router.put("/courses/password", access.ajax, access.logged, (req,res) => {
+	let cid = req.body["cid"];
+	let pwd = req.body["pwd"];
 	let error = validator({password:pwd, _id:cid}, "Course");
 	if (error.length > 0)
 		return res.json({errmsg:error});
@@ -31,7 +31,7 @@ router.get("/set/password", access.ajax, access.logged, (req,res) => {
 	});
 });
 
-router.post('/import/students', access.ajax, access.logged, (req,res) => {
+router.put('/courses/student-list', access.ajax, access.logged, (req,res) => {
 	let cid = req.body["cid"];
 	let students = JSON.parse(req.body["students"]);
 	let error = validator({_id:cid, students: students}, "Course");
@@ -48,9 +48,9 @@ router.post('/import/students', access.ajax, access.logged, (req,res) => {
 	});
 });
 
-router.get('/get/student', access.ajax, (req,res) => {
-	let number = req.query["number"];
+router.get('/courses/student', access.ajax, (req,res) => {
 	let cid = req.query["cid"];
+	let number = req.query["number"];
 	let error = validator({ _id: cid, students: [{number:number}] }, "Course");
 	if (error.length > 0)
 		return res.json({errmsg:error});
@@ -61,7 +61,7 @@ router.get('/get/student', access.ajax, (req,res) => {
 	});
 });
 
-router.get('/remove/course', access.ajax, access.logged, (req,res) => {
+router.delete('/courses', access.ajax, access.logged, (req,res) => {
 	let cid = req.query["cid"];
 	let error = validator({_id:cid}, "Course");
 	if (error.length > 0)
diff --git a/routes/users.js b/routes/users.js
index 5dab77e..993c15e 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -7,7 +7,7 @@ const access = require("../utils/access");
 const params = require("../config/parameters");
 
 // to: object user
-function sendLoginToken(subject, to, res)
+function setAndSendLoginToken(subject, to, res)
 {
 	// Set login token and send welcome(back) email with auth link
 	let token = TokenGen.generate(params.token.length);
@@ -28,12 +28,10 @@ function sendLoginToken(subject, to, res)
 	});
 }
 
-router.get('/register', access.ajax, access.unlogged, (req,res) => {
-	let email = decodeURIComponent(req.query.email);
-	let name = decodeURIComponent(req.query.name);
+router.post('/register', access.ajax, access.unlogged, (req,res) => {
 	const newUser = {
-		email: email,
-		name: name,
+		email: req.body.email,
+		name: req.body.name,
 	};
 	let error = validator(newUser, "User");
 	if (error.length > 0)
@@ -45,7 +43,7 @@ router.get('/register', access.ajax, access.unlogged, (req,res) => {
 			UserModel.create(newUser, (err,user) => {
 				access.checkRequest(res, err, user, "Registration failed", () => {
 					user.ip = req.ip;
-					sendLoginToken("Welcome to " + params.siteURL, user, res);
+					setAndSendLoginToken("Welcome to " + params.siteURL, user, res);
 				});
 			});
 		});
@@ -53,25 +51,22 @@ router.get('/register', access.ajax, access.unlogged, (req,res) => {
 });
 
 // Login:
-router.get('/sendtoken', access.ajax, access.unlogged, (req,res) => {
-	let email = decodeURIComponent(req.query.email);
+router.put('/sendtoken', access.ajax, access.unlogged, (req,res) => {
+	const email = req.body.email;
 	let error = validator({email:email}, "User");
 	if (error.length > 0)
 		return res.json({errmsg:error});
 	UserModel.getByEmail(email, (err,user) => {
 		access.checkRequest(res, err, user, "Unknown user", () => {
 			user.ip = req.ip;
-			sendLoginToken("Token for " + params.siteURL, user, res);
+			setAndSendLoginToken("Token for " + params.siteURL, user, res);
 		});
 	});
 });
 
 // Authentication process, optionally with email changing:
-router.get('/authenticate', access.unlogged, (req,res) => {
-	let loginToken = req.query.token;
-	let error = validator({token:loginToken}, "User");
-	if (error.length > 0)
-		return res.json({errmsg:error});
+router.put('/authenticate/:token([a-z0-9]+)', access.unlogged, (req,res) => {
+	const loginToken = req.params.token;
 	UserModel.getByLoginToken(loginToken, (err,user) => {
 		access.checkRequest(res, err, user, "Invalid token", () => {
 			if (user.loginToken.ip != req.ip)
@@ -101,7 +96,7 @@ router.get('/authenticate', access.unlogged, (req,res) => {
 	});
 });
 
-router.get('/logout', access.logged, (req,res) => {
+router.put('/logout', access.logged, (req,res) => {
 	UserModel.removeToken(req.user._id, req.cookies.token, (err,ret) => {
 		access.checkRequest(res, err, ret, "Logout failed", () => {
 			res.clearCookie("initials");
-- 
2.44.0