From 71d1ca9c594b64d959c608a2abbff926480abad5 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Mon, 5 Feb 2018 00:07:42 +0100 Subject: [PATCH] Basic monitoring OK (sockets non-functional atm) --- README.md | 3 +- TODO | 10 ++- app.js | 6 +- config/parameters.js.dist | 3 + models/assessment.js | 24 ++++-- public/javascripts/assessment.js | 52 ++++++------ public/javascripts/components/statements.js | 8 +- public/javascripts/course.js | 4 +- public/javascripts/grading.js | 1 + public/javascripts/monitor.js | 89 +++++++++++++++++---- public/stylesheets/assessment.css | 11 +++ public/stylesheets/course.css | 11 +++ public/stylesheets/monitor.css | 42 +++++++++- routes/assessments.js | 31 ++++++- routes/courses.js | 2 + routes/pages.js | 7 +- sockets.js | 19 +---- views/assessment.pug | 14 ++-- views/course.pug | 2 +- views/grading.pug | 43 ++++++++++ views/monitor.pug | 51 ++++++++---- 21 files changed, 334 insertions(+), 99 deletions(-) create mode 100644 public/javascripts/grading.js create mode 100644 views/grading.pug diff --git a/README.md b/README.md index d1a22b5..845524b 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,7 @@ 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 the monitoring + socket part is still unimplemented, -and exams composition is limited to single question exercises. +*Note:* for now exams composition is limited to single question exercises. Automatic grades are also not available. ## Installation diff --git a/TODO b/TODO index 0d3b5e7..8670fcb 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,11 @@ -permettre temps par question : gestion côté serveur, y réfléchir... -auto grading + finish erdiag + corr tp1 +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) + ----- TODO: format général TXT: (compilé en JSON) diff --git a/app.js b/app.js index 5e7fdfe..0fd56f9 100644 --- a/app.js +++ b/app.js @@ -48,8 +48,10 @@ app.use(function(req, res, next) { // Error handler app.use(function(err, req, res, next) { - // Set locals, only providing error in development - res.locals.message = err.message; + // Set locals, only providing error in development (TODO: difference req.app and app ?!) + res.locals.message = err.message; + if (app.get('env') === 'development') + console.log(err); res.locals.error = req.app.get('env') === 'development' ? err : {}; res.status(err.status || 500); res.render('error'); diff --git a/config/parameters.js.dist b/config/parameters.js.dist index 82ca2ae..66e0bd1 100644 --- a/config/parameters.js.dist +++ b/config/parameters.js.dist @@ -6,6 +6,9 @@ Parameters.siteURL = "http://localhost"; // Lifespan of a (login) cookie Parameters.cookieExpire = 183*24*3600*1000; //6 months in milliseconds +// Secret string used in monitoring page + review +Parameters.secret = "ImNotSoSecretChangeMe"; + // Characters in a login token, and period of validity (in milliseconds) Parameters.token = { length: 16, diff --git a/models/assessment.js b/models/assessment.js index 9269aee..9ab92ba 100644 --- a/models/assessment.js +++ b/models/assessment.js @@ -23,6 +23,18 @@ const AssessmentModel = }); }, + checkPassword: function(aid, number, password, cb) + { + AssessmentEntity.getById(aid, (err,assessment) => { + if (!!err || !assessment) + return cb(err, assessment); + const paperIdx = assessment.papers.findIndex( item => { return item.number == number; }); + if (paperIdx === -1) + return cb({errmsg: "Paper not found"}, false); + cb(null, assessment.papers[paperIdx].password == password); + }); + }, + add: function(uid, cid, name, cb) { // 1) Check that course is owned by user of ID uid @@ -38,9 +50,9 @@ const AssessmentModel = update: function(uid, assessment, cb) { - const qid = ObjectId(assessment._id); + const aid = ObjectId(assessment._id); // 1) Check that assessment is owned by user of ID uid - AssessmentEntity.getById(qid, (err,assessmentOld) => { + AssessmentEntity.getById(aid, (err,assessmentOld) => { if (!!err || !assessmentOld) return cb({errmsg: "Assessment retrieval failure"}); CourseEntity.getById(ObjectId(assessmentOld.cid), (err2,course) => { @@ -51,7 +63,7 @@ const AssessmentModel = // 2) Replace assessment delete assessment["_id"]; assessment.cid = ObjectId(assessment.cid); - AssessmentEntity.replace(qid, assessment, cb); + AssessmentEntity.replace(aid, assessment, cb); }); }); }, @@ -62,12 +74,14 @@ const AssessmentModel = AssessmentEntity.getPaperByNumber(aid, number, (err,paper) => { if (!!err) return cb(err,null); + if (!paper && !!password) + return cb({errmsg: "Cannot start a new exam before finishing current"},null); if (!!paper) { if (!password) - return cb({errmsg:"Missing password"}); + return cb({errmsg: "Missing password"}); if (paper.password != password) - return cb({errmsg:"Wrong password"}); + return cb({errmsg: "Wrong password"}); } AssessmentEntity.getQuestions(aid, (err,questions) => { if (!!err) diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js index 20ca2cb..09caf12 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; }; -new Vue({ +let V = new Vue({ el: "#assessment", data: { assessment: assessment, @@ -62,7 +62,7 @@ new Vue({ }, methods: { // In case of AJAX errors - warning: function(message) { + showWarning: function(message) { this.warnMsg = message; $("#warning").modal("open"); }, @@ -73,7 +73,7 @@ new Vue({ }, trySendCurrentAnswer: function() { if (this.stage == 2) - this.sendAnswer(assessment.indices[assessment.index]); + this.sendAnswer(); }, // stage 0 --> 1 getStudent: function(cb) { @@ -86,7 +86,7 @@ new Vue({ dataType: "json", success: s => { if (!!s.errmsg) - return this.warning(s.errmsg); + return this.showWarning(s.errmsg); this.stage = 1; this.student = s.student; Vue.nextTick( () => { Materialize.updateTextFields(); }); @@ -114,7 +114,7 @@ new Vue({ assessment.questions = questions; this.answers.inputs = [ ]; for (let q of assessment.questions) - this.inputs.push( _(q.options.length).times( _.constant(false) ) ); + this.answers.inputs.push( _(q.options.length).times( _.constant(false) ) ); if (!paper) { this.answers.indices = assessment.fixed @@ -129,7 +129,7 @@ new Vue({ this.answers.indices = indices.concat( _.shuffle(remainingIndices) ); } this.answers.index = !!paper ? paper.inputs.length : 0; - Vue.nextTick(libsRefresh); + Vue.nextTick(statementsLibsRefresh); this.stage = 2; }; if (assessment.mode == "open") @@ -143,7 +143,7 @@ new Vue({ dataType: "json", success: s => { if (!!s.errmsg) - return this.warning(s.errmsg); + return this.showWarning(s.errmsg); if (!!s.paper) { // Resuming: receive stored answers + startTime @@ -157,7 +157,7 @@ new Vue({ // action (power failure, computer down, ...) } socket = io.connect("/" + assessment.name, { - query: "number=" + this.student.number + "&password=" + this.password + query: "aid=" + assessment._id + "&number=" + this.student.number + "&password=" + this.student.password }); socket.on(message.allAnswers, this.setAnswers); initializeStage2(s.questions, s.paper); @@ -171,30 +171,31 @@ new Vue({ let self = this; setInterval( function() { self.remainingTime--; - if (self.remainingTime <= 0 || self.stage >= 4) - self.endAssessment(); + if (self.remainingTime <= 0) + { + if (self.stage == 2) + self.endAssessment(); clearInterval(this); + } }, 1000); }, // stage 2 - // TODO: currentIndex ? click: () => this.sendAnswer(assessment.indices[assessment.index]), - // De même, cette condition sur le display d'une question doit remonter (résumée dans 'index' property) : - // à faire par ici : "hide": this.stage == 2 && assessment.display == 'one' && assessment.indices[assessment.index] != i, - sendAnswer: function(realIndex) { + sendAnswer: function() { + const realIndex = this.answers.indices[this.answers.index]; let gotoNext = () => { - if (assessment.index == assessment.questions.length - 1) + if (this.answers.index == assessment.questions.length - 1) this.endAssessment(); else - assessment.index++; - this.$forceUpdate(); //TODO: shouldn't be required + this.answers.index++; + this.$children[0].$forceUpdate(); //TODO: bad HACK, and shouldn't be required... }; if (assessment.mode == "open") return gotoNext(); //only local let answerData = { aid: assessment._id, answer: JSON.stringify({ - index:realIndex.toString(), - input:this.inputs[realIndex] + index: realIndex.toString(), + input: this.answers.inputs[realIndex] .map( (tf,i) => { return {val:tf,idx:i}; } ) .filter( item => { return item.val; }) .map( item => { return item.idx; }) @@ -208,9 +209,8 @@ new Vue({ dataType: "json", success: ret => { if (!!ret.errmsg) - return this.$emit("warning", ret.errmsg); - else - gotoNext(); + return this.showWarning(ret.errmsg); + gotoNext(); socket.emit(message.newAnswer, answerData); }, }); @@ -235,7 +235,7 @@ new Vue({ dataType: "json", success: ret => { if (!!ret.errmsg) - return this.warning(ret.errmsg); + return this.showWarning(ret.errmsg); assessment.conclusion = ret.conclusion; this.stage = 3; delete this.student["password"]; //unable to send new answers now @@ -245,9 +245,9 @@ new Vue({ }); }, // stage 3 --> 4 (on socket message "feedback") - setAnswers: function(answers) { - for (let i=0; i