From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 13 Feb 2018 17:45:19 +0000 (+0100)
Subject: refactoring, better README (breaking commit...)
X-Git-Url: https://git.auder.net/doc/html/assets/pieces/app_dev.php/%24%7BgetWhatsApp%28link%29%7D?a=commitdiff_plain;h=43828378be054cf3604b753e8d9ab24af911188f;p=qomet.git

refactoring, better README (breaking commit...)
---

diff --git a/README.md b/README.md
index 845524b..0fc0587 100644
--- a/README.md
+++ b/README.md
@@ -12,12 +12,10 @@ Source code of [qomet.auder.net](https://qomet.auder.net)
 
 Allow teachers to create courses, containing assessments. Each of them can be public, or
 restricted to a classroom (identification by student ID).
-Individual answers to an exam are monitored in real time, and feedback is sent
-to each participant in the end (answers, computing grade).
-Once a series of exam is over, the teacher can get all grades in CSV format from course page.
-
-*Note:* for now exams composition is limited to single question exercises.
-Automatic grades are also not available.
+Individual answers to an exam are monitored in real time, and answers are sent
+to each participant in the end (allowing them to estimate their grade).
+Once a series of exam is over, the teacher can get all grades in CSV format
+(assuming all questions were either quiz-like or parameterized).
 
 ## Installation
 
@@ -25,7 +23,42 @@ See setup/README
 
 ## Usage
 
-TODO: write tutorial, maybe a demo video.
+As a teacher, first create an account from the upper-left "login" menu.
+Then create a course using the appropriate button in the middle of the screen.
+Finally, create some exams ("new assessment" button). The syntax for a series of questions is described by the following example:
+
+	> Global (HTML) introduction [optional]
+
+	Question 1 text or introduction (optional if there are subquestions)
+
+		Text for question 1.1 (index detected from indentation)
+		* Answer to 1.1
+
+		Question 1.2 (a text is optional since there are subquestions)
+
+			Question 1.2.1 text (mandatory); e.g. quiz-like:
+			+ choice 1
+			- choice 2
+			- choice 3
+
+			Question 1.2.2 text (mandatory); e.g. open question:
+			* answer to 1.2.2 (can be on several lines)
+	
+	Question 2 text ...
+	* An answer to question 2
+	...
+
+All question texts (and open answers) can be on several lines.
+HTML markup (slightly limited) can be used, as well as [MathJax](https://www.mathjax.org/) with $ and $$ delimiters,
+and syntax highlighting using [prism](http://prismjs.com/): `<code class="language-xyz">` for language code `xyz`.
+The syntax for parameterized exercises (not working yet) is still undecided.
+
+Use the "exam" mode if browsing the web is allowed, and "watch" mode otherwise to monitor
+students actions like losing focus or resizing window.
+Finally the "secure" mode forbids all attempts to do anything else than focusing on the exam,
+but can be "a bit too much"; keep in mind next section if using it.
+All these modes restrict the access to a classroom. To open a series of question to the world,
+the "open" mode is for you.
 
 *Note about exams:*
 Once an assessment is started, it's impossible to quit and restart using another browser,
@@ -33,16 +66,21 @@ because a password stored in cookies need to be sent with every request.
 So under normal circumstances it's also impossible for a student to continue the exam of another.
 (The password is destroyed when exam ends or when the teacher decides to finish assessment).
 
-## Limitations
+## Limitations & workarounds
+
+Version "standard classroom": some potential internet cheating ways even in 'secure' mode (in addition to
+the usual ones like using phones, talking, doing signs, using short memos...)
 
-Version "standard classroom": some potential cheating ways,
  - headless browsers with renamed http-user-agent; difficult to counter with 100% confidence
  - block JS script using e.g. [uBlock Origin](https://github.com/gorhill/uBlock), then re-inject the script cleaned of listeners
  - intercept HTTP response to "start quiz" signal, re-compose the page without listeners and run
 
-The only way to garanty zero internet cheat is to use some SELinux configuration in kiosk mode
-with just one safe web browser enabled, e.g. [surf](https://surf.suckless.org/).
-Not that more traditional ways of cheating may still be used (phones, talking, signs, memos...)
+The easy way to prevent these cheating attempts would consist in installing qomet on a local server,
+and restricting exam rooms to the intranet while preventing users to access their account (where they could
+keep a copy of the courses). This also prevent internet-based students communication.
+
+Another option (which seems more complicated, but might be required if the intranet itself shouldn't be accessed)
+would be to force e.g. chromium in kiosk mode restricted to one domain (using SELinux on a special account maybe).
 
 ## Alternative softwares
 
@@ -56,8 +94,7 @@ Not that more traditional ways of cheating may still be used (phones, talking, s
 
  * [wims](http://wims.unice.fr/~wims/)<br/>
   Full-featured (and open source) training center for students, with various types of exercises,
-  possibly in exam mode too.
-  The spirit, however, is more "enhanced homework" than "internet exams".
+  possibly in exam mode too. The spirit, however, is more "enhanced homework" than "internet exams".
 
  * [socrative](https://socrative.com/)<br/>
   Nice looking realtime feedback (lacking in evalbox), but thought for interactive classes.
diff --git a/TODO b/TODO
deleted file mode 100644
index 19b41c8..0000000
--- a/TODO
+++ /dev/null
@@ -1,54 +0,0 @@
-Replace underscore by lodash (or better: ES6)
-Replace socket.io by Websockets ( https://www.npmjs.com/package/websocket )
-
-time per question (in mode "one question at a time" from server...)
-compute grades after exam (in teacher's view)
-factorize redundant code in course.js, monitor.js and (TOWRITE) grade.js
-  (showing students list + grades or papers)
-monitoring: main teacher should not be asked for pwd, and button "send feedback" hidden for others
-(or just disabled)
-
------
-
-Draft format (compiled to json)
-
-> Some global (HTML) intro
-
-<some html question (or/+ exercise intro)>
-
-	<some html subQuestion>
-	* some answer [trigger input/index in answers]
-
-	<another subquestion>
-
-		<sub-subQuestion>
-		+ choix1
-		- choix 2
-		+ choix 3
-		- choix4
-
-		<another sub sub>
-		* answer 2 (which can
-		be on
-		several lines)
-
-<Some second question>
-* With answer
-
-=====
-
-questions group by index prefix 1.2.3 1.1 ...etc --> '1'
-
-NOTE: questions can contain parameterized exercises (how ?
---> describe variables (syntax ?)
---> write javascript script (OK, users trusted ? ==> safe mode possible if public website)
-Imaginary example: (using math.js)
-	<params> (avant l'exo)
-	x: math.random()
-	y: math.random()
-	M: math.matrix([[7, x], [y, -3]]);
-	res: math.det(M)
-	</params>
-	<div>Calculer le déterminant de 
-	$$\begin{matrix}7 & x\\y & -3\end{matrix}$$</div>
-	* ...
diff --git a/entities/assessment.js b/entities/assessment.js
deleted file mode 100644
index b4b6d89..0000000
--- a/entities/assessment.js
+++ /dev/null
@@ -1,257 +0,0 @@
-const db = require("../utils/database");
-
-const AssessmentEntity =
-{
-	/*
-	 * Structure:
-	 *   _id: BSON id
-	 *   cid: course ID
-	 *   name: varchar
-	 *   active: boolean true/false
-	 *   mode: secure | 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
-	 *   introduction: "",
-	 *   coefficient: number, default 1
-	 *   questions: array of
-	 *     index: for paper test, like 2.1.a (?!); and quiz: 0, 1, 2, 3...
-	 *     wording: varchar (HTML)
-	 *     options: array of varchar --> if present, question type == quiz!
-	 *     fixed: bool, options in fixed order (default: false)
-	 *     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)
-	 *   papers : array of
-	 *     number: student number
-	 *     inputs: array of indexed arrays of integers (or html text if not quiz)
-	 *     startTime, endTime
-	 *     discoTime, totalDisco: last disconnect timestamp (if relevant) + total
-	 *     discoCount: total disconnections
-	 *     password: random string identifying student for exam session TEMPORARY
-	 */
-
-	getById: function(aid, callback)
-	{
-		db.assessments.findOne(
-			{ _id: aid },
-			callback
-		);
-	},
-
-	getByPath: function(cid, name, callback)
-	{
-		db.assessments.findOne(
-			{
-				cid: cid,
-				name: name,
-			},
-			callback
-		);
-	},
-
-	insert: function(cid, name, callback)
-	{
-		db.assessments.insert(
-			{
-				name: name,
-				cid: cid,
-				active: false,
-				mode: "exam",
-				fixed: false,
-				display: "one",
-				time: 0,
-				introduction: "",
-				coefficient: 1,
-				questions: [ ],
-				papers: [ ],
-			},
-			callback
-		);
-	},
-
-	getByCourse: function(cid, callback)
-	{
-		db.assessments.find(
-			{ cid: cid },
-			callback
-		);
-	},
-
-	// arg: full assessment without _id field
-	replace: function(aid, assessment, cb)
-	{
-		// Should be: (but unsupported by mongojs)
-//		db.assessments.replaceOne(
-//			{ _id: aid },
-//			assessment,
-//			cb
-//		);
-		// Temporary workaround:
-		db.assessments.update(
-			{ _id: aid },
-			{ $set: assessment },
-			cb
-		);
-	},
-
-	getQuestions: function(aid, callback)
-	{
-		db.assessments.findOne(
-			{ _id: aid },
-			{ questions: 1},
-			(err,res) => {
-				callback(err, !!res ? res.questions : null);
-			}
-		);
-	},
-
-	getPaperByNumber: function(aid, number, callback)
-	{
-		db.assessments.findOne(
-			{
-				_id: aid,
-				"papers.number": number,
-			},
-			(err,a) => {
-				if (!!err || !a)
-					return callback(err,a);
-				for (let p of a.papers)
-				{
-					if (p.number == number)
-						return callback(null,p); //reached for sure
-				}
-			}
-		);
-	},
-
-	startSession: function(aid, number, password, callback)
-	{
-		db.assessments.update(
-			{ _id: aid },
-			{ $push: { papers: {
-				number: number,
-				startTime: Date.now(),
-				endTime: undefined,
-				password: password,
-				totalDisco: 0,
-				discoCount: 0,
-				inputs: [ ], //TODO: this is stage 1, stack indexed answers.
-				// then build JSON tree for easier access / correct
-			}}},
-			callback
-		);
-	},
-
-	// NOTE: no callbacks for 2 next functions, failures are not so important
-	// (because monitored: teachers can see what's going on)
-
-	addDisco: function(aid, number, deltaTime)
-	{
-		db.assessments.update(
-			{
-				_id: aid,
-				"papers.number": number,
-			},
-			{ $inc: {
-				"papers.$.discoCount": 1,
-				"papers.$.totalDisco": deltaTime,
-			} },
-			{ $set: { "papers.$.discoTime": null } }
-		);
-	},
-
-	setDiscoTime: function(aid, number)
-	{
-		db.assessments.update(
-			{
-				_id: aid,
-				"papers.number": number,
-			},
-			{ $set: { "papers.$.discoTime": Date.now() } }
-		);
-	},
-
-	getDiscoTime: function(aid, number, cb)
-	{
-		db.assessments.findOne(
-			{ _id: aid },
-			(err,a) => {
-				if (!!err)
-					return cb(err, null);
-				const idx = a.papers.findIndex( item => { return item.number == number; });
-				cb(null, a.papers[idx].discoTime);
-			}
-		);
-	},
-
-	hasInput: function(aid, number, password, idx, cb)
-	{
-		db.assessments.findOne(
-			{
-				_id: aid,
-				"papers.number": number,
-				"papers.password": password,
-			},
-			(err,a) => {
-				if (!!err || !a)
-					return cb(err,a);
-				let papIdx = a.papers.findIndex( item => { return item.number == number; });
-				for (let i of a.papers[papIdx].inputs)
-				{
-					if (i.index == idx)
-						return cb(null,true);
-				}
-				cb(null,false);
-			}
-		);
-	},
-
-	// https://stackoverflow.com/questions/27874469/mongodb-push-in-nested-array
-	setInput: function(aid, number, password, input, callback) //input: index + arrayOfInt (or txt)
-	{
-		db.assessments.update(
-			{
-				_id: aid,
-				"papers.number": number,
-				"papers.password": password,
-			},
-			{ $push: { "papers.$.inputs": input } },
-			callback
-		);
-	},
-
-	endAssessment: function(aid, number, password, callback)
-	{
-		db.assessments.update(
-			{
-				_id: aid,
-				"papers.number": number,
-				"papers.password": password,
-			},
-			{ $set: {
-				"papers.$.endTime": Date.now(),
-				"papers.$.password": "",
-			} },
-			callback
-		);
-	},
-
-	remove: function(aid, cb)
-	{
-		db.assessments.remove(
-			{ _id: aid },
-			cb
-		);
-	},
-
-	removeGroup: function(cid, cb)
-	{
-		db.assessments.remove(
-			{ cid: cid },
-			cb
-		);
-	},
-}
-
-module.exports = AssessmentEntity;
diff --git a/entities/course.js b/entities/course.js
deleted file mode 100644
index 9b3fb46..0000000
--- a/entities/course.js
+++ /dev/null
@@ -1,99 +0,0 @@
-const db = require("../utils/database");
-
-const CourseEntity =
-{
-	/*
-	 * Structure:
-	 *   _id: BSON id
-	 *   uid: prof ID
-	 *   code: varchar
-	 *   description: varchar
-	 *   password: monitoring password hash
-	 *   students: array of
-	 *     number: student number
-	 *     name: varchar
-	 *     group: integer
-	 */
-
-	getByUser: function(uid, callback)
-	{
-		db.courses.find(
-			{ uid: uid },
-			callback
-		);
-	},
-
-	getById: function(cid, callback)
-	{
-		db.courses.findOne(
-			{ _id: cid },
-			callback
-		);
-	},
-
-	getByPath: function(uid, code, callback)
-	{
-		db.courses.findOne(
-			{
-				$and: [
-					{ uid: uid },
-					{ code: code },
-				]
-			},
-			callback
-		);
-	},
-
-	insert: function(uid, code, description, cb)
-	{
-		db.courses.insert(
-			{
-				uid: uid,
-				code: code,
-				description: description,
-				students: [ ],
-			},
-			cb);
-	},
-
-	setStudents: function(cid, students, cb)
-	{
-		db.courses.update(
-			{ _id: cid },
-			{ $set: { students: students } },
-			cb
-		);
-	},
-
-	// Note: return { students: { ... } }, pointing on the requested row
-	getStudent: function(cid, number, cb)
-	{
-		db.courses.findOne(
-			{ _id: cid },
-			{
-				_id: 0,
-				students: { $elemMatch: {number: number} }
-			},
-			cb
-		);
-	},
-
-	setPassword: function(cid, pwd, cb)
-	{
-		db.courses.update(
-			{ _id: cid },
-			{ $set: { password: pwd } },
-			cb
-		);
-	},
-
-	remove: function(cid, cb)
-	{
-		db.courses.remove(
-			{ _id: cid },
-			cb
-		);
-	},
-}
-
-module.exports = CourseEntity;
diff --git a/entities/user.js b/entities/user.js
deleted file mode 100644
index 72b3a43..0000000
--- a/entities/user.js
+++ /dev/null
@@ -1,145 +0,0 @@
-const db = require("../utils/database");
-
-const UserEntity =
-{
-	/*
-	 * Structure:
-	 *   _id: BSON id
-	 *   ** Strings, identification informations:
-	 *   email
-	 *   name
-	 *   initials : computed, Benjamin Auder --> ba ...etc
-	 *   loginToken: {
-	 *     value: string
-	 *     timestamp: datetime (validity)
-	 *     ip: address of requesting machine
-	 *   }
-	 *   sessionTokens (array): cookie identification
-	 */
-
-	getInitialsByPrefix: function(prefix, cb)
-	{
-		db.users.find(
-			{ initials: new RegExp("^" + prefix) },
-			{ initials: 1, _id: 0 },
-			cb
-		);
-	},
-
-	insert: function(newUser, cb)
-	{
-		db.users.insert(Object.assign({},
-			newUser,
-			{
-				loginToken: { },
-				sessionTokens: [ ],
-			}),
-			cb
-		);
-	},
-
-	getByLoginToken: function(token, cb)
-	{
-		db.users.findOne(
-			{ "loginToken.value": token },
-			cb
-		);
-	},
-
-	getBySessionToken: function(token, cb)
-	{
-		db.users.findOne(
-			{ sessionTokens: token},
-			cb
-		);
-	},
-
-	getById: function(uid, cb)
-	{
-		db.users.findOne(
-			{ _id: uid },
-			cb
-		);
-	},
-
-	getByEmail: function(email, cb)
-	{
-		db.users.findOne(
-			{ email: email },
-			cb
-		);
-	},
-
-	getByInitials: function(initials, cb)
-	{
-		db.users.findOne(
-			{ initials: initials },
-			cb
-		);
-	},
-
-	getUnlogged: function(cb)
-	{
-		var tsNow = new Date().getTime();
-		// 86400000 = 24 hours in milliseconds
-		var day = 86400000;
-		db.users.find({}, (err,userArray) => {
-			let unlogged = userArray.filter( u => {
-				return u.sessionTokens.length==0 && u._id.getTimestamp().getTime() + day < tsNow;
-			});
-			cb(err, unlogged);
-		});
-	},
-
-	getAll: function(cb)
-	{
-		db.users.find({}, cb);
-	},
-
-	setLoginToken: function(token, uid, ip, cb)
-	{
-		db.users.update(
-			{ _id: uid },
-			{ $set: { loginToken: {
-					value: token,
-					timestamp: new Date().getTime(),
-					ip: ip,
-				}}
-			},
-			cb
-		);
-	},
-
-	setSessionToken: function(token, uid, cb)
-	{
-		// Also empty the login token to invalidate future attempts
-		db.users.update(
-			{ _id: uid },
-			{
-				$set: { loginToken: {} },
-				$push: { sessionTokens: {
-					$each: [token],
-					$slice: -7 //only allow 7 simultaneous connections per user (TODO?)
-				}}
-			},
-			cb
-		);
-	},
-
-	removeToken: function(uid, token, cb)
-	{
-		db.users.update(
-			{ _id: uid },
-			{ $pull: {sessionTokens: token} },
-			cb
-		);
-	},
-
-	// TODO: later, allow account removal
-	remove: function(uids)
-	{
-		db.users.remove({_id: uids});
-	},
-}
-
-module.exports = UserEntity;
diff --git a/models/assessment.js b/models/assessment.js
index d0e0d0b..c7cb0fd 100644
--- a/models/assessment.js
+++ b/models/assessment.js
@@ -1,20 +1,283 @@
-const AssessmentEntity = require("../entities/assessment");
-const CourseEntity = require("../entities/course");
+const CourseModel = require("../models/course");
+const UserModel = require("../models/user");
 const ObjectId = require("bson-objectid");
-const UserEntity = require("../entities/user");
 const TokenGen = require("../utils/tokenGenerator");
+const db = require("../utils/database");
 
 const AssessmentModel =
 {
+	/*
+	 * Structure:
+	 *   _id: BSON id
+	 *   cid: course ID
+	 *   name: varchar
+	 *   active: boolean true/false
+	 *   mode: secure | 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
+	 *   introduction: "",
+	 *   coefficient: number, default 1
+	 *   questions: array of
+	 *     index: for paper test, like 2.1.a (?!); and quiz: 0, 1, 2, 3...
+	 *     wording: varchar (HTML)
+	 *     options: array of varchar --> if present, question type == quiz!
+	 *     fixed: bool, options in fixed order (default: false)
+	 *     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}
+	 *     current: index of current question (if relevant: display="one")
+	 *     startTime
+	 *     discoTime, totalDisco: last disconnect timestamp (if relevant) + total time
+	 *     discoCount: number of disconnections
+	 *     password: random string identifying student for exam session TEMPORARY
+	 */
+
+	//////////////////
+	// BASIC FUNCTIONS
+
+	getById: function(aid, callback)
+	{
+		db.assessments.findOne(
+			{ _id: aid },
+			callback
+		);
+	},
+
+	getByPath: function(cid, name, callback)
+	{
+		db.assessments.findOne(
+			{
+				cid: cid,
+				name: name,
+			},
+			callback
+		);
+	},
+
+	insert: function(cid, name, callback)
+	{
+		db.assessments.insert(
+			{
+				name: name,
+				cid: cid,
+				active: false,
+				mode: "exam",
+				fixed: false,
+				display: "one",
+				time: 0,
+				introduction: "",
+				coefficient: 1,
+				questions: [ ],
+				papers: [ ],
+			},
+			callback
+		);
+	},
+
+	getByCourse: function(cid, callback)
+	{
+		db.assessments.find(
+			{ cid: cid },
+			callback
+		);
+	},
+
+	// arg: full assessment without _id field
+	replace: function(aid, assessment, cb)
+	{
+		// Should be: (but unsupported by mongojs)
+//		db.assessments.replaceOne(
+//			{ _id: aid },
+//			assessment,
+//			cb
+//		);
+		// Temporary workaround:
+		db.assessments.update(
+			{ _id: aid },
+			{ $set: assessment },
+			cb
+		);
+	},
+
+	getQuestions: function(aid, callback)
+	{
+		db.assessments.findOne(
+			{
+				_id: aid,
+				display: "all",
+			},
+			{ questions: 1},
+			(err,res) => {
+				callback(err, !!res ? res.questions : null);
+			}
+		);
+	},
+
+	getQuestion: function(aid, index, callback)
+	{
+		db.assessments.findOne(
+			{
+				_id: aid,
+				display: "one",
+			},
+			{ questions: 1},
+			(err,res) => {
+				if (!!err || !res)
+					return callback(err, res);
+				const qIdx = res.questions.findIndex( item => { return item.index == index; });
+				if (qIdx === -1)
+					return callback({errmsg: "Question not found"}, null);
+				callback(null, res.questions[qIdx]);
+			}
+		);
+	},
+
+	getPaperByNumber: function(aid, number, callback)
+	{
+		db.assessments.findOne(
+			{
+				_id: aid,
+				"papers.number": number,
+			},
+			(err,a) => {
+				if (!!err || !a)
+					return callback(err,a);
+				for (let p of a.papers)
+				{
+					if (p.number == number)
+						return callback(null,p); //reached for sure
+				}
+			}
+		);
+	},
+
+	// NOTE: no callbacks for 2 next functions, failures are not so important
+	// (because monitored: teachers can see what's going on)
+
+	addDisco: function(aid, number, deltaTime)
+	{
+		db.assessments.update(
+			{
+				_id: aid,
+				"papers.number": number,
+			},
+			{ $inc: {
+				"papers.$.discoCount": 1,
+				"papers.$.totalDisco": deltaTime,
+			} },
+			{ $set: { "papers.$.discoTime": null } }
+		);
+	},
+
+	setDiscoTime: function(aid, number)
+	{
+		db.assessments.update(
+			{
+				_id: aid,
+				"papers.number": number,
+			},
+			{ $set: { "papers.$.discoTime": Date.now() } }
+		);
+	},
+
+	getDiscoTime: function(aid, number, cb)
+	{
+		db.assessments.findOne(
+			{ _id: aid },
+			(err,a) => {
+				if (!!err)
+					return cb(err, null);
+				const idx = a.papers.findIndex( item => { return item.number == number; });
+				cb(null, a.papers[idx].discoTime);
+			}
+		);
+	},
+
+	hasInput: function(aid, number, password, idx, cb)
+	{
+		db.assessments.findOne(
+			{
+				_id: aid,
+				"papers.number": number,
+				"papers.password": password,
+			},
+			(err,a) => {
+				if (!!err || !a)
+					return cb(err,a);
+				let papIdx = a.papers.findIndex( item => { return item.number == number; });
+				for (let i of a.papers[papIdx].inputs)
+				{
+					if (i.index == idx)
+						return cb(null,true);
+				}
+				cb(null,false);
+			}
+		);
+	},
+
+	// https://stackoverflow.com/questions/27874469/mongodb-push-in-nested-array
+	setInput: function(aid, number, password, input, callback) //input: index + arrayOfInt (or txt)
+	{
+		db.assessments.update(
+			{
+				_id: aid,
+				"papers.number": number,
+				"papers.password": password,
+			},
+			{ $push: { "papers.$.inputs": input } },
+			callback
+		);
+	},
+
+	endAssessment: function(aid, number, password, callback)
+	{
+		db.assessments.update(
+			{
+				_id: aid,
+				"papers.number": number,
+				"papers.password": password,
+			},
+			{ $set: {
+				"papers.$.endTime": Date.now(),
+				"papers.$.password": "",
+			} },
+			callback
+		);
+	},
+
+	remove: function(aid, cb)
+	{
+		db.assessments.remove(
+			{ _id: aid },
+			cb
+		);
+	},
+
+	removeGroup: function(cid, cb)
+	{
+		db.assessments.remove(
+			{ cid: cid },
+			cb
+		);
+	},
+
+	/////////////////////
+	// ADVANCED FUNCTIONS
+
 	getByRefs: function(initials, code, name, cb)
 	{
-		UserEntity.getByInitials(initials, (err,user) => {
+		UserModel.getByInitials(initials, (err,user) => {
 			if (!!err || !user)
 				return cb(err || {errmsg: "User not found"});
-			CourseEntity.getByPath(user._id, code, (err2,course) => {
+			CourseModel.getByPath(user._id, code, (err2,course) => {
 				if (!!err2 || !course)
 					return cb(err2 || {errmsg: "Course not found"});
-				AssessmentEntity.getByPath(course._id, name, (err3,assessment) => {
+				AssessmentModel.getByPath(course._id, name, (err3,assessment) => {
 					if (!!err3 || !assessment)
 						return cb(err3 || {errmsg: "Assessment not found"});
 					cb(null,assessment);
@@ -25,7 +288,7 @@ const AssessmentModel =
 
 	checkPassword: function(aid, number, password, cb)
 	{
-		AssessmentEntity.getById(aid, (err,assessment) => {
+		AssessmentModel.getById(aid, (err,assessment) => {
 			if (!!err || !assessment)
 				return cb(err, assessment);
 			const paperIdx = assessment.papers.findIndex( item => { return item.number == number; });
@@ -38,13 +301,13 @@ const AssessmentModel =
 	add: function(uid, cid, name, cb)
 	{
 		// 1) Check that course is owned by user of ID uid
-		CourseEntity.getById(cid, (err,course) => {
+		CourseModel.getById(cid, (err,course) => {
 			if (!!err || !course)
 				return cb({errmsg: "Course retrieval failure"});
 			if (!course.uid.equals(uid))
 				return cb({errmsg:"Not your course"},undefined);
 			// 2) Insert new blank assessment
-			AssessmentEntity.insert(cid, name, cb);
+			AssessmentModel.insert(cid, name, cb);
 		});
 	},
 
@@ -52,10 +315,10 @@ const AssessmentModel =
 	{
 		const aid = ObjectId(assessment._id);
 		// 1) Check that assessment is owned by user of ID uid
-		AssessmentEntity.getById(aid, (err,assessmentOld) => {
+		AssessmentModel.getById(aid, (err,assessmentOld) => {
 			if (!!err || !assessmentOld)
 				return cb({errmsg: "Assessment retrieval failure"});
-			CourseEntity.getById(ObjectId(assessmentOld.cid), (err2,course) => {
+			CourseModel.getById(ObjectId(assessmentOld.cid), (err2,course) => {
 				if (!!err2 || !course)
 					return cb({errmsg: "Course retrieval failure"});
 				if (!course.uid.equals(uid))
@@ -63,7 +326,7 @@ const AssessmentModel =
 				// 2) Replace assessment
 				delete assessment["_id"];
 				assessment.cid = ObjectId(assessment.cid);
-				AssessmentEntity.replace(aid, assessment, cb);
+				AssessmentModel.replace(aid, assessment, cb);
 			});
 		});
 	},
@@ -71,7 +334,7 @@ const AssessmentModel =
 	// Set password in responses collection
 	startSession: function(aid, number, password, cb)
 	{
-		AssessmentEntity.getPaperByNumber(aid, number, (err,paper) => {
+		AssessmentModel.getPaperByNumber(aid, number, (err,paper) => {
 			if (!!err)
 				return cb(err,null);
 			if (!paper && !!password)
@@ -83,18 +346,26 @@ const AssessmentModel =
 				if (paper.password != password)
 					return cb({errmsg: "Wrong password"});
 			}
-			AssessmentEntity.getQuestions(aid, (err,questions) => {
-				if (!!err)
-					return cb(err,null);
+			AssessmentModel.getQuestions(aid, (err2,questions) => {
+				if (!!err2)
+					return cb(err2,null);
 				if (!!paper)
-					return cb(null,{paper:paper,questions:questions});
+					return cb(null,{paper:paper});
 				const pwd = TokenGen.generate(12); //arbitrary number, 12 seems enough...
-				AssessmentEntity.startSession(aid, number, pwd, (err2,ret) => {
-					cb(err2, {
-						questions: questions,
-						password: pwd,
-					});
-				});
+				db.assessments.update(
+					{ _id: aid },
+					{ $push: { papers: {
+						number: number,
+						startTime: Date.now(),
+						endTime: undefined,
+						password: password,
+						totalDisco: 0,
+						discoCount: 0,
+						inputs: [ ], //TODO: this is stage 1, stack indexed answers.
+						// then build JSON tree for easier access / correct
+					}}},
+					(err3,ret) => { cb(err3,{password:password}); }
+				);
 			});
 		});
 	},
@@ -102,12 +373,12 @@ const AssessmentModel =
 	newAnswer: function(aid, number, password, input, cb)
 	{
 		// Check that student hasn't already answered
-		AssessmentEntity.hasInput(aid, number, password, input.index, (err,ret) => {
+		AssessmentModel.hasInput(aid, number, password, input.index, (err,ret) => {
 			if (!!err)
 				return cb(err,null);
 			if (!!ret)
 				return cb({errmsg:"Question already answered"},null);
-			AssessmentEntity.setInput(aid, number, password, input, (err2,ret2) => {
+			AssessmentModel.setInput(aid, number, password, input, (err2,ret2) => {
 				if (!!err2 || !ret2)
 					return cb(err2,ret2);
 				return cb(null,ret2);
@@ -115,17 +386,16 @@ const AssessmentModel =
 		});
 	},
 
-	// NOTE: no callbacks for 2 next functions, failures are not so important
+	// NOTE: no callbacks for next function, failures are not so important
 	// (because monitored: teachers can see what's going on)
-
 	newConnection: function(aid, number)
 	{
 		//increment discoCount, reset discoTime to NULL, update totalDisco
-		AssessmentEntity.getDiscoTime(aid, number, (err,discoTime) => {
+		AssessmentModel.getDiscoTime(aid, number, (err,discoTime) => {
 			if (!!discoTime)
-				AssessmentEntity.addDisco(aid, number, Date.now() - discoTime);
+				AssessmentModel.addDisco(aid, number, Date.now() - discoTime);
 		});
 	},
-};
+}
 
 module.exports = AssessmentModel;
diff --git a/models/course.js b/models/course.js
index 53419c1..631bab9 100644
--- a/models/course.js
+++ b/models/course.js
@@ -1,17 +1,116 @@
-const CourseEntity = require("../entities/course");
-const UserEntity = require("../entities/user");
-const AssessmentEntity = require("../entities/assessment");
+const UserModel = require("../models/user");
+const AssessmentModel = require("../models/assessment");
+const db = require("../utils/database");
 
 const CourseModel =
 {
+	/*
+	 * Structure:
+	 *   _id: BSON id
+	 *   uid: prof ID
+	 *   code: varchar
+	 *   description: varchar
+	 *   password: monitoring password hash
+	 *   students: array of
+	 *     number: student number
+	 *     name: varchar
+	 *     group: integer
+	 */
+
+	//////////////////
+	// BASIC FUNCTIONS
+
+	getByUser: function(uid, callback)
+	{
+		db.courses.find(
+			{ uid: uid },
+			callback
+		);
+	},
+
+	getById: function(cid, callback)
+	{
+		db.courses.findOne(
+			{ _id: cid },
+			callback
+		);
+	},
+
+	getByPath: function(uid, code, callback)
+	{
+		db.courses.findOne(
+			{
+				$and: [
+					{ uid: uid },
+					{ code: code },
+				]
+			},
+			callback
+		);
+	},
+
+	insert: function(uid, code, description, cb)
+	{
+		db.courses.insert(
+			{
+				uid: uid,
+				code: code,
+				description: description,
+				students: [ ],
+			},
+			cb);
+	},
+
+	setStudents: function(cid, students, cb)
+	{
+		db.courses.update(
+			{ _id: cid },
+			{ $set: { students: students } },
+			cb
+		);
+	},
+
+	// Note: return { students: { ... } }, pointing on the requested row
+	getStudent: function(cid, number, cb)
+	{
+		db.courses.findOne(
+			{ _id: cid },
+			{
+				_id: 0,
+				students: { $elemMatch: {number: number} }
+			},
+			cb
+		);
+	},
+
+	setPassword: function(cid, pwd, cb)
+	{
+		db.courses.update(
+			{ _id: cid },
+			{ $set: { password: pwd } },
+			cb
+		);
+	},
+
+	remove: function(cid, cb)
+	{
+		db.courses.remove(
+			{ _id: cid },
+			cb
+		);
+	},
+
+	/////////////////////
+	// ADVANCED FUNCTIONS
+
 	getByInitials: function(initials, callback)
 	{
-		UserEntity.getByInitials(initials, (err,user) => {
+		UserModel.getByInitials(initials, (err,user) => {
 			if (!!err || !user)
 				callback(err, []);
 			else
 			{
-				CourseEntity.getByUser(user._id, (err2,courseArray) => {
+				CourseModel.getByUser(user._id, (err2,courseArray) => {
 					callback(err2, courseArray);
 				});
 			}
@@ -20,12 +119,12 @@ const CourseModel =
 
 	getByRefs: function(initials, code, callback)
 	{
-		UserEntity.getByInitials(initials, (err,user) => {
+		UserModel.getByInitials(initials, (err,user) => {
 			if (!!err || !user)
 				callback(err, []);
 			else
 			{
-				CourseEntity.getByPath(user._id, code, (err2,course) => {
+				CourseModel.getByPath(user._id, code, (err2,course) => {
 					callback(err2, course);
 				});
 			}
@@ -35,37 +134,37 @@ const CourseModel =
 	importStudents: function(uid, cid, students, cb)
 	{
 		// 1) check if uid == course uid
-		CourseEntity.getById(cid, (err,course) => {
+		CourseModel.getById(cid, (err,course) => {
 			if (!!err || !course || !course.uid.equals(uid))
 				return cb({errmsg:"Not your course"},{});
 			// 2) Set students
-			CourseEntity.setStudents(cid, students, cb);
+			CourseModel.setStudents(cid, students, cb);
 		});
 	},
 
 	setPassword: function(uid, cid, pwd, cb)
 	{
 		// 1) check if uid == course uid
-		CourseEntity.getById(cid, (err,course) => {
+		CourseModel.getById(cid, (err,course) => {
 			if (!!err || !course || !course.uid.equals(uid))
 				return cb({errmsg:"Not your course"},{});
 			// 2) Insert new student (overwrite if number already exists)
-			CourseEntity.setPassword(cid, pwd, cb);
+			CourseModel.setPassword(cid, pwd, cb);
 		});
 	},
 
 	remove: function(uid, cid, cb)
 	{
 		// 1) check if uid == course uid
-		CourseEntity.getById(cid, (err,course) => {
+		CourseModel.getById(cid, (err,course) => {
 			if (!!err || !course || !course.uid.equals(uid))
 				return cb({errmsg:"Not your course"},{});
 			// 2) remove all associated assessments
-			AssessmentEntity.removeGroup(cid, (err2,ret) => {
+			AssessmentModel.removeGroup(cid, (err2,ret) => {
 				if (!!err)
 					return cb(err,{});
 				// 3) remove course (with its students)
-				CourseEntity.remove(cid, cb);
+				CourseModel.remove(cid, cb);
 			});
 		});
 	},
diff --git a/models/user.js b/models/user.js
index c6fa776..4b66c88 100644
--- a/models/user.js
+++ b/models/user.js
@@ -1,15 +1,160 @@
-const UserEntity = require("../entities/user");
 const params = require("../config/parameters");
+const db = require("../utils/database");
 
 const UserModel =
 {
+	/*
+	 * Structure:
+	 *   _id: BSON id
+	 *   ** Strings, identification informations:
+	 *   email
+	 *   name
+	 *   initials : computed, Benjamin Auder --> ba ...etc
+	 *   loginToken: {
+	 *     value: string
+	 *     timestamp: datetime (validity)
+	 *     ip: address of requesting machine
+	 *   }
+	 *   sessionTokens (array): cookie identification
+	 */
+
+	// BASIC FUNCTIONS
+	//////////////////
+
+	getInitialsByPrefix: function(prefix, cb)
+	{
+		db.users.find(
+			{ initials: new RegExp("^" + prefix) },
+			{ initials: 1, _id: 0 },
+			cb
+		);
+	},
+
+	insert: function(newUser, cb)
+	{
+		db.users.insert(Object.assign({},
+			newUser,
+			{
+				loginToken: { },
+				sessionTokens: [ ],
+			}),
+			cb
+		);
+	},
+
+	getByLoginToken: function(token, cb)
+	{
+		db.users.findOne(
+			{ "loginToken.value": token },
+			cb
+		);
+	},
+
+	getBySessionToken: function(token, cb)
+	{
+		db.users.findOne(
+			{ sessionTokens: token},
+			cb
+		);
+	},
+
+	getById: function(uid, cb)
+	{
+		db.users.findOne(
+			{ _id: uid },
+			cb
+		);
+	},
+
+	getByEmail: function(email, cb)
+	{
+		db.users.findOne(
+			{ email: email },
+			cb
+		);
+	},
+
+	getByInitials: function(initials, cb)
+	{
+		db.users.findOne(
+			{ initials: initials },
+			cb
+		);
+	},
+
+	getUnlogged: function(cb)
+	{
+		var tsNow = new Date().getTime();
+		// 86400000 = 24 hours in milliseconds
+		var day = 86400000;
+		db.users.find({}, (err,userArray) => {
+			let unlogged = userArray.filter( u => {
+				return u.sessionTokens.length==0 && u._id.getTimestamp().getTime() + day < tsNow;
+			});
+			cb(err, unlogged);
+		});
+	},
+
+	getAll: function(cb)
+	{
+		db.users.find({}, cb);
+	},
+
+	setLoginToken: function(token, uid, ip, cb)
+	{
+		db.users.update(
+			{ _id: uid },
+			{ $set: { loginToken: {
+					value: token,
+					timestamp: new Date().getTime(),
+					ip: ip,
+				}}
+			},
+			cb
+		);
+	},
+
+	setSessionToken: function(token, uid, cb)
+	{
+		// Also empty the login token to invalidate future attempts
+		db.users.update(
+			{ _id: uid },
+			{
+				$set: { loginToken: {} },
+				$push: { sessionTokens: {
+					$each: [token],
+					$slice: -7 //only allow 7 simultaneous connections per user (TODO?)
+				}}
+			},
+			cb
+		);
+	},
+
+	removeToken: function(uid, token, cb)
+	{
+		db.users.update(
+			{ _id: uid },
+			{ $pull: {sessionTokens: token} },
+			cb
+		);
+	},
+
+	// TODO: later, allow account removal
+	remove: function(uids)
+	{
+		db.users.remove({_id: uids});
+	},
+
+	/////////////////////
+	// ADVANCED FUNCTIONS
+
 	create: function(newUser, callback)
 	{
 		// Determine initials from name parts
 		let nameParts = newUser.name.split(/[ -]+/);
 		let initials = nameParts.map( n => { return n.charAt(0).toLowerCase(); }).join("");
 		// First retrieve all users with similar prefix initials
-		UserEntity.getInitialsByPrefix(initials, (err,userArray) => {
+		UserModel.getInitialsByPrefix(initials, (err,userArray) => {
 			if (!!userArray && userArray.length == 1)
 				initials = initials + "2"; //thus number == users count for this hash
 			else if (!!userArray && userArray.length > 1)
@@ -25,7 +170,7 @@ const UserModel =
 				initials = initials + (Math.max(...numbers)+1);
 			}
 			Object.assign(newUser, {initials: initials});
-			UserEntity.insert(newUser, callback);
+			UserModel.insert(newUser, callback);
 		});
 	},
 
@@ -41,15 +186,10 @@ const UserModel =
 		return false;
 	},
 
-	logout: function(uid, token, cb)
-	{
-		UserEntity.removeToken(uid, token, cb);
-	},
-
 	cleanUsersDb: function()
 	{
-		UserEntity.getUnlogged( (err,unlogged) => {
-			UserEntity.remove(unlogged);
+		UserModel.getUnlogged( (err,unlogged) => {
+			UserModel.remove(unlogged);
 		});
 	},
 }
diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js
index 95cd4d7..ec56309 100644
--- a/public/javascripts/assessment.js
+++ b/public/javascripts/assessment.js
@@ -133,6 +133,9 @@ new Vue({
 				$("#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();
diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js
index 819d723..0fb5124 100644
--- a/public/javascripts/components/statements.js
+++ b/public/javascripts/components/statements.js
@@ -1,3 +1,21 @@
+/*
+ * questions group by index prefix 1.2.3 1.1 ...etc --> '1'
+
+NOTE: questions can contain parameterized exercises (how ?
+--> describe variables (syntax ?)
+--> write javascript script (OK, users trusted ? ==> safe mode possible if public website)
+Imaginary example: (using math.js)
+	<params> (avant l'exo)
+	x: math.random()
+	y: math.random()
+	M: math.matrix([[7, x], [y, -3]]);
+	res: math.det(M)
+	</params>
+	<div>Calculer le déterminant de 
+	$$\begin{matrix}7 & x\\y & -3\end{matrix}$$</div>
+	* ...
+*/
+
 Vue.component("statements", {
 	// 'answers' is an object containing
 	//   'inputs'(array),
diff --git a/public/javascripts/course.js b/public/javascripts/course.js
index 8064eaf..0e6a169 100644
--- a/public/javascripts/course.js
+++ b/public/javascripts/course.js
@@ -1,7 +1,28 @@
-// TODO: YAML format for questions, parsed from text (nested questions)
-// Then yaml parsed to json --> array of indexed questions
-// Use open mode for question banks: add setting "nbQuestions" to show nbQuestions
-// at random among active questions
+/*Draft format (compiled to json)
+
+> Some global (HTML) intro
+
+<some html question (or/+ exercise intro)>
+
+	<some html subQuestion>
+	* some answer [trigger input/index in answers]
+
+	<another subquestion>
+
+		<sub-subQuestion>
+		+ choix1
+		- choix 2
+		+ choix 3
+		- choix4
+
+		<another sub sub>
+		* answer 2 (which can
+		be on
+		several lines)
+
+<Some second question>
+* With answer
+*/
 
 new Vue({
 	el: '#course',
@@ -9,21 +30,12 @@ new Vue({
 		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
 	},
 	mounted: function() {
 		$('.modal').each( (i,elem) => {
@@ -273,79 +285,11 @@ new Vue({
 			);
 		},
 		// NOTE: artifact required for Vue v-model to behave well
-		checkBoxFixedId: function(i) {
+		checkboxFixedId: function(i) {
 			return "questionFixed" + i;
 		},
-		checkBoxActiveId: function(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/grade.js b/public/javascripts/grade.js
new file mode 100644
index 0000000..334b2a3
--- /dev/null
+++ b/public/javascripts/grade.js
@@ -0,0 +1,88 @@
+//TODO: compute grades after exam (in teacher's view)
+
+new Vue({
+	el: '#grade',
+	data: {
+		assessmentArray: assessmentArray,
+		settings: {
+			totalPoints: 20,
+			halfPoints: false,
+			zeroSum: false,
+		},
+		group: 1, //for detailed grades tables
+		grades: { }, //computed
+	},
+	mounted: function() {
+		// TODO
+	},
+	methods: {
+		// 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/grading.js b/public/javascripts/grading.js
deleted file mode 100644
index 5264d83..0000000
--- a/public/javascripts/grading.js
+++ /dev/null
@@ -1 +0,0 @@
-//TODO: similar to monitor / course (need to factor some code)
diff --git a/public/stylesheets/assessment.css b/public/stylesheets/assessment.css
index 1baab9c..8f414f5 100644
--- a/public/stylesheets/assessment.css
+++ b/public/stylesheets/assessment.css
@@ -1,67 +1 @@
-a#rightButton {
-	position: absolute;
-	top: 0;
-	right: 0;
-}
-
-button.sendAnswer {
-	display: block;
-	margin: 0 auto;
-}
-
-.question {
-	margin: 20px 5px;
-	padding: 15px 0;
-}
-
-.question label {
-	color: black;
-}
-
-.question .choiceCorrect {
-	background-color: lightgreen;
-}
-
-.question .choiceWrong {
-	background-color: peachpuff;
-}
-
-.question .wording {
-	margin-bottom: 10px;
-	overflow: auto;
-}
-
-.question .option {
-	margin-left: 15px;
-}
-
-.question p {
-	margin-top: 10px;
-}
-
-.questionInactive {
-	background-color: lightgrey;
-}
-
-.introduction {
-	padding: 20px 5px;
-}
-
-.timer {
-	font-size: 2rem;
-}
-
-table.in-question {
-	border: 1px solid black;
-	width: auto;
-	margin: 10px auto;
-}
-
-table.in-question th, table.in-question td {
-	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; }
+/* TODO */
diff --git a/public/stylesheets/course.css b/public/stylesheets/course.css
index 20c9527..d62457c 100644
--- a/public/stylesheets/course.css
+++ b/public/stylesheets/course.css
@@ -19,55 +19,3 @@ input#password {
 table.result {
 	cursor: pointer;
 }
-
-tr.stats {
-	padding-top: 10px;
-}
-
-#questionList {
-	margin: 20px 5px;
-}
-
-.question {
-	margin: 20px 0;
-}
-
-.question .choiceCorrect {
-	background-color: lightgreen;
-}
-
-.question .wording {
-	margin-bottom: 10px;
-	overflow: auto;
-}
-
-.question .option {
-	margin-left: 15px;
-}
-
-.question p {
-	margin-top: 10px;
-}
-
-.questionInactive {
-	background-color: lightgrey;
-}
-
-.introduction {
-	margin-top: 20px;
-}
-
-table.in-question {
-	border: 1px solid black;
-	width: auto;
-	margin: 10px auto;
-}
-
-table.in-question th, table.in-question td {
-	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/grade.css b/public/stylesheets/grade.css
new file mode 100644
index 0000000..2cb1f48
--- /dev/null
+++ b/public/stylesheets/grade.css
@@ -0,0 +1,11 @@
+table.result {
+	cursor: pointer;
+}
+
+tr.stats {
+	padding-top: 10px;
+}
+
+#questionList {
+	margin: 20px 5px;
+}
diff --git a/public/stylesheets/layout.css b/public/stylesheets/layout.css
index d4dc11d..81b7bfa 100644
--- a/public/stylesheets/layout.css
+++ b/public/stylesheets/layout.css
@@ -1,3 +1,14 @@
+a#rightButton {
+	position: absolute;
+	top: 0;
+	right: 0;
+}
+
 .on-left {
 	margin-right: 20px;
 }
+
+h4.title {
+	cursor: pointer;
+	background-color: lightgrey;
+}
diff --git a/public/stylesheets/monitor.css b/public/stylesheets/monitor.css
index b22b317..42a4895 100644
--- a/public/stylesheets/monitor.css
+++ b/public/stylesheets/monitor.css
@@ -1,58 +1,16 @@
 .blur {
 	background-color: lightsalmon;
 }
+
 .resize {
 	font-style: italic;
 	color: darkred;
 }
+
 .disconnect {
 	background-color: grey;
 }
 
-/* TODO: factor this piece of code from assessment (and course, and here...) */
-.question {
-	margin: 20px 5px;
-	padding: 15px 0;
-}
-.question label {
-	color: black;
-}
-.question .choiceCorrect {
-	background-color: lightgreen;
-}
-.question .choiceWrong {
-	background-color: peachpuff;
-}
-.question .wording {
-	margin-bottom: 10px;
-}
-.question .option {
-	margin-left: 15px;
-}
-.question p {
-	margin-top: 10px;
-}
-.questionInactive {
-	background-color: lightgrey;
-}
-.introduction {
-	padding: 20px 5px;
-}
-table.in-question {
-	border: 1px solid black;
-	width: auto;
-	margin: 10px auto;
-}
-
-table.in-question th, table.in-question td {
-	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; }
-
 .absent {
 	opacity: 0.5;
 	background-color: lightgrey;
diff --git a/public/stylesheets/statements.css b/public/stylesheets/statements.css
new file mode 100644
index 0000000..169e67d
--- /dev/null
+++ b/public/stylesheets/statements.css
@@ -0,0 +1,61 @@
+button.sendAnswer {
+	display: block;
+	margin: 0 auto;
+}
+
+.question {
+	margin: 20px 5px;
+	padding: 15px 0;
+}
+
+.question label {
+	color: black;
+}
+
+.question .choiceCorrect {
+	background-color: lightgreen;
+}
+
+.question .choiceWrong {
+	background-color: peachpuff;
+}
+
+.question .wording {
+	margin-bottom: 10px;
+	overflow: auto;
+}
+
+.question .option {
+	margin-left: 15px;
+}
+
+.question p {
+	margin-top: 10px;
+}
+
+.questionInactive {
+	background-color: lightgrey;
+}
+
+.introduction {
+	padding: 20px 5px;
+}
+
+.timer {
+	font-size: 2rem;
+}
+
+table.in-question {
+	border: 1px solid black;
+	width: auto;
+	margin: 10px auto;
+}
+
+table.in-question th, table.in-question td {
+	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 3a91b5a..d9f83ea 100644
--- a/routes/assessments.js
+++ b/routes/assessments.js
@@ -2,7 +2,6 @@ let router = require("express").Router();
 const access = require("../utils/access");
 const UserModel = require("../models/user");
 const AssessmentModel = require("../models/assessment");
-const AssessmentEntity = require("../entities/assessment");
 const CourseModel = require("../models/course");
 const params = require("../config/parameters");
 const validator = require("../public/javascripts/utils/validation");
@@ -68,7 +67,7 @@ router.get("/start/assessment", access.ajax, (req,res) => {
 					maxAge: params.cookieExpire,
 				});
 			}
-			res.json(ret); //contains questions+password(or paper if resuming)
+			res.json(ret); //contains password (or paper if resuming)
 		});
 	});
 });
@@ -119,7 +118,7 @@ router.get("/end/assessment", access.ajax, (req,res) => {
 	if (error.length > 0)
 		return res.json({errmsg:error});
 	// Destroy pwd, set endTime
-	AssessmentEntity.endAssessment(ObjectId(aid), number, password, (err,ret) => {
+	AssessmentModel.endAssessment(ObjectId(aid), number, password, (err,ret) => {
 		access.checkRequest(res,err,ret,"Cannot end assessment", () => {
 			res.clearCookie('password');
 			res.json({});
diff --git a/routes/courses.js b/routes/courses.js
index e07e243..89b38b3 100644
--- a/routes/courses.js
+++ b/routes/courses.js
@@ -3,7 +3,6 @@ const access = require("../utils/access.js");
 const validator = require("../public/javascripts/utils/validation");
 const sanitizeHtml = require('sanitize-html');
 const ObjectId = require("bson-objectid");
-const CourseEntity = require("../entities/course");
 const CourseModel = require("../models/course");
 
 router.get('/add/course', access.ajax, access.logged, (req,res) => {
@@ -12,7 +11,7 @@ router.get('/add/course', access.ajax, access.logged, (req,res) => {
 	let error = validator({code:code}, "Course");
 	if (error.length > 0)
 		return res.json({errmsg:error});
-	CourseEntity.insert(req.user._id, code, description, (err,course) => {
+	CourseModel.insert(req.user._id, code, description, (err,course) => {
 		access.checkRequest(res, err, course, "Course addition failed", () => {
 			res.json(course);
 		});
@@ -55,7 +54,7 @@ router.get('/get/student', access.ajax, (req,res) => {
 	let error = validator({ _id: cid, students: [{number:number}] }, "Course");
 	if (error.length > 0)
 		return res.json({errmsg:error});
-	CourseEntity.getStudent(ObjectId(cid), number, (err,ret) => {
+	CourseModel.getStudent(ObjectId(cid), number, (err,ret) => {
 		access.checkRequest(res, err, ret, "Failed retrieving student", () => {
 			res.json({student: ret.students[0]});
 		});
@@ -74,6 +73,4 @@ router.get('/remove/course', access.ajax, access.logged, (req,res) => {
 	});
 });
 
-// TODO: grading page (for at least partially open-questions exams)
-
 module.exports = router;
diff --git a/routes/pages.js b/routes/pages.js
index a78ba0d..c92f2e7 100644
--- a/routes/pages.js
+++ b/routes/pages.js
@@ -1,15 +1,14 @@
 let router = require("express").Router();
 const access = require("../utils/access");
-const UserEntity = require("../entities/user");
-const AssessmentEntity = require("../entities/assessment");
-const CourseModel = require("../models/course");
+const UserModel = require("../models/user");
 const AssessmentModel = require("../models/assessment");
+const CourseModel = require("../models/course");
 
 // Actual pages (least specific last)
 
 // List initials and count assessments
 router.get("/", (req,res) => {
-	UserEntity.getAll( (err,userArray) => {
+	UserModel.getAll( (err,userArray) => {
 		if (!!err)
 			return res.json(err);
 		res.render("index", {
@@ -76,7 +75,7 @@ router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)", (req,res) => {
 	let code = req.params["courseCode"];
 	CourseModel.getByRefs(initials, code, (err,course) => {
 		access.checkRequest(res, err, course, "Course not found", () => {
-			AssessmentEntity.getByCourse(course._id, (err2,assessmentArray) => {
+			AssessmentModel.getByCourse(course._id, (err2,assessmentArray) => {
 				if (!!err)
 					return res.json(err);
 				access.getUser(req, res, (err2,user) => {
@@ -97,6 +96,18 @@ router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)", (req,res) => {
 	});
 });
 
+// Grading students answers: --> after identification (password), always send secret with requests
+router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/grade", (req,res) => {
+	let initials = req.params["initials"];
+	let code = req.params["courseCode"];
+	// TODO: if (main) teacher, also send secret, saving one request
+	res.render("grade", {
+		title: "grade exams " + code + "/" + name,
+		initials: initials,
+		courseCode: code,
+	});
+});
+
 // Display assessment (exam or open status)
 router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z0-9._-]+)", (req,res) => {
 	let initials = req.params["initials"];
diff --git a/routes/users.js b/routes/users.js
index 2de89b0..5dab77e 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -1,7 +1,6 @@
 let router = require("express").Router();
 const validator = require('../public/javascripts/utils/validation');
 const UserModel = require('../models/user');
-const UserEntity = require('../entities/user');
 const maild = require('../utils/mailer');
 const TokenGen = require("../utils/tokenGenerator");
 const access = require("../utils/access");
@@ -12,7 +11,7 @@ function sendLoginToken(subject, to, res)
 {
 	// Set login token and send welcome(back) email with auth link
 	let token = TokenGen.generate(params.token.length);
-	UserEntity.setLoginToken(token, to._id, to.ip, (err,ret) => {
+	UserModel.setLoginToken(token, to._id, to.ip, (err,ret) => {
 		access.checkRequest(res, err, ret, "Cannot set login token", () => {
 			maild.send({
 				from: params.mail.from,
@@ -41,7 +40,7 @@ router.get('/register', access.ajax, access.unlogged, (req,res) => {
 		return res.json({errmsg:error});
 	if (!UserModel.whitelistCheck(newUser.email))
 		return res.json({errmsg: "Email not in whitelist"});
-	UserEntity.getByEmail(newUser.email, (err,user0) => {
+	UserModel.getByEmail(newUser.email, (err,user0) => {
 		access.checkRequest(res, err, !user0?["ok"]:{}, "An account exists with this email", () => {
 			UserModel.create(newUser, (err,user) => {
 				access.checkRequest(res, err, user, "Registration failed", () => {
@@ -59,7 +58,7 @@ router.get('/sendtoken', access.ajax, access.unlogged, (req,res) => {
 	let error = validator({email:email}, "User");
 	if (error.length > 0)
 		return res.json({errmsg:error});
-	UserEntity.getByEmail(email, (err,user) => {
+	UserModel.getByEmail(email, (err,user) => {
 		access.checkRequest(res, err, user, "Unknown user", () => {
 			user.ip = req.ip;
 			sendLoginToken("Token for " + params.siteURL, user, res);
@@ -73,7 +72,7 @@ router.get('/authenticate', access.unlogged, (req,res) => {
 	let error = validator({token:loginToken}, "User");
 	if (error.length > 0)
 		return res.json({errmsg:error});
-	UserEntity.getByLoginToken(loginToken, (err,user) => {
+	UserModel.getByLoginToken(loginToken, (err,user) => {
 		access.checkRequest(res, err, user, "Invalid token", () => {
 			if (user.loginToken.ip != req.ip)
 				return res.json({errmsg: "IP address mismatch"});
@@ -84,7 +83,7 @@ router.get('/authenticate', access.unlogged, (req,res) => {
 				return res.json({errmsg: "Token expired"});
 			// Generate and update session token + destroy login token
 			let token = TokenGen.generate(params.token.length);
-			UserEntity.setSessionToken(token, user._id, (err,ret) => {
+			UserModel.setSessionToken(token, user._id, (err,ret) => {
 				access.checkRequest(res, err, ret, "Authentication failed", () => {
 					// Set cookies and redirect to user main control panel
 					res.cookie("token", token, {
@@ -103,7 +102,7 @@ router.get('/authenticate', access.unlogged, (req,res) => {
 });
 
 router.get('/logout', access.logged, (req,res) => {
-	UserModel.logout(req.user._id, req.cookies.token, (err,ret) => {
+	UserModel.removeToken(req.user._id, req.cookies.token, (err,ret) => {
 		access.checkRequest(res, err, ret, "Logout failed", () => {
 			res.clearCookie("initials");
 			res.clearCookie("token");
diff --git a/stt.csv b/setup/students_small_test.csv
similarity index 100%
rename from stt.csv
rename to setup/students_small_test.csv
diff --git a/sockets.js b/sockets.js
index 57fd657..30beb32 100644
--- a/sockets.js
+++ b/sockets.js
@@ -1,6 +1,5 @@
 const message = require("./public/javascripts/utils/socketMessages");
 const params = require("./config/parameters");
-const AssessmentEntity = require("./entities/assessment");
 const AssessmentModel = require("./models/assessment");
 const ObjectId = require("bson-objectid");
 
@@ -43,7 +42,7 @@ module.exports = function(io)
 					socket.broadcast.to(aid + "_teacher").emit(message.studentFullscreen, m);
 				});
 				socket.on("disconnect", () => { //notify monitor + server
-					AssessmentEntity.setDiscoTime(ObjectId(aid), number);
+					AssessmentModel.setDiscoTime(ObjectId(aid), number);
 					socket.broadcast.to(aid + "_teacher").emit(message.studentDisconnect, {number: number});
 				});
 			});
diff --git a/utils/access.js b/utils/access.js
index 1f91724..23a0b79 100644
--- a/utils/access.js
+++ b/utils/access.js
@@ -1,5 +1,5 @@
 const _ = require("underscore");
-const UserEntity = require("../entities/user");
+const UserModel = require("../models/user");
 
 let Access =
 {
@@ -7,7 +7,7 @@ let Access =
 	{
 		if (!res.locals.loggedIn)
 			return callback({errmsg: "Not logged in!"}, undefined);
-		UserEntity.getBySessionToken(req.cookies.token, function(err, user) {
+		UserModel.getBySessionToken(req.cookies.token, function(err, user) {
 			if (!user)
 				return callback({errmsg: "Not logged in!"}, undefined);
 			return callback(null, user);
diff --git a/views/assessment.pug b/views/assessment.pug
index 87d9fba..531354d 100644
--- a/views/assessment.pug
+++ b/views/assessment.pug
@@ -1,6 +1,7 @@
 extends withQuestions
 
 block append stylesheets
+	link(rel="stylesheet" href="/stylesheets/statements.css")
 	link(rel="stylesheet" href="/stylesheets/assessment.css")
 	noscript
 		meta(http-equiv="Refresh" content="0; URL=/enablejs")
diff --git a/views/course.pug b/views/course.pug
index 87788e3..8e2fe51 100644
--- a/views/course.pug
+++ b/views/course.pug
@@ -1,6 +1,7 @@
 extends withQuestions
 
 block append stylesheets
+	link(rel="stylesheet" href="/stylesheets/statements.css")
 	link(rel="stylesheet" href="/stylesheets/course.css")
 
 block content
@@ -17,6 +18,15 @@ block content
 						a.waves-effect.waves-light.btn(href="#!" @click="addAssessment()")
 							span Submit
 							i.material-icons.right send
+		.row(v-show="mode=='view'")
+			
+			
+			
+			
+			
+			
+			
+			
 			#assessmentSettings.modal
 				.modal-content
 					form
@@ -26,6 +36,9 @@ block content
 						p
 							input#secure(name="status" type="radio" value="secure" v-model="assessment.mode")
 							label(for="secure") Exam mode, secured (class only)
+						p
+							input#watch(name="status" type="radio" value="watch" v-model="assessment.mode")
+							label(for="watch") Exam mode, watched (class only)
 						p
 							input#exam(name="status" type="radio" value="exam" v-model="assessment.mode")
 							label(for="exam") Exam mode, free (class only)
@@ -59,39 +72,13 @@ block content
 				.modal-footer
 					.center-align
 						a.modal-action.modal-close.waves-effect.waves-light.btn-flat(href="#!") Done
-			#gradeSettings.modal
-				.modal-content
-					form(@submit.prevent="computeGrades")
-						.input-field
-							input#points(type="number" v-model.number="settings.totalPoints" required)
-							label(for="points") Total points
-						p
-							input#partial(type="checkbox" v-model="settings.halfPoint")
-							label(for="partial") Half point for partial answers? (&ge; 50%)
-						p
-							input#malus(type="checkbox" v-model="settings.zeroSum")
-							label(for="malus") Lose points on wrong answers? ("Zero-sum" game)
-				.modal-footer
-					.center-align
-						a.modal-action.modal-close.waves-effect.waves-light.btn(href="#!" @click="computeGrades()")
-							span Compute
-							i.material-icons.right send
-			#detailedGrades.modal
-				.modal-content
-					table
-						thead
-							tr
-								th Number
-								th(v-for="assessment in assessmentArray") {{ assessment.name }}
-						tbody
-							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) }}
-				.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
 				if teacher
 					h4.title(@click="toggleDisplay('students')") Students
@@ -131,32 +118,6 @@ block content
 								td {{ assessment.mode }}
 								td {{ assessment.questions.reduce( (a,b) => { return b.active ? a+1 : a; }, 0) }}
 								td {{ assessment.time }}
-				if teacher
-					h4.title(@click="toggleDisplay('grades')") Grades
-					.card(v-show="display=='grades'")
-						.center-align
-							button.on-left.waves-effect.waves-light.btn(@click="gradeSettings()") Settings
-							a#download.hide(href="#" ref="download")
-							button.waves-effect.waves-light.btn(@click="download") Download
-						ul.tabs.tabs-fixed-width
-							li.tab
-								a(href="#group0") All
-							li.tab(v-for="group in groupList()")
-								a(:href="groupId(group,'#')") G.{{ group }}
-						table.result(:id="groupId(group)" v-for="group in [0].concat(groupList())" @click="showDetails(group)")
-							thead
-								tr
-									th Number
-									th Name
-									th Final
-							tbody
-								tr.grade(v-for="student in studentList(group)")
-									td {{ student.number }}
-									td {{ student.name }}
-									td grade...
-									//td {{ grades[student.number].final }}
-								tr.stats
-									td(colspan="4") Stats: range= stdev= mean=
 		if teacher
 			.row(v-show="mode=='edit'")
 				.col.s12.m10.offset-m1
@@ -172,10 +133,12 @@ block content
 								.wording(v-html="question.wording")
 								.option(v-for="(option,j) in question.options" :class="{choiceCorrect:question.answer.includes(j)}" v-html="option")
 								p
-									input(:id="checkBoxFixedId(i)" type="checkbox" v-model="question.fixed")
-									label.on-left(:for="checkBoxFixedId(i)") Fixed
-									input(:id="checkBoxActiveId(i)" type="checkbox" v-model="question.active")
-									label(:for="checkBoxActiveId(i)") Active
+									input(:id="checkboxFixedId(i)" type="checkbox" v-model="question.fixed")
+									label.on-left(:for="checkboxFixedId(i)") Fixed
+									input(:id="checkboxActiveId(i)" type="checkbox" v-model="question.active")
+									label(:for="checkboxActiveId(i)") Active
+									input(time default 0 = untimed)
+									label Time for the question
 						.center-align
 							button.waves-effect.waves-light.btn.on-left(@click="mode='view'") Cancel
 							button.waves-effect.waves-light.btn(@click="updateAssessment") Send
diff --git a/views/grade.pug b/views/grade.pug
new file mode 100644
index 0000000..d38032a
--- /dev/null
+++ b/views/grade.pug
@@ -0,0 +1,111 @@
+extends withQuestions
+
+block append stylesheets
+	link(rel="stylesheet" href="/stylesheets/statements.css")
+	link(rel="stylesheet" href="/stylesheets/grade.css")
+
+block rightMenu
+	a#rightButton.btn-floating.btn-large.grey(href="grade")
+		i.material-icons mode_edit
+
+
+			#gradeSettings.modal
+				.modal-content
+					form(@submit.prevent="computeGrades")
+						.input-field
+							input#points(type="number" v-model.number="settings.totalPoints" required)
+							label(for="points") Total points
+						p
+							input#partial(type="checkbox" v-model="settings.halfPoint")
+							label(for="partial") Half point for partial answers? (&ge; 50%)
+						p
+							input#malus(type="checkbox" v-model="settings.zeroSum")
+							label(for="malus") Lose points on wrong answers? ("Zero-sum" game)
+				.modal-footer
+					.center-align
+						a.modal-action.modal-close.waves-effect.waves-light.btn(href="#!" @click="computeGrades()")
+							span Compute
+							i.material-icons.right send
+			#detailedGrades.modal
+				.modal-content
+					table
+						thead
+							tr
+								th Number
+								th(v-for="assessment in assessmentArray") {{ assessment.name }}
+						tbody
+							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) }}
+				.modal-footer
+					.center-align
+						a.modal-action.modal-close.waves-effect.waves-light.btn-flat(href="#!") Close
+
+
+block content
+	.container#grading
+		.row
+			.col.s12.m10.offset-m1
+				h4 #{courseCode} grading
+				// TODO: Allow grading per student, per question or sub-question
+				.card
+					ul.tabs.tabs-fixed-width
+						li.tab
+							a(href="#group0") All
+						li.tab(v-for="group in groupList()")
+							a(:href="groupId(group,'#')") G.{{ group }}
+					table(:id="groupId(group)" v-for="group in [0].concat(groupList())")
+						thead
+							tr
+								th Name
+								th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }}
+						tbody
+							tr.assessment(v-for="s in studentList(group)")
+								td {{ s.name }}
+								td(v-for="(q,i) in assessment.questions" :style="{background-color: getColor(number,i)}" @click="seeDetails(number,i)") &nbsp;
+				h4.title(@click="toggleDisplay('assessment')") Assessment
+				div(v-show="display=='assessment'")
+					.card
+						.introduction(v-html="assessment.introduction")
+					.card
+						statements(:questions="assessment.questions" :answers:"answers")
+
+
+
+				if teacher
+					h4.title(@click="toggleDisplay('grades')") Grades
+					.card(v-show="display=='grades'")
+						.center-align
+							button.on-left.waves-effect.waves-light.btn(@click="gradeSettings()") Settings
+							a#download.hide(href="#" ref="download")
+							button.waves-effect.waves-light.btn(@click="download") Download
+						ul.tabs.tabs-fixed-width
+							li.tab
+								a(href="#group0") All
+							li.tab(v-for="group in groupList()")
+								a(:href="groupId(group,'#')") G.{{ group }}
+						table.result(:id="groupId(group)" v-for="group in [0].concat(groupList())" @click="showDetails(group)")
+							thead
+								tr
+									th Number
+									th Name
+									th Final
+							tbody
+								tr.grade(v-for="student in studentList(group)")
+									td {{ student.number }}
+									td {{ student.name }}
+									td grade...
+									//td {{ grades[student.number].final }}
+								tr.stats
+									td(colspan="4") Stats: range= stdev= mean=
+
+
+
+
+block append javascripts
+	script.
+		const initials = "#{initials}";
+		const courseCode = "#{courseName}";
+	script(src="/javascripts/utils/sha1.js")
+	script(src="/javascripts/grade.js")
diff --git a/views/grading.pug b/views/grading.pug
deleted file mode 100644
index 5a0abf4..0000000
--- a/views/grading.pug
+++ /dev/null
@@ -1,39 +0,0 @@
-extends withQuestions
-
-block content
-	.container#grading
-		.row
-			.col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
-				h4= examName
-				h4.title(@click="toggleDisplay('grading')") Grading
-				// TODO: Allow grading per student, per question or sub-question
-				.card(v-show="display=='grading'")
-					ul.tabs.tabs-fixed-width
-						li.tab
-							a(href="#group0") All
-						li.tab(v-for="group in groupList()")
-							a(:href="groupId(group,'#')") G.{{ group }}
-					table(:id="groupId(group)" v-for="group in [0].concat(groupList())")
-						thead
-							tr
-								th Name
-								th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }}
-						tbody
-							tr.assessment(v-for="s in studentList(group)")
-								td {{ s.name }}
-								td(v-for="(q,i) in assessment.questions" :style="{background-color: getColor(number,i)}" @click="seeDetails(number,i)") &nbsp;
-				h4.title(@click="toggleDisplay('assessment')") Assessment
-				div(v-show="display=='assessment'")
-					.card
-						.introduction(v-html="assessment.introduction")
-					.card
-						statements(:questions="assessment.questions" :answers:"answers")
-
-block append javascripts
-	script.
-		const examName = "#{examName}";
-		const courseCode = "#{courseName}";
-		const initials = "#{initials}";
-	script(src="/javascripts/components/statements.js")
-	script(src="/javascripts/utils/sha1.js")
-	script(src="/javascripts/grading.js")
diff --git a/views/index.pug b/views/index.pug
index 9856758..4eb1653 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -6,7 +6,7 @@ block stylesheets
 block content
 	.container
 		.row
-			.card.col.s12.m8.offset-m2.l6.offset-l3.xl4.offset-xl4
+			.card.col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
 				table
 					thead
 						tr
diff --git a/views/login.pug b/views/login.pug
index 31adc75..32f0495 100644
--- a/views/login.pug
+++ b/views/login.pug
@@ -6,7 +6,7 @@ block stylesheets
 block content
 	.container#login
 		.row
-			.col.s12.m8.offset-m2.l6.offset-l3.xl4.offset-xl4
+			.col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3
 				.card#form
 					form(@submit.prevent="submit")
 						.input-field
diff --git a/views/monitor.pug b/views/monitor.pug
index b3da2d6..671b136 100644
--- a/views/monitor.pug
+++ b/views/monitor.pug
@@ -1,6 +1,7 @@
 extends withQuestions
 
 block append stylesheets
+	link(rel="stylesheet" href="/stylesheets/statements.css")
 	link(rel="stylesheet" href="/stylesheets/monitor.css")
 
 block content