From 8a2b3260841fc5c2e0d24758bf94628ac52300d3 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 12 Feb 2018 19:54:51 +0100
Subject: [PATCH] Harmonize with web version, better style, fix for Firefox
 45.9.0 ESR

---
 public/javascripts/assessment.js            |   2 +-
 public/javascripts/components/statements.js |   1 +
 public/javascripts/course.js                | 620 ++++++++++----------
 public/javascripts/courseList.js            | 108 ++--
 public/javascripts/login.js                 | 148 +++--
 public/stylesheets/assessment.css           |   7 +-
 public/stylesheets/course.css               |  12 +-
 routes/assessments.js                       |   3 +-
 views/assessment.pug                        |   2 +-
 views/course.pug                            |   8 +-
 10 files changed, 458 insertions(+), 453 deletions(-)

diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js
index ad309d7..acce548 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;
 };
 
-let V = new Vue({
+new Vue({
 	el: "#assessment",
 	data: {
 		assessment: assessment,
diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js
index 67aaf12..819d723 100644
--- a/public/javascripts/components/statements.js
+++ b/public/javascripts/components/statements.js
@@ -48,6 +48,7 @@ Vue.component("statements", {
 								change: e => { this.answers.inputs[i][idx] = e.target.checked; },
 							},
 						},
+						[ '' ] //to work in Firefox 45.9 ESR @ ENSTA...
 					)
 				);
 				option.push(
diff --git a/public/javascripts/course.js b/public/javascripts/course.js
index e0f2e37..8064eaf 100644
--- a/public/javascripts/course.js
+++ b/public/javascripts/course.js
@@ -3,353 +3,349 @@
 // Use open mode for question banks: add setting "nbQuestions" to show nbQuestions
 // at random among active questions
 
-window.onload = function() {
-
-	V = new Vue({
-		el: '#course',
-		data: {
-			display: "assessments", //or "students", or "grades" (admin mode)
-			course: course,
-			mode: "view", //or "edit" (some assessment)
-			// assessment data:
-			monitorPwd: "",
-			newAssessment: { name: "" },
-			assessmentArray: assessmentArray,
-			assessmentIndex: 0, //current edited assessment index
-			assessment: { }, //copy of assessment at editing index in array
-			assessmentText: "", //questions in an assessment, in text format
-			// grades data:
-			settings: {
-				totalPoints: 20,
-				halfPoints: false,
-				zeroSum: false,
-			},
-			group: 1, //for detailed grades tables
-			grades: { }, //computed
+new Vue({
+	el: '#course',
+	data: {
+		display: "assessments", //or "students", or "grades" (admin mode)
+		course: course,
+		mode: "view", //or "edit" (some assessment)
+		// assessment data:
+		monitorPwd: "",
+		newAssessment: { name: "" },
+		assessmentArray: assessmentArray,
+		assessmentIndex: 0, //current edited assessment index
+		assessment: { }, //copy of assessment at editing index in array
+		assessmentText: "", //questions in an assessment, in text format
+		// grades data:
+		settings: {
+			totalPoints: 20,
+			halfPoints: false,
+			zeroSum: false,
 		},
-		mounted: function() {
-			$('.modal').each( (i,elem) => {
-				if (elem.id != "assessmentEdit")
-					$(elem).modal();
-			});
-			$('ul.tabs').tabs();
-			$('#assessmentEdit').modal({
-				complete: () => {
-					this.parseAssessment();
-					Vue.nextTick( () => {
-						$("#questionList").find("code[class^=language-]").each( (i,elem) => {
-							Prism.highlightElement(elem);
-						});
-						MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
+		group: 1, //for detailed grades tables
+		grades: { }, //computed
+	},
+	mounted: function() {
+		$('.modal').each( (i,elem) => {
+			if (elem.id != "assessmentEdit")
+				$(elem).modal();
+		});
+		$('ul.tabs').tabs();
+		$('#assessmentEdit').modal({
+			complete: () => {
+				this.parseAssessment();
+				Vue.nextTick( () => {
+					$("#questionList").find("code[class^=language-]").each( (i,elem) => {
+						Prism.highlightElement(elem);
 					});
-				},
-			});
-		},
-		methods: {
-			// GENERAL:
-			toggleDisplay: function(area) {
-				if (this.display == area)
-					this.display = "";
-				else
-					this.display = area;
-			},
-			studentList: function(group) {
-				return this.course.students
-					.filter( s => { return group==0 || s.group == group; })
-					.map( s => { return Object.assign({}, s); }) //not altering initial array
-					.sort( (a,b) => { return a.name.localeCompare(b.name); })
-			},
-			// STUDENTS:
-			uploadTrigger: function() {
-				$("#upload").click();
-			},
-			upload: function(e) {
-				let file = (e.target.files || e.dataTransfer.files)[0];
-				Papa.parse(file, {
-					header: true,
-					skipEmptyLines: true,
-					complete: (results,file) => {
-						let students = [ ];
-						// Post-process: add group/number if missing
-						let number = 1;
-						results.data.forEach( d => {
-							if (!d.group)
-								d.group = 1;
-							if (!d.number)
-								d.number = number++;
-							if (typeof d.number !== "string")
-								d.number = d.number.toString();
-							students.push(d);
-						});
-						$.ajax("/import/students", {
-							method: "POST",
-							data: {
-								cid: this.course._id,
-								students: JSON.stringify(students),
-							},
-							dataType: "json",
-							success: res => {
-								if (!res.errmsg)
-									this.course.students = students;
-								else
-									alert(res.errmsg);
-							},
-						});
-					},
+					MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
 				});
 			},
-			// ASSESSMENT:
-			addAssessment: function() {
-				if (!admin)
-					return;
-				// modal, fill code and description
-				let error = Validator.checkObject(this.newAssessment, "Assessment");
-				if (!!error)
-					return alert(error);
-				else
-					$('#newAssessment').modal('close');
-				$.ajax("/add/assessment",
-					{
-						method: "GET",
+		});
+	},
+	methods: {
+		// GENERAL:
+		toggleDisplay: function(area) {
+			if (this.display == area)
+				this.display = "";
+			else
+				this.display = area;
+		},
+		studentList: function(group) {
+			return this.course.students
+				.filter( s => { return group==0 || s.group == group; })
+				.map( s => { return Object.assign({}, s); }) //not altering initial array
+				.sort( (a,b) => { return a.name.localeCompare(b.name); })
+		},
+		// STUDENTS:
+		uploadTrigger: function() {
+			$("#upload").click();
+		},
+		upload: function(e) {
+			let file = (e.target.files || e.dataTransfer.files)[0];
+			Papa.parse(file, {
+				header: true,
+				skipEmptyLines: true,
+				complete: (results,file) => {
+					let students = [ ];
+					// Post-process: add group/number if missing
+					let number = 1;
+					results.data.forEach( d => {
+						if (!d.group)
+							d.group = 1;
+						if (!d.number)
+							d.number = number++;
+						if (typeof d.number !== "string")
+							d.number = d.number.toString();
+						students.push(d);
+					});
+					$.ajax("/import/students", {
+						method: "POST",
 						data: {
-							name: this.newAssessment.name,
-							cid: course._id,
+							cid: this.course._id,
+							students: JSON.stringify(students),
 						},
 						dataType: "json",
 						success: res => {
 							if (!res.errmsg)
-							{
-								this.newAssessment["name"] = "";
-								this.assessmentArray.push(res);
-							}
+								this.course.students = students;
 							else
 								alert(res.errmsg);
 						},
-					}
-				);
-			},
-			materialOpenModal: function(id) {
-				$("#" + id).modal("open");
-				Materialize.updateTextFields(); //textareas, time field...
-			},
-			updateAssessment: function() {
-				$.ajax("/update/assessment", {
-					method: "POST",
-					data: {assessment: JSON.stringify(this.assessment)},
+					});
+				},
+			});
+		},
+		// ASSESSMENT:
+		addAssessment: function() {
+			if (!admin)
+				return;
+			// modal, fill code and description
+			let error = Validator.checkObject(this.newAssessment, "Assessment");
+			if (!!error)
+				return alert(error);
+			else
+				$('#newAssessment').modal('close');
+			$.ajax("/add/assessment",
+				{
+					method: "GET",
+					data: {
+						name: this.newAssessment.name,
+						cid: course._id,
+					},
 					dataType: "json",
 					success: res => {
 						if (!res.errmsg)
 						{
-							this.assessmentArray[this.assessmentIndex] = this.assessment;
-							this.mode = "view";
+							this.newAssessment["name"] = "";
+							this.assessmentArray.push(res);
 						}
 						else
 							alert(res.errmsg);
 					},
-				});
-			},
-			deleteAssessment: function(assessment) {
-				if (!admin)
-					return;
-				if (confirm("Delete assessment '" + assessment.name + "' ?"))
-				{
-					$.ajax("/remove/assessment",
-						{
-							method: "GET",
-							data: { qid: this.assessment._id },
-							dataType: "json",
-							success: res => {
-								if (!res.errmsg)
-									this.assessmentArray.splice( this.assessmentArray.findIndex( item => {
-										return item._id == assessment._id;
-									}), 1 );
-								else
-									alert(res.errmsg);
-							},
-						}
-					);
 				}
-			},
-			toggleState: function(questionIndex) {
-				// add or remove from activeSet of current assessment
-				let activeIndex = this.assessment.activeSet.findIndex( item => { return item == questionIndex; });
-				if (activeIndex >= 0)
-					this.assessment.activeSet.splice(activeIndex, 1);
-				else
-					this.assessment.activeSet.push(questionIndex);
-			},
-			setAssessmentText: function() {
-				let txt = "";
-				this.assessment.questions.forEach( q => {
-					txt += q.wording + "\n";
-					q.options.forEach( (o,i) => {
-						let symbol = q.answer.includes(i) ? "+" : "-";
-						txt += symbol + " " + o + "\n";
-					});
-					txt += "\n"; //separate questions by new line
-				});
-				this.assessmentText = txt;
-			},
-			parseAssessment: function() {
-				let questions = [ ];
-				let lines = this.assessmentText.split("\n").map( L => { return L.trim(); })
-				lines.push(""); //easier parsing
-				let emptyQuestion = () => {
-					return {
-						wording: "",
-						options: [ ],
-						answer: [ ],
-						active: true, //default
-					};
-				};
-				let q = emptyQuestion();
-				lines.forEach( L => {
-					if (L.length > 0)
+			);
+		},
+		materialOpenModal: function(id) {
+			$("#" + id).modal("open");
+			Materialize.updateTextFields(); //textareas, time field...
+		},
+		updateAssessment: function() {
+			$.ajax("/update/assessment", {
+				method: "POST",
+				data: {assessment: JSON.stringify(this.assessment)},
+				dataType: "json",
+				success: res => {
+					if (!res.errmsg)
 					{
-						if (['+','-'].includes(L.charAt(0)))
-						{
-							if (L.charAt(0) == '+')
-								q.answer.push(q.options.length);
-							q.options.push(L.slice(1).trim());
-						}
-						else if (L.charAt(0) == '*')
-						{
-							// TODO: read current + next lines into q.answer (HTML, 1-elem array)
-						}
-						else
-							q.wording += L + " "; //space required at line breaks, generally
+						this.assessmentArray[this.assessmentIndex] = this.assessment;
+						this.mode = "view";
 					}
 					else
-					{
-						// Flush current question (if any)
-						if (q.wording.length > 0)
-						{
-							questions.push(q);
-							q = emptyQuestion();
-						}
-					}
-				});
-				this.assessment.questions = questions;
-			},
-			actionAssessment: function(index) {
-				if (admin)
-				{
-					// Edit screen
-					this.assessmentIndex = index;
-					this.assessment = $.extend(true, {}, this.assessmentArray[index]);
-					this.setAssessmentText();
-					this.mode = "edit";
-					Vue.nextTick( () => {
-						$("#questionList").find("code[class^=language-]").each( (i,elem) => {
-							Prism.highlightElement(elem);
-						});
-						MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
-					});
-				}
-				else //external user: show assessment
-					this.redirect(this.assessmentArray[index].name);
-			},
-			redirect: function(assessmentName) {
-				document.location.href = "/" + initials + "/" + course.code + "/" + assessmentName;
-			},
-			setPassword: function() {
-				let hashPwd = Sha1.Compute(this.monitorPwd);
-				let error = Validator.checkObject({password:hashPwd}, "Course");
-				if (error.length > 0)
-					return alert(error);
-				$.ajax("/set/password",
+						alert(res.errmsg);
+				},
+			});
+		},
+		deleteAssessment: function(assessment) {
+			if (!admin)
+				return;
+			if (confirm("Delete assessment '" + assessment.name + "' ?"))
+			{
+				$.ajax("/remove/assessment",
 					{
 						method: "GET",
-						data: {
-							cid: this.course._id,
-							pwd: hashPwd,
-						},
+						data: { qid: this.assessment._id },
 						dataType: "json",
 						success: res => {
 							if (!res.errmsg)
-								alert("Password saved!");
+								this.assessmentArray.splice( this.assessmentArray.findIndex( item => {
+									return item._id == assessment._id;
+								}), 1 );
 							else
 								alert(res.errmsg);
 						},
 					}
 				);
-			},
-			// NOTE: artifact required for Vue v-model to behave well
-			checkBoxFixedId: function(i) {
-				return "questionFixed" + i;
-			},
-			checkBoxActiveId: function(i) {
-				return "questionActive" + i;
-			},
-			// GRADES:
-			gradeSettings: function() {
-				$("#gradeSettings").modal("open");
-				Materialize.updateTextFields(); //total points field in grade settings overlap
-			},
-			download: function() {
-				// Download (all) grades as a CSV file
-				let data = [ ];
-				this.studentList(0).forEach( s => {
-					let finalGrade = 0.;
-					let gradesCount = 0;
-					if (!!this.grades[s.number])
+			}
+		},
+		toggleState: function(questionIndex) {
+			// add or remove from activeSet of current assessment
+			let activeIndex = this.assessment.activeSet.findIndex( item => { return item == questionIndex; });
+			if (activeIndex >= 0)
+				this.assessment.activeSet.splice(activeIndex, 1);
+			else
+				this.assessment.activeSet.push(questionIndex);
+		},
+		setAssessmentText: function() {
+			let txt = "";
+			this.assessment.questions.forEach( q => {
+				txt += q.wording; //already ended by \n
+				q.options.forEach( (o,i) => {
+					let symbol = q.answer.includes(i) ? "+" : "-";
+					txt += symbol + " " + o + "\n";
+				});
+				txt += "\n"; //separate questions by new line
+			});
+			this.assessmentText = txt;
+		},
+		parseAssessment: function() {
+			let questions = [ ];
+			let lines = this.assessmentText.split("\n").map( L => { return L.trim(); })
+			lines.push(""); //easier parsing
+			let emptyQuestion = () => {
+				return {
+					wording: "",
+					options: [ ],
+					answer: [ ],
+					active: true, //default
+				};
+			};
+			let q = emptyQuestion();
+			lines.forEach( L => {
+				if (L.length > 0)
+				{
+					if (['+','-'].includes(L.charAt(0)))
 					{
-						Object.keys(this.grades[s.number]).forEach( assessmentName => {
-							s[assessmentName] = this.grades[s.number][assessmentName];
-							if (_.isNumeric(s[assessmentName]) && !isNaN(s[assessmentName]))
-							{
-								finalGrade += s[assessmentName];
-								gradesCount++;
-							}
-							if (gradesCount >= 1)
-								finalGrade /= gradesCount;
-							s["final"] = finalGrade; //TODO: forbid "final" as assessment name
-						});
+						if (L.charAt(0) == '+')
+							q.answer.push(q.options.length);
+						q.options.push(L.slice(1).trim());
 					}
-					data.push(s); //number,name,group,assessName1...assessNameN,final
-				});
-				let csv = Papa.unparse(data, {
-					quotes: true,
-					header: true,
-				});
-				let downloadAnchor = $("#download");
-				downloadAnchor.attr("download", this.course.code + "_results.csv");
-				downloadAnchor.attr("href", "data:text/plain;charset=utf-8," + encodeURIComponent(csv));
-				this.$refs.download.click()
-				//downloadAnchor.click(); //fails
-			},
-			showDetails: function(group) {
-				this.group = group;
-				$("#detailedGrades").modal("open");
-			},
-			groupList: function() {
-				let maxGrp = 1;
-				this.course.students.forEach( s => {
-					if (s.group > maxGrp)
-						maxGrp = s.group;
+					else if (L.charAt(0) == '*')
+					{
+						// TODO: read current + next lines into q.answer (HTML, 1-elem array)
+					}
+					else
+						q.wording += L + "\n";
+				}
+				else
+				{
+					// Flush current question (if any)
+					if (q.wording.length > 0)
+					{
+						questions.push(q);
+						q = emptyQuestion();
+					}
+				}
+			});
+			this.assessment.questions = questions;
+		},
+		actionAssessment: function(index) {
+			if (admin)
+			{
+				// Edit screen
+				this.assessmentIndex = index;
+				this.assessment = $.extend(true, {}, this.assessmentArray[index]);
+				this.setAssessmentText();
+				this.mode = "edit";
+				Vue.nextTick( () => {
+					$("#questionList").find("code[class^=language-]").each( (i,elem) => {
+						Prism.highlightElement(elem);
+					});
+					MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]);
 				});
-				return _.range(1,maxGrp+1);
-			},
-			grade: function(assessmentIndex, studentNumber) {
-				if (!this.grades[assessmentIndex] || !this.grades[assessmentIndex][studentNumber])
-					return ""; //no grade yet
-				return this.grades[assessmentIndex][studentNumber];
-			},
-			groupId: function(group, prefix) {
-				return (prefix || "") + "group" + group;
-			},
-			togglePresence: function(number, index) {
-				// UNIMPLEMENTED
-				// TODO: if no grade (thus automatic 0), toggle "exempt" state on student for current exam
-				// --> automatic update of grades view (just a few number to change)
-			},
-			computeGrades: function() {
-				// UNIMPLEMENTED
-				// TODO: compute all grades using settings (points, coefficients, bonus/malus...).
-				// If some questions with free answers (open), display answers and ask teacher action.
-				// TODO: need a setting for that too (by student, by exercice, by question)
-			},
+			}
+			else //external user: show assessment
+				this.redirect(this.assessmentArray[index].name);
 		},
-	});
-
-};
+		redirect: function(assessmentName) {
+			document.location.href = "/" + initials + "/" + course.code + "/" + assessmentName;
+		},
+		setPassword: function() {
+			let hashPwd = Sha1.Compute(this.monitorPwd);
+			let error = Validator.checkObject({password:hashPwd}, "Course");
+			if (error.length > 0)
+				return alert(error);
+			$.ajax("/set/password",
+				{
+					method: "GET",
+					data: {
+						cid: this.course._id,
+						pwd: hashPwd,
+					},
+					dataType: "json",
+					success: res => {
+						if (!res.errmsg)
+							alert("Password saved!");
+						else
+							alert(res.errmsg);
+					},
+				}
+			);
+		},
+		// NOTE: artifact required for Vue v-model to behave well
+		checkBoxFixedId: function(i) {
+			return "questionFixed" + i;
+		},
+		checkBoxActiveId: function(i) {
+			return "questionActive" + i;
+		},
+		// GRADES:
+		gradeSettings: function() {
+			$("#gradeSettings").modal("open");
+			Materialize.updateTextFields(); //total points field in grade settings overlap
+		},
+		download: function() {
+			// Download (all) grades as a CSV file
+			let data = [ ];
+			this.studentList(0).forEach( s => {
+				let finalGrade = 0.;
+				let gradesCount = 0;
+				if (!!this.grades[s.number])
+				{
+					Object.keys(this.grades[s.number]).forEach( assessmentName => {
+						s[assessmentName] = this.grades[s.number][assessmentName];
+						if (_.isNumeric(s[assessmentName]) && !isNaN(s[assessmentName]))
+						{
+							finalGrade += s[assessmentName];
+							gradesCount++;
+						}
+						if (gradesCount >= 1)
+							finalGrade /= gradesCount;
+						s["final"] = finalGrade; //TODO: forbid "final" as assessment name
+					});
+				}
+				data.push(s); //number,name,group,assessName1...assessNameN,final
+			});
+			let csv = Papa.unparse(data, {
+				quotes: true,
+				header: true,
+			});
+			let downloadAnchor = $("#download");
+			downloadAnchor.attr("download", this.course.code + "_results.csv");
+			downloadAnchor.attr("href", "data:text/plain;charset=utf-8," + encodeURIComponent(csv));
+			this.$refs.download.click()
+			//downloadAnchor.click(); //fails
+		},
+		showDetails: function(group) {
+			this.group = group;
+			$("#detailedGrades").modal("open");
+		},
+		groupList: function() {
+			let maxGrp = 1;
+			this.course.students.forEach( s => {
+				if (s.group > maxGrp)
+					maxGrp = s.group;
+			});
+			return _.range(1,maxGrp+1);
+		},
+		grade: function(assessmentIndex, studentNumber) {
+			if (!this.grades[assessmentIndex] || !this.grades[assessmentIndex][studentNumber])
+				return ""; //no grade yet
+			return this.grades[assessmentIndex][studentNumber];
+		},
+		groupId: function(group, prefix) {
+			return (prefix || "") + "group" + group;
+		},
+		togglePresence: function(number, index) {
+			// UNIMPLEMENTED
+			// TODO: if no grade (thus automatic 0), toggle "exempt" state on student for current exam
+			// --> automatic update of grades view (just a few number to change)
+		},
+		computeGrades: function() {
+			// UNIMPLEMENTED
+			// TODO: compute all grades using settings (points, coefficients, bonus/malus...).
+			// If some questions with free answers (open), display answers and ask teacher action.
+			// TODO: need a setting for that too (by student, by exercice, by question)
+		},
+	},
+});
diff --git a/public/javascripts/courseList.js b/public/javascripts/courseList.js
index 238c1fc..020af55 100644
--- a/public/javascripts/courseList.js
+++ b/public/javascripts/courseList.js
@@ -1,69 +1,65 @@
-window.onload = function() {
-
-	new Vue({
-		el: '#courseList',
-		data: {
-			courseArray: courseArray,
-			newCourse: {
-				code: "",
-				description: "",
-			},
+new Vue({
+	el: '#courseList',
+	data: {
+		courseArray: courseArray,
+		newCourse: {
+			code: "",
+			description: "",
 		},
-		mounted: function() {
-			$('.modal').modal();
+	},
+	mounted: function() {
+		$('.modal').modal();
+	},
+	methods: {
+		redirect: function(code) {
+			document.location.href = "/" + initials + "/" + code;
 		},
-		methods: {
-			redirect: function(code) {
-				document.location.href = "/" + initials + "/" + code;
-			},
-			addCourse: function() {
-				if (!admin)
-					return;
-				// modal, fill code and description
-				let error = Validator.checkObject({code:this.newCourse.code}, "Course");
-				if (!!error)
-					return alert(error);
-				else
-					$('#newCourse').modal('close');
-				$.ajax("/add/course",
+		addCourse: function() {
+			if (!admin)
+				return;
+			// modal, fill code and description
+			let error = Validator.checkObject({code:this.newCourse.code}, "Course");
+			if (!!error)
+				return alert(error);
+			else
+				$('#newCourse').modal('close');
+			$.ajax("/add/course",
+				{
+					method: "GET",
+					data: this.newCourse,
+					dataType: "json",
+					success: res => {
+						if (!res.errmsg)
+						{
+							this.newCourse["code"] = "";
+							this.newCourse["description"] = "";
+							this.courseArray.push(res);
+						}
+						else
+							alert(res.errmsg);
+					},
+				}
+			);
+		},
+		deleteCourse: function(course) {
+			if (!admin)
+				return;
+			if (confirm("Delete course '" + course.code + "' ?"))
+				$.ajax("/remove/course",
 					{
 						method: "GET",
-						data: this.newCourse,
+						data: { cid: course._id },
 						dataType: "json",
 						success: res => {
 							if (!res.errmsg)
-							{
-								this.newCourse["code"] = "";
-								this.newCourse["description"] = "";
-								this.courseArray.push(res);
-							}
+								this.courseArray.splice( this.courseArray.findIndex( item => {
+									return item._id == course._id;
+								}), 1 );
 							else
 								alert(res.errmsg);
 						},
 					}
 				);
 			},
-			deleteCourse: function(course) {
-				if (!admin)
-					return;
-				if (confirm("Delete course '" + course.code + "' ?"))
-					$.ajax("/remove/course",
-						{
-							method: "GET",
-							data: { cid: course._id },
-							dataType: "json",
-							success: res => {
-								if (!res.errmsg)
-									this.courseArray.splice( this.courseArray.findIndex( item => {
-										return item._id == course._id;
-									}), 1 );
-								else
-									alert(res.errmsg);
-							},
-						}
-					);
-				},
-			}
-	});
-
-};
+		}
+});
diff --git a/public/javascripts/login.js b/public/javascripts/login.js
index 4f70e19..8752ba8 100644
--- a/public/javascripts/login.js
+++ b/public/javascripts/login.js
@@ -1,86 +1,82 @@
-window.onload = function() {
-
-	const messages = {
-		"login": "Go",
-		"register": "Send",
-	};
+const messages = {
+	"login": "Go",
+	"register": "Send",
+};
 
-	const ajaxUrl = {
-		"login": "/sendtoken",
-		"register": "/register",
-	};
+const ajaxUrl = {
+	"login": "/sendtoken",
+	"register": "/register",
+};
 
-	const infos = {
-		"login": "Connection token sent. Check your emails!",
-		"register": "Registration complete! Please check your emails.",
-	};
+const infos = {
+	"login": "Connection token sent. Check your emails!",
+	"register": "Registration complete! Please check your emails.",
+};
 
-	const animationDuration = 300; //in milliseconds
+const animationDuration = 300; //in milliseconds
 
-	// Basic anti-bot measure: force at least N seconds between arrival on page, and register form validation:
-	const enterTime = Date.now();
+// Basic anti-bot measure: force at least N seconds between arrival on page, and register form validation:
+const enterTime = Date.now();
 
-	new Vue({
-		el: '#login',
-		data: {
-			messages: messages,
-			user: {
-				name: "",
-				email: "",
-			},
-			stage: "login", //or "register"
+new Vue({
+	el: '#login',
+	data: {
+		messages: messages,
+		user: {
+			name: "",
+			email: "",
 		},
-		mounted: function() {
-			// https://laracasts.com/discuss/channels/vue/vuejs-set-focus-on-textfield
-			this.$refs.userEmail.focus();
+		stage: "login", //or "register"
+	},
+	mounted: function() {
+		// https://laracasts.com/discuss/channels/vue/vuejs-set-focus-on-textfield
+		this.$refs.userEmail.focus();
+	},
+	methods: {
+		toggleStage: function(stage) {
+			let $form = $("#form");
+			$form.fadeOut(animationDuration);
+			setTimeout( () => {
+				this.stage = stage;
+				$form.show(0);
+			}, animationDuration);
 		},
-		methods: {
-			toggleStage: function(stage) {
-				let $form = $("#form");
-				$form.fadeOut(animationDuration);
-				setTimeout( () => {
-					this.stage = stage;
-					$form.show(0);
-				}, animationDuration);
-			},
-			submit: function() {
-				if (this.stage=="register")
+		submit: function() {
+			if (this.stage=="register")
+			{
+				if (Date.now() - enterTime < 5000)
+					return;
+			}
+			let error = Validator.checkObject({email: this.user.email}, "User");
+			if (!error && this.stage == "register")
+				error = Validator.checkObject({name: this.user.name}, "User");
+			let $dialog = $("#dialog");
+			show($dialog);
+			setTimeout(() => {hide($dialog);}, 3000);
+			if (error.length > 0)
+				return showMsg($dialog, "error", error);
+			showMsg($dialog, "process", "Processing... Please wait");
+			$.ajax(ajaxUrl[this.stage],
 				{
-					if (Date.now() - enterTime < 5000)
-						return;
-				}
-				let error = Validator.checkObject({email: this.user.email}, "User");
-				if (!error && this.stage == "register")
-					error = Validator.checkObject({name: this.user.name}, "User");
-				let $dialog = $("#dialog");
-				show($dialog);
-				setTimeout(() => {hide($dialog);}, 3000);
-				if (error.length > 0)
-					return showMsg($dialog, "error", error);
-				showMsg($dialog, "process", "Processing... Please wait");
-				$.ajax(ajaxUrl[this.stage],
+					method: "GET",
+					data:
 					{
-						method: "GET",
-						data:
+						email: encodeURIComponent(this.user.email),
+						name: encodeURIComponent(this.user.name), //may be unused
+					},
+					dataType: "json",
+					success: res => {
+						if (!res.errmsg)
 						{
-							email: encodeURIComponent(this.user.email),
-							name: encodeURIComponent(this.user.name), //may be unused
-						},
-						dataType: "json",
-						success: res => {
-							if (!res.errmsg)
-							{
-								this.user["name"] = "";
-								this.user["email"] = "";
-								showMsg($dialog, "info", infos[this.stage]);
-							}
-							else
-								showMsg($dialog, "error", res.errmsg);
-						},
-					}
-				);
-			},
-		}
-	});
-
-};
+							this.user["name"] = "";
+							this.user["email"] = "";
+							showMsg($dialog, "info", infos[this.stage]);
+						}
+						else
+							showMsg($dialog, "error", res.errmsg);
+					},
+				}
+			);
+		},
+	}
+});
diff --git a/public/stylesheets/assessment.css b/public/stylesheets/assessment.css
index 26d9d75..7677063 100644
--- a/public/stylesheets/assessment.css
+++ b/public/stylesheets/assessment.css
@@ -28,6 +28,7 @@ button.sendAnswer {
 
 .question .wording {
 	margin-bottom: 10px;
+	overflow: auto;
 }
 
 .question .option {
@@ -61,6 +62,10 @@ table.in-question {
 }
 
 table.in-question th, table.in-question td {
-	padding: 3px;
+	padding: 7px;
 	border-bottom: 1px solid grey;
 }
+
+/*table { border: none; border-collapse: collapse; }*/
+table.in-question td { border-left: 1px solid grey; }
+table.in-question td:first-child { border-left: none; }
diff --git a/public/stylesheets/course.css b/public/stylesheets/course.css
index c1c4a9b..178e824 100644
--- a/public/stylesheets/course.css
+++ b/public/stylesheets/course.css
@@ -3,6 +3,11 @@ h4.title {
 	background-color: lightgrey;
 }
 
+.idle {
+	background-color: #EFEFEF;
+	opacity: 0.5;
+}
+
 tr.assessment {
 	cursor: pointer;
 }
@@ -33,6 +38,7 @@ tr.stats {
 
 .question .wording {
 	margin-bottom: 10px;
+	overflow: auto;
 }
 
 .question .option {
@@ -62,6 +68,10 @@ table.in-question {
 }
 
 table.in-question th, table.in-question td {
-	padding: 3px;
+	padding: 7px;
 	border-bottom: 1px solid grey;
 }
+
+/*table { border: none; border-collapse: collapse; }*/
+table.in-question td { border-left: 1px solid grey; }
+table.in-question td:first-child { border-left: none; }
diff --git a/routes/assessments.js b/routes/assessments.js
index a107d7e..b7dcfd9 100644
--- a/routes/assessments.js
+++ b/routes/assessments.js
@@ -11,9 +11,10 @@ const sanitizeHtml = require('sanitize-html');
 const sanitizeOpts = {
 	allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img', 'u' ]),
 	allowedAttributes: {
-		img: [ 'src' ],
+		img: [ 'src','style' ],
 		code: [ 'class' ],
 		table: [ 'class' ],
+		div: [ 'style' ],
 	},
 };
 
diff --git a/views/assessment.pug b/views/assessment.pug
index d1f696f..9e8550f 100644
--- a/views/assessment.pug
+++ b/views/assessment.pug
@@ -18,7 +18,7 @@ block content
 					.center-align
 						a.modal-action.modal-close.waves-effect.waves-light.btn-flat(href="#!") Ok
 		.row
-			.col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
+			.col.s12.m10.offset-m1
 				h4= assessment.name
 				#stage0(v-show="stage==0")
 					.card
diff --git a/views/course.pug b/views/course.pug
index 7aa6619..9de3570 100644
--- a/views/course.pug
+++ b/views/course.pug
@@ -90,12 +90,12 @@ block content
 							tr.grade(v-for="student in studentList(group)")
 								td {{ student.number }}
 								td(v-for="(assessment,i) in assessmentArray" @click="togglePresence(student.number,i)")
-									{{ grade(i,student.number) }}
+									| {{ grade(i,student.number) }}
 				.modal-footer
 					.center-align
 						a.modal-action.modal-close.waves-effect.waves-light.btn-flat(href="#!") Close
 		.row(v-show="mode=='view'")
-			.col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
+			.col.s12.m10.offset-m1
 				if teacher
 					h4.title(@click="toggleDisplay('students')") Students
 					.card(v-show="display=='students'")
@@ -128,7 +128,7 @@ block content
 								th #Questions
 								th Time
 						tbody
-							tr.assessment(v-for="(assessment,i) in assessmentArray" v-if="assessment.active"
+							tr.assessment(v-for="(assessment,i) in assessmentArray" :class="{idle:!assessment.active}"
 									@click.left="actionAssessment(i)" @contextmenu.prevent="deleteAssessment(assessment)")
 								td {{ assessment.name }}
 								td {{ assessment.mode }}
@@ -162,7 +162,7 @@ block content
 									td(colspan="4") Stats: range= stdev= mean=
 		if teacher
 			.row(v-show="mode=='edit'")
-				.col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
+				.col.s12.m10.offset-m1
 					h4 {{ assessment.name }}
 					.card
 						.center-align
-- 
2.44.0