From a3080c337cfaca9d600911396cae5a9233d43554 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 15 Feb 2018 01:34:00 +0100 Subject: [PATCH] 'update' --- README.md | 2 - gulpfile.js | 2 - models/course.js | 6 +- models/{assessment.js => evaluation.js} | 164 ++++++++--------- public/javascripts/components/statements.js | 8 +- public/javascripts/course.js | 116 ++++++------ .../{assessment.js => evaluation.js} | 82 ++++----- public/javascripts/grade.js | 18 +- public/javascripts/monitor.js | 34 ++-- public/javascripts/utils/validation.js | 4 +- routes/all.js | 2 +- routes/{assessments.js => evaluations.js} | 60 +++--- routes/pages.js | 40 ++-- setup/database.js | 4 +- sockets.js | 8 +- views/course.pug | 171 +++++++----------- views/{assessment.pug => evaluation.pug} | 0 views/grade.pug | 18 +- views/monitor.pug | 14 +- 19 files changed, 356 insertions(+), 397 deletions(-) rename models/{assessment.js => evaluation.js} (69%) rename public/javascripts/{assessment.js => evaluation.js} (80%) rename routes/{assessments.js => evaluations.js} (60%) rename views/{assessment.pug => evaluation.pug} (100%) diff --git a/README.md b/README.md index 0fc0587..3de47b1 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,6 @@ 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) diff --git a/gulpfile.js b/gulpfile.js index a1286c5..b05610e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,8 +9,6 @@ var nodemonOptions = { watch: ['./','config','utils','routes','models','entities'] }; -// TODO: tasks for uglify / sass / use webpack - gulp.task('server', function () { nodemon(nodemonOptions) .on('restart', function () { diff --git a/models/course.js b/models/course.js index 631bab9..cbe2c44 100644 --- a/models/course.js +++ b/models/course.js @@ -1,5 +1,5 @@ const UserModel = require("../models/user"); -const AssessmentModel = require("../models/assessment"); +const EvaluationModel = require("../models/evaluation"); const db = require("../utils/database"); const CourseModel = @@ -159,8 +159,8 @@ const CourseModel = CourseModel.getById(cid, (err,course) => { if (!!err || !course || !course.uid.equals(uid)) return cb({errmsg:"Not your course"},{}); - // 2) remove all associated assessments - AssessmentModel.removeGroup(cid, (err2,ret) => { + // 2) remove all associated evaluations + EvaluationModel.removeGroup(cid, (err2,ret) => { if (!!err) return cb(err,{}); // 3) remove course (with its students) diff --git a/models/assessment.js b/models/evaluation.js similarity index 69% rename from models/assessment.js rename to models/evaluation.js index de3d2d7..89f2560 100644 --- a/models/assessment.js +++ b/models/evaluation.js @@ -4,7 +4,7 @@ const ObjectId = require("bson-objectid"); const TokenGen = require("../utils/tokenGenerator"); const db = require("../utils/database"); -const AssessmentModel = +const EvaluationModel = { /* * Structure: @@ -24,7 +24,7 @@ const AssessmentModel = * 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? + * active: boolean, is question in current evaluation? * points: points for this question (default 1) * param: parameter (if applicable) * papers : array of @@ -40,17 +40,17 @@ const AssessmentModel = ////////////////// // BASIC FUNCTIONS - getById: function(aid, callback) + getById: function(eid, callback) { - db.assessments.findOne( - { _id: aid }, + db.evaluations.findOne( + { _id: eid }, callback ); }, getByPath: function(cid, name, callback) { - db.assessments.findOne( + db.evaluations.findOne( { cid: cid, name: name, @@ -61,7 +61,7 @@ const AssessmentModel = insert: function(cid, name, callback) { - db.assessments.insert( + db.evaluations.insert( { name: name, cid: cid, @@ -81,34 +81,34 @@ const AssessmentModel = getByCourse: function(cid, callback) { - db.assessments.find( + db.evaluations.find( { cid: cid }, callback ); }, - // arg: full assessment without _id field - replace: function(aid, assessment, cb) + // arg: full evaluation without _id field + replace: function(eid, evaluation, cb) { // Should be: (but unsupported by mongojs) -// db.assessments.replaceOne( -// { _id: aid }, -// assessment, +// db.evaluations.replaceOne( +// { _id: eid }, +// evaluation, // cb // ); // Temporary workaround: - db.assessments.update( - { _id: aid }, - { $set: assessment }, + db.evaluations.update( + { _id: eid }, + { $set: evaluation }, cb ); }, - getQuestions: function(aid, callback) + getQuestions: function(eid, callback) { - db.assessments.findOne( + db.evaluations.findOne( { - _id: aid, + _id: eid, display: "all", }, { questions: 1}, @@ -118,11 +118,11 @@ const AssessmentModel = ); }, - getQuestion: function(aid, index, callback) + getQuestion: function(eid, index, callback) { - db.assessments.findOne( + db.evaluations.findOne( { - _id: aid, + _id: eid, display: "one", }, { questions: 1}, @@ -137,11 +137,11 @@ const AssessmentModel = ); }, - getPaperByNumber: function(aid, number, callback) + getPaperByNumber: function(eid, number, callback) { - db.assessments.findOne( + db.evaluations.findOne( { - _id: aid, + _id: eid, "papers.number": number, }, (err,a) => { @@ -159,11 +159,11 @@ const AssessmentModel = // 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) + addDisco: function(eid, number, deltaTime) { - db.assessments.update( + db.evaluations.update( { - _id: aid, + _id: eid, "papers.number": number, }, { $inc: { @@ -174,21 +174,21 @@ const AssessmentModel = ); }, - setDiscoTime: function(aid, number) + setDiscoTime: function(eid, number) { - db.assessments.update( + db.evaluations.update( { - _id: aid, + _id: eid, "papers.number": number, }, { $set: { "papers.$.discoTime": Date.now() } } ); }, - getDiscoTime: function(aid, number, cb) + getDiscoTime: function(eid, number, cb) { - db.assessments.findOne( - { _id: aid }, + db.evaluations.findOne( + { _id: eid }, (err,a) => { if (!!err) return cb(err, null); @@ -198,11 +198,11 @@ const AssessmentModel = ); }, - hasInput: function(aid, number, password, idx, cb) + hasInput: function(eid, number, password, idx, cb) { - db.assessments.findOne( + db.evaluations.findOne( { - _id: aid, + _id: eid, "papers.number": number, "papers.password": password, }, @@ -221,11 +221,11 @@ const AssessmentModel = }, // https://stackoverflow.com/questions/27874469/mongodb-push-in-nested-array - setInput: function(aid, number, password, input, callback) //input: index + arrayOfInt (or txt) + setInput: function(eid, number, password, input, callback) //input: index + arrayOfInt (or txt) { - db.assessments.update( + db.evaluations.update( { - _id: aid, + _id: eid, "papers.number": number, "papers.password": password, }, @@ -234,11 +234,11 @@ const AssessmentModel = ); }, - endAssessment: function(aid, number, password, callback) + endEvaluation: function(eid, number, password, callback) { - db.assessments.update( + db.evaluations.update( { - _id: aid, + _id: eid, "papers.number": number, "papers.password": password, }, @@ -250,17 +250,17 @@ const AssessmentModel = ); }, - remove: function(aid, cb) + remove: function(eid, cb) { - db.assessments.remove( - { _id: aid }, + db.evaluations.remove( + { _id: eid }, cb ); }, removeGroup: function(cid, cb) { - db.assessments.remove( + db.evaluations.remove( { cid: cid }, cb ); @@ -277,24 +277,24 @@ const AssessmentModel = CourseModel.getByPath(user._id, code, (err2,course) => { if (!!err2 || !course) return cb(err2 || {errmsg: "Course not found"}); - AssessmentModel.getByPath(course._id, name, (err3,assessment) => { - if (!!err3 || !assessment) - return cb(err3 || {errmsg: "Assessment not found"}); - cb(null,assessment); + EvaluationModel.getByPath(course._id, name, (err3,evaluation) => { + if (!!err3 || !evaluation) + return cb(err3 || {errmsg: "Evaluation not found"}); + cb(null,evaluation); }); }); }); }, - checkPassword: function(aid, number, password, cb) + checkPassword: function(eid, number, password, cb) { - AssessmentModel.getById(aid, (err,assessment) => { - if (!!err || !assessment) - return cb(err, assessment); - const paperIdx = assessment.papers.findIndex( item => { return item.number == number; }); + EvaluationModel.getById(eid, (err,evaluation) => { + if (!!err || !evaluation) + return cb(err, evaluation); + const paperIdx = evaluation.papers.findIndex( item => { return item.number == number; }); if (paperIdx === -1) return cb({errmsg: "Paper not found"}, false); - cb(null, assessment.papers[paperIdx].password == password); + cb(null, evaluation.papers[paperIdx].password == password); }); }, @@ -306,35 +306,35 @@ const AssessmentModel = return cb({errmsg: "Course retrieval failure"}); if (!course.uid.equals(uid)) return cb({errmsg:"Not your course"},undefined); - // 2) Insert new blank assessment - AssessmentModel.insert(cid, name, cb); + // 2) Insert new blank evaluation + EvaluationModel.insert(cid, name, cb); }); }, - update: function(uid, assessment, cb) + update: function(uid, evaluation, cb) { - const aid = ObjectId(assessment._id); - // 1) Check that assessment is owned by user of ID uid - AssessmentModel.getById(aid, (err,assessmentOld) => { - if (!!err || !assessmentOld) - return cb({errmsg: "Assessment retrieval failure"}); - CourseModel.getById(ObjectId(assessmentOld.cid), (err2,course) => { + const eid = ObjectId(evaluation._id); + // 1) Check that evaluation is owned by user of ID uid + EvaluationModel.getById(eid, (err,evaluationOld) => { + if (!!err || !evaluationOld) + return cb({errmsg: "Evaluation retrieval failure"}); + CourseModel.getById(ObjectId(evaluationOld.cid), (err2,course) => { if (!!err2 || !course) return cb({errmsg: "Course retrieval failure"}); if (!course.uid.equals(uid)) return cb({errmsg:"Not your course"},undefined); - // 2) Replace assessment - delete assessment["_id"]; - assessment.cid = ObjectId(assessment.cid); - AssessmentModel.replace(aid, assessment, cb); + // 2) Replace evaluation + delete evaluation["_id"]; + evaluation.cid = ObjectId(evaluation.cid); + EvaluationModel.replace(eid, evaluation, cb); }); }); }, // Set password in responses collection - startSession: function(aid, number, password, cb) + startSession: function(eid, number, password, cb) { - AssessmentModel.getPaperByNumber(aid, number, (err,paper) => { + EvaluationModel.getPaperByNumber(eid, number, (err,paper) => { if (!!err) return cb(err,null); if (!paper && !!password) @@ -346,14 +346,14 @@ const AssessmentModel = if (paper.password != password) return cb({errmsg: "Wrong password"}); } - AssessmentModel.getQuestions(aid, (err2,questions) => { + EvaluationModel.getQuestions(eid, (err2,questions) => { if (!!err2) return cb(err2,null); if (!!paper) return cb(null,{paper:paper}); const pwd = TokenGen.generate(12); //arbitrary number, 12 seems enough... - db.assessments.update( - { _id: aid }, + db.evaluations.update( + { _id: eid }, { $push: { papers: { number: number, startTime: Date.now(), @@ -370,15 +370,15 @@ const AssessmentModel = }); }, - newAnswer: function(aid, number, password, input, cb) + newAnswer: function(eid, number, password, input, cb) { // Check that student hasn't already answered - AssessmentModel.hasInput(aid, number, password, input.index, (err,ret) => { + EvaluationModel.hasInput(eid, number, password, input.index, (err,ret) => { if (!!err) return cb(err,null); if (!!ret) return cb({errmsg:"Question already answered"},null); - AssessmentModel.setInput(aid, number, password, input, (err2,ret2) => { + EvaluationModel.setInput(eid, number, password, input, (err2,ret2) => { if (!!err2 || !ret2) return cb(err2,ret2); return cb(null,ret2); @@ -388,14 +388,14 @@ const AssessmentModel = // NOTE: no callbacks for next function, failures are not so important // (because monitored: teachers can see what's going on) - newConnection: function(aid, number) + newConnection: function(eid, number) { //increment discoCount, reset discoTime to NULL, update totalDisco - AssessmentModel.getDiscoTime(aid, number, (err,discoTime) => { + EvaluationModel.getDiscoTime(eid, number, (err,discoTime) => { if (!!discoTime) - AssessmentModel.addDisco(aid, number, Date.now() - discoTime); + EvaluationModel.addDisco(eid, number, Date.now() - discoTime); }); }, } -module.exports = AssessmentModel; +module.exports = EvaluationModel; diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js index 64057f9..824ce76 100644 --- a/public/javascripts/components/statements.js +++ b/public/javascripts/components/statements.js @@ -15,6 +15,8 @@ Imaginary example: (using math.js) $$\begin{matrix}7 & x\\y & -3\end{matrix}$$ * ... ++ fixed + question time (syntax ?) + --> input of type text (number, or vector, or matrix e.g. in R syntax) --> parameter stored in question.param (TODO) @@ -91,8 +93,8 @@ Vue.component("statements", { "input", { domProps: { - checked: this.inputs[q.index][idx], - disabled: monitoring, + checked: !!this.inputs && this.inputs[q.index][idx], + disabled: monitoring || this.display == "solution", }, attrs: { id: this.inputId(q.index,idx), @@ -125,7 +127,7 @@ Vue.component("statements", { "class": { option: true, choiceCorrect: this.display == "solution" && q.answer.includes(idx), - choiceWrong: this.display == "solution" && this.inputs[q.index][idx] && !q.answer.includes(idx), + choiceWrong: this.display == "solution" && !!this.inputs && this.inputs[q.index][idx] && !q.answer.includes(idx), }, }, option diff --git a/public/javascripts/course.js b/public/javascripts/course.js index ab17fe3..65b6ab7 100644 --- a/public/javascripts/course.js +++ b/public/javascripts/course.js @@ -1,7 +1,5 @@ /*Draft format (compiled to json) -> Some global (HTML) intro - @@ -27,31 +25,32 @@ new Vue({ el: '#course', data: { - display: "assessments", //or "students", or "grades" (admin mode) + display: "evaluations", //or "students", or "grades" (admin mode) course: course, - mode: "view", //or "edit" (some assessment) 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 + newEvaluation: { name: "" }, + evaluationArray: evaluationArray, + mode: "view", //or "edit" (some evaluation) + evaluationIndex: 0, //current edited evaluation index + evaluation: { }, //copy of evaluation at editing index in array + questionsText: "", //questions in an evaluation, in text format }, mounted: function() { + + + $('.modal').each( (i,elem) => { - if (elem.id != "assessmentEdit") + if (elem.id != "evaluationEdit") $(elem).modal(); }); - $('ul.tabs').tabs(); - $('#assessmentEdit').modal({ + $('ul.tabs').tabs(); //--> migrate to grade.js + + + + $('#evaluationEdit').modal({ complete: () => { - this.parseAssessment(); - Vue.nextTick( () => { - $("#questionList").find("code[class^=language-]").each( (i,elem) => { - Prism.highlightElement(elem); - }); - MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]); - }); + this.parseEvaluation(); + Vue.nextTick(statementsLibsRefresh); }, }); }, @@ -108,29 +107,29 @@ new Vue({ }, }); }, - // ASSESSMENT: - addAssessment: function() { + // evaluation: + addEvaluation: function() { if (!admin) return; // modal, fill code and description - let error = Validator.checkObject(this.newAssessment, "Assessment"); + let error = Validator.checkObject(this.newEvaluation, "Evaluation"); if (!!error) return alert(error); else - $('#newAssessment').modal('close'); - $.ajax("/assessments", + $('#newEvaluation').modal('close'); + $.ajax("/evaluations", { method: "POST", data: { - name: this.newAssessment.name, + name: this.newEvaluation.name, cid: course._id, }, dataType: "json", success: res => { if (!res.errmsg) { - this.newAssessment["name"] = ""; - this.assessmentArray.push(res); + this.newEvaluation["name"] = ""; + this.evaluationArray.push(res); } else alert(res.errmsg); @@ -142,15 +141,15 @@ new Vue({ $("#" + id).modal("open"); Materialize.updateTextFields(); //textareas, time field... }, - updateAssessment: function() { - $.ajax("/assessments", { + updateEvaluation: function() { + $.ajax("/evaluations", { method: "PUT", - data: {assessment: JSON.stringify(this.assessment)}, + data: {evaluation: JSON.stringify(this.evaluation)}, dataType: "json", success: res => { if (!res.errmsg) { - this.assessmentArray[this.assessmentIndex] = this.assessment; + this.evaluationArray[this.evaluationIndex] = this.evaluation; this.mode = "view"; } else @@ -158,20 +157,20 @@ new Vue({ }, }); }, - deleteAssessment: function(assessment) { + deleteEvaluation: function(evaluation) { if (!admin) return; - if (confirm("Delete assessment '" + assessment.name + "' ?")) + if (confirm("Delete evaluation '" + evaluation.name + "' ?")) { - $.ajax("/assessments", + $.ajax("/evaluations", { method: "DELETE", - data: { qid: this.assessment._id }, + data: { qid: this.evaluation._id }, dataType: "json", success: res => { if (!res.errmsg) - this.assessmentArray.splice( this.assessmentArray.findIndex( item => { - return item._id == assessment._id; + this.evaluationArray.splice( this.evaluationArray.findIndex( item => { + return item._id == evaluation._id; }), 1 ); else alert(res.errmsg); @@ -181,16 +180,16 @@ new Vue({ } }, toggleState: function(questionIndex) { - // add or remove from activeSet of current assessment - let activeIndex = this.assessment.activeSet.findIndex( item => { return item == questionIndex; }); + // add or remove from activeSet of current evaluation + let activeIndex = this.evaluation.activeSet.findIndex( item => { return item == questionIndex; }); if (activeIndex >= 0) - this.assessment.activeSet.splice(activeIndex, 1); + this.evaluation.activeSet.splice(activeIndex, 1); else - this.assessment.activeSet.push(questionIndex); + this.evaluation.activeSet.push(questionIndex); }, - setAssessmentText: function() { + setEvaluationText: function() { let txt = ""; - this.assessment.questions.forEach( q => { + this.evaluation.questions.forEach( q => { txt += q.wording; //already ended by \n q.options.forEach( (o,i) => { let symbol = q.answer.includes(i) ? "+" : "-"; @@ -198,11 +197,11 @@ new Vue({ }); txt += "\n"; //separate questions by new line }); - this.assessmentText = txt; + this.questionsText = txt; }, - parseAssessment: function() { + parseEvaluation: function() { let questions = [ ]; - let lines = this.assessmentText.split("\n").map( L => { return L.trim(); }) + let lines = this.questionsText.split("\n").map( L => { return L.trim(); }) lines.push(""); //easier parsing let emptyQuestion = () => { return { @@ -239,28 +238,23 @@ new Vue({ } } }); - this.assessment.questions = questions; + this.evaluation.questions = questions; }, - actionAssessment: function(index) { + actionEvaluation: function(index) { if (admin) { // Edit screen - this.assessmentIndex = index; - this.assessment = $.extend(true, {}, this.assessmentArray[index]); - this.setAssessmentText(); + this.evaluationIndex = index; + this.evaluation = $.extend(true, {}, this.evaluationArray[index]); + this.setEvaluationText(); this.mode = "edit"; - Vue.nextTick( () => { - $("#questionList").find("code[class^=language-]").each( (i,elem) => { - Prism.highlightElement(elem); - }); - MathJax.Hub.Queue(["Typeset",MathJax.Hub,"questionList"]); - }); + Vue.nextTick(statementsLibsRefresh); } - else //external user: show assessment - this.redirect(this.assessmentArray[index].name); + else //external user: show evaluation + this.redirect(this.evaluationArray[index].name); }, - redirect: function(assessmentName) { - document.location.href = "/" + initials + "/" + course.code + "/" + assessmentName; + redirect: function(evaluationName) { + document.location.href = "/" + initials + "/" + course.code + "/" + evaluationName; }, setPassword: function() { let hashPwd = Sha1.Compute(this.monitorPwd); diff --git a/public/javascripts/assessment.js b/public/javascripts/evaluation.js similarity index 80% rename from public/javascripts/assessment.js rename to public/javascripts/evaluation.js index 2a451e4..565794f 100644 --- a/public/javascripts/assessment.js +++ b/public/javascripts/evaluation.js @@ -1,6 +1,6 @@ let socket = null; //monitor answers in real time -if (assessment.mode == "secure" && !checkWindowSize()) +if (evaluation.mode == "secure" && !checkWindowSize()) document.location.href= "/fullscreen"; function checkWindowSize() @@ -13,9 +13,9 @@ function checkWindowSize() } new Vue({ - el: "#assessment", + el: "#evaluation", data: { - assessment: assessment, + evaluation: evaluation, answers: { }, //filled later with answering parameters student: { }, //filled later (name, password) // Stage 0: unauthenticated (number), @@ -23,13 +23,13 @@ new Vue({ // 2: locked: password set, exam started // 3: completed // 4: show answers - remainingTime: assessment.time, //integer or array - stage: assessment.mode != "open" ? 0 : 1, + remainingTime: evaluation.time, //integer or array + stage: evaluation.mode != "open" ? 0 : 1, warnMsg: "", }, computed: { countdown: function() { - const remainingTime = assessment.display == "one" && _.isArray(assessment.time) + const remainingTime = evaluation.display == "one" && _.isArray(evaluation.time) ? this.remainingTime[this.answers.index] : this.remainingTime; let seconds = remainingTime % 60; @@ -39,12 +39,12 @@ new Vue({ }, mounted: function() { $(".modal").modal(); - if (["exam","open"].includes(assessment.mode)) + if (["exam","open"].includes(evaluation.mode)) return; window.addEventListener("blur", () => { if (this.stage != 2) return; - if (assessment.mode == "secure") + if (evaluation.mode == "secure") { this.sendAnswer(); document.location.href= "/noblur"; @@ -52,7 +52,7 @@ new Vue({ else //"watch" mode socket.emit(message.studentBlur, {number:this.student.number}); }, false); - if (assessment.mode == "watch") + if (evaluation.mode == "watch") { window.addEventListener("focus", () => { if (this.stage != 2) @@ -63,7 +63,7 @@ new Vue({ window.addEventListener("resize", e => { if (this.stage != 2) return; - if (assessment.mode == "secure") + if (evaluation.mode == "secure") { this.sendAnswer(); document.location.href = "/fullscreen"; @@ -94,7 +94,7 @@ new Vue({ method: "GET", data: { number: this.student.number, - cid: assessment.cid, + cid: evaluation.cid, }, dataType: "json", success: s => { @@ -111,7 +111,7 @@ new Vue({ this.stage = 0; }, // stage 1 --> 2 (get all questions, set password) - startAssessment: function() { + startEvaluation: function() { let initializeStage2 = paper => { $("#leftButton, #rightButton").hide(); // Initialize structured answer(s) based on questions type and nesting (TODO: more general) @@ -121,21 +121,21 @@ new Vue({ if (!!questions) - assessment.questions = questions; + evaluation.questions = questions; this.answers.inputs = [ ]; - for (let q of assessment.questions) + for (let q of evaluation.questions) this.answers.inputs.push( _(q.options.length).times( _.constant(false) ) ); if (!paper) { - this.answers.indices = assessment.fixed - ? _.range(assessment.questions.length) - : _.shuffle( _.range(assessment.questions.length) ); + this.answers.indices = evaluation.fixed + ? _.range(evaluation.questions.length) + : _.shuffle( _.range(evaluation.questions.length) ); } else { // Resuming let indices = paper.inputs.map( input => { return input.index; }); - let remainingIndices = _.difference( _.range(assessment.questions.length).map(String), indices ); + let remainingIndices = _.difference( _.range(evaluation.questions.length).map(String), indices ); this.answers.indices = indices.concat( _.shuffle(remainingIndices) ); } @@ -145,29 +145,29 @@ new Vue({ - if (assessment.time > 0) + if (evaluation.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.remainingTime = evaluation.time * 60 - Math.round(deltaTime / 1000); this.runTimer(); } this.answers.index = !!paper ? paper.inputs.length : 0; - this.answers.displayAll = assessment.display == "all"; + this.answers.displayAll = evaluation.display == "all"; this.answers.showSolution = false; this.stage = 2; }; - if (assessment.mode == "open") + if (evaluation.mode == "open") return initializeStage2(); - $.ajax("/assessments/start", { + $.ajax("/evaluations/start", { method: "PUT", data: { number: this.student.number, - aid: assessment._id + aid: evaluation._id }, dataType: "json", success: s => { @@ -186,7 +186,7 @@ new Vue({ // action (power failure, computer down, ...) } socket = io.connect("/", { - query: "aid=" + assessment._id + "&number=" + this.student.number + "&password=" + this.student.password + query: "aid=" + evaluation._id + "&number=" + this.student.number + "&password=" + this.student.password }); socket.on(message.allAnswers, this.setAnswers); initializeStage2(s.questions, s.paper); @@ -197,7 +197,7 @@ new Vue({ // stage 2 runGlobalTimer: function() { - if (assessment.time <= 0) + if (evaluation.time <= 0) return; let self = this; setInterval( function() { @@ -205,13 +205,13 @@ new Vue({ if (self.remainingTime <= 0) { if (self.stage == 2) - self.endAssessment(); + self.endEvaluation(); clearInterval(this); } }, 1000); }, runQuestionTimer: function(idx) { - if (assessment.questions[idx].time <= 0) + if (evaluation.questions[idx].time <= 0) return; let self = this; //TODO: question remaining time setInterval( function() { @@ -219,7 +219,7 @@ new Vue({ if (self.remainingTime <= 0) { if (self.stage == 2) - self.endAssessment(); + self.endEvaluation(); clearInterval(this); } }, 1000); @@ -231,16 +231,16 @@ new Vue({ sendOneAnswer: function() { const realIndex = this.answers.indices[this.answers.index]; let gotoNext = () => { - if (this.answers.index == assessment.questions.length - 1) - this.endAssessment(); + if (this.answers.index == evaluation.questions.length - 1) + this.endEvaluation(); else this.answers.index++; this.$children[0].$forceUpdate(); //TODO: bad HACK, and shouldn't be required... }; - if (assessment.mode == "open") + if (evaluation.mode == "open") return gotoNext(); //only local let answerData = { - aid: assessment._id, + aid: evaluation._id, answer: JSON.stringify({ index: realIndex.toString(), input: this.answers.inputs[realIndex] @@ -251,7 +251,7 @@ new Vue({ number: this.student.number, password: this.student.password, }; - $.ajax("/assessments/answer", { + $.ajax("/evaluations/answer", { method: "PUT", data: answerData, dataType: "json", @@ -265,27 +265,27 @@ new Vue({ }, // TODO: I don't like that + sending should not be definitive in exam mode with display = all sendAnswer: function() { - if (assessment.display == "one") + if (evaluation.display == "one") this.sendOneAnswer(); else - assessment.questions.forEach(this.sendOneAnswer); + evaluation.questions.forEach(this.sendOneAnswer); }, // stage 2 --> 3 (or 4) // from a message by statements component, or time over - endAssessment: function() { + endEvaluation: function() { // Set endTime, destroy password $("#leftButton, #rightButton").show(); - if (assessment.mode == "open") + if (evaluation.mode == "open") { this.stage = 4; this.answers.showSolution = true; this.answers.displayAll = true; return; } - $.ajax("/assessments/end", { + $.ajax("/evaluations/end", { method: "PUT", data: { - aid: assessment._id, + aid: evaluation._id, number: this.student.number, password: this.student.password, }, @@ -302,7 +302,7 @@ new Vue({ setAnswers: function(m) { const answers = JSON.parse(m.answers); for (let i=0; i { - s[assessmentName] = this.grades[s.number][assessmentName]; - if (_.isNumeric(s[assessmentName]) && !isNaN(s[assessmentName])) + Object.keys(this.grades[s.number]).forEach( evaluationName => { + s[evaluationName] = this.grades[s.number][evaluationName]; + if (_.isNumeric(s[evaluationName]) && !isNaN(s[evaluationName])) { - finalGrade += s[assessmentName]; + finalGrade += s[evaluationName]; gradesCount++; } if (gradesCount >= 1) finalGrade /= gradesCount; - s["final"] = finalGrade; //TODO: forbid "final" as assessment name + s["final"] = finalGrade; //TODO: forbid "final" as evaluation name }); } data.push(s); //number,name,group,assessName1...assessNameN,final @@ -65,10 +65,10 @@ new Vue({ }); return _.range(1,maxGrp+1); }, - grade: function(assessmentIndex, studentNumber) { - if (!this.grades[assessmentIndex] || !this.grades[assessmentIndex][studentNumber]) + grade: function(evaluationIndex, studentNumber) { + if (!this.grades[evaluationIndex] || !this.grades[evaluationIndex][studentNumber]) return ""; //no grade yet - return this.grades[assessmentIndex][studentNumber]; + return this.grades[evaluationIndex][studentNumber]; }, groupId: function(group, prefix) { return (prefix || "") + "group" + group; diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js index 29ce858..9b3964b 100644 --- a/public/javascripts/monitor.js +++ b/public/javascripts/monitor.js @@ -4,7 +4,7 @@ new Vue({ el: "#monitor", data: { password: "", //from password field - assessment: { }, //obtained after authentication + evaluation: { }, //obtained after authentication // Stage 0: unauthenticated (password), // 1: authenticated (password hash validated), start monitoring stage: 0, @@ -15,7 +15,7 @@ new Vue({ index : -1, }, students: [ ], //to know their names - display: "assessment", //or student's answers + display: "evaluation", //or student's answers }, methods: { // TODO: redundant code, next 4 funcs already exist in course.js @@ -52,11 +52,11 @@ new Vue({ { if (!s.present) continue; - const paperIdx = this.assessment.papers.findIndex( item => { return item.number == s.number; }); + const paperIdx = this.evaluation.papers.findIndex( item => { return item.number == s.number; }); if (paperIdx === -1) return false; - const paper = this.assessment.papers[paperIdx]; - if (paper.inputs.length < this.assessment.questions.length) + const paper = this.evaluation.papers[paperIdx]; + if (paper.inputs.length < this.evaluation.questions.length) return false; } return true; @@ -64,16 +64,16 @@ new Vue({ getColor: function(number, qIdx) { // For the moment, green if correct and red if wrong; grey if unanswered yet // TODO: in-between color for partially right (especially for multi-questions) - const paperIdx = this.assessment.papers.findIndex( item => { return item.number == number; }); + const paperIdx = this.evaluation.papers.findIndex( item => { return item.number == number; }); if (paperIdx === -1) return "grey"; //student didn't start yet - const inputIdx = this.assessment.papers[paperIdx].inputs.findIndex( item => { + const inputIdx = this.evaluation.papers[paperIdx].inputs.findIndex( item => { const qNum = parseInt(item.index.split(".")[0]); //indexes separated by dots return qIdx == qNum; }); if (inputIdx === -1) return "grey"; - if (_.isEqual(this.assessment.papers[paperIdx].inputs[inputIdx].input, this.assessment.questions[qIdx].answer)) + if (_.isEqual(this.evaluation.papers[paperIdx].inputs[inputIdx].input, this.evaluation.questions[qIdx].answer)) return "green"; return "red"; }, @@ -82,7 +82,7 @@ new Vue({ }, // stage 0 --> 1 startMonitoring: function() { - $.ajax("/assessments/monitor", { + $.ajax("/evaluations/monitor", { method: "GET", data: { password: Sha1.Compute(this.password), @@ -94,8 +94,8 @@ new Vue({ success: s => { if (!!s.errmsg) return alert(s.errmsg); - this.assessment = s.assessment; - this.answers.inputs = s.assessment.questions.map( q => { + this.evaluation = s.evaluation; + this.answers.inputs = s.evaluation.questions.map( q => { let input = _(q.options.length).times( _.constant(false) ); q.answer.forEach( idx => { input[idx] = true; }); return input; @@ -104,7 +104,7 @@ new Vue({ this.students.forEach( s => { s.present = true; }); //a priori... this.stage = 1; socket = io.connect("/", { - query: "aid=" + this.assessment._id + "&secret=" + s.secret + query: "aid=" + this.evaluation._id + "&secret=" + s.secret }); socket.on(message.studentBlur, m => { const sIdx = this.students.findIndex( item => { return item.number == m.number; }); @@ -134,20 +134,20 @@ new Vue({ this.students[sIdx].disco = false; }); socket.on(message.newAnswer, m => { - let paperIdx = this.assessment.papers.findIndex( item => { + let paperIdx = this.evaluation.papers.findIndex( item => { return item.number == m.number; }); if (paperIdx === -1) { // First answer - paperIdx = this.assessment.papers.length; - this.assessment.papers.push({ + paperIdx = this.evaluation.papers.length; + this.evaluation.papers.push({ number: m.number, inputs: [ ], //other fields irrelevant here }); } // TODO: notations not coherent (input / answer... when, which ?) - this.assessment.papers[paperIdx].inputs.push(JSON.parse(m.answer)); //input+index + this.evaluation.papers[paperIdx].inputs.push(JSON.parse(m.answer)); //input+index }); }, }); @@ -157,7 +157,7 @@ new Vue({ // TODO: disable this button until everyone finished (need ability to mark absents) socket.emit( message.allAnswers, - { answers: JSON.stringify(this.assessment.questions.map( q => { return q.answer; })) } + { answers: JSON.stringify(this.evaluation.questions.map( q => { return q.answer; })) } ); }, }, diff --git a/public/javascripts/utils/validation.js b/public/javascripts/utils/validation.js index 625fb42..f604f77 100644 --- a/public/javascripts/utils/validation.js +++ b/public/javascripts/utils/validation.js @@ -2,7 +2,7 @@ try { var _ = require("underscore"); } catch (err) {} //for server let Validator = { }; -// Cell in assessment.questions array +// Cell in evaluation.questions array Validator.Question = { "index": "section", //"2.2.1", "3.2", "1" ...etc "wording": "string", @@ -32,7 +32,7 @@ Validator.Paper = { "password": "password", }; -Validator.Assessment = { +Validator.Evaluation = { "_id": "bson", "cid": "bson", "name": "code", diff --git a/routes/all.js b/routes/all.js index 1c0d052..4ca00ea 100644 --- a/routes/all.js +++ b/routes/all.js @@ -3,7 +3,7 @@ var router = require("express").Router(); // AJAX requests: router.use("/", require("./users")); router.use("/", require("./courses")); -router.use("/", require("./assessments")); +router.use("/", require("./evaluations")); // Pages: router.use("/", require("./pages")); diff --git a/routes/assessments.js b/routes/evaluations.js similarity index 60% rename from routes/assessments.js rename to routes/evaluations.js index 03e483e..18f57f7 100644 --- a/routes/assessments.js +++ b/routes/evaluations.js @@ -1,7 +1,7 @@ let router = require("express").Router(); const access = require("../utils/access"); const UserModel = require("../models/user"); -const AssessmentModel = require("../models/assessment"); +const EvaluationModel = require("../models/evaluation"); const CourseModel = require("../models/course"); const params = require("../config/parameters"); const validator = require("../public/javascripts/utils/validation"); @@ -17,47 +17,47 @@ const sanitizeOpts = { }, }; -router.post("/assessments", access.ajax, access.logged, (req,res) => { +router.post("/evaluations", access.ajax, access.logged, (req,res) => { const name = req.body["name"]; const cid = req.body["cid"]; - let error = validator({cid:cid, name:name}, "Assessment"); + let error = validator({cid:cid, name:name}, "Evaluation"); if (error.length > 0) return res.json({errmsg:error}); - AssessmentModel.add(req.user._id, ObjectId(cid), name, (err,assessment) => { - access.checkRequest(res, err, assessment, "Assessment addition failed", () => { - res.json(assessment); + EvaluationModel.add(req.user._id, ObjectId(cid), name, (err,evaluation) => { + access.checkRequest(res, err, evaluation, "Evaluation addition failed", () => { + res.json(evaluation); }); }); }); -router.put("/assessments", access.ajax, access.logged, (req,res) => { - const assessment = JSON.parse(req.body["assessment"]); - let error = validator(assessment, "Assessment"); +router.put("/evaluations", access.ajax, access.logged, (req,res) => { + const evaluation = JSON.parse(req.body["evaluation"]); + let error = validator(evaluation, "Evaluation"); if (error.length > 0) return res.json({errmsg:error}); - assessment.introduction = sanitizeHtml(assessment.introduction, sanitizeOpts); - assessment.questions.forEach( q => { + evaluation.introduction = sanitizeHtml(evaluation.introduction, sanitizeOpts); + evaluation.questions.forEach( q => { q.wording = sanitizeHtml(q.wording, sanitizeOpts); //q.answer = sanitizeHtml(q.answer); //if text (TODO: it's an array in this case?!) for (let i=0; i { - access.checkRequest(res, err, ret, "Assessment update failed", () => { + EvaluationModel.update(req.user._id, evaluation, (err,ret) => { + access.checkRequest(res, err, ret, "Evaluation update failed", () => { res.json({}); }); }); }); // Generate and set student password, return it -router.put("/assessments/start", access.ajax, (req,res) => { +router.put("/evaluations/start", access.ajax, (req,res) => { let number = req.body["number"]; - let aid = req.body["aid"]; + let eid = req.body["eid"]; let password = req.cookies["password"]; //potentially from cookies, resuming - let error = validator({ _id:aid, papers:[{number:number,password:password || "samplePwd"}] }, "Assessment"); + let error = validator({ _id:eid, papers:[{number:number,password:password || "samplePwd"}] }, "Evaluation"); if (error.length > 0) return res.json({errmsg:error}); - AssessmentModel.startSession(ObjectId(aid), number, password, (err,ret) => { + EvaluationModel.startSession(ObjectId(eid), number, password, (err,ret) => { access.checkRequest(res,err,ret,"Failed session initialization", () => { if (!password) { @@ -72,7 +72,7 @@ router.put("/assessments/start", access.ajax, (req,res) => { }); }); -router.get("/assessments/monitor", access.ajax, (req,res) => { +router.get("/evaluations/monitor", access.ajax, (req,res) => { const password = req.query["password"]; const examName = req.query["aname"]; const courseCode = req.query["ccode"]; @@ -82,11 +82,11 @@ router.get("/assessments/monitor", access.ajax, (req,res) => { access.checkRequest(res,err,course,"Course not found", () => { if (password != course.password) return res.json({errmsg: "Wrong password"}); - AssessmentModel.getByRefs(initials, courseCode, examName, (err2,assessment) => { - access.checkRequest(res,err2,assessment,"Assessment not found", () => { + EvaluationModel.getByRefs(initials, courseCode, examName, (err2,evaluation) => { + access.checkRequest(res,err2,evaluation,"Evaluation not found", () => { res.json({ students: course.students, - assessment: assessment, + evaluation: evaluation, secret: params.secret, }); }); @@ -95,31 +95,31 @@ router.get("/assessments/monitor", access.ajax, (req,res) => { }); }); -router.put("/assessments/answer", access.ajax, (req,res) => { - let aid = req.body["aid"]; +router.put("/evaluations/answer", access.ajax, (req,res) => { + let eid = req.body["eid"]; let number = req.body["number"]; let password = req.body["password"]; let input = JSON.parse(req.body["answer"]); - let error = validator({ _id:aid, papers:[{number:number,password:password,inputs:[input]}] }, "Assessment"); + let error = validator({ _id:eid, papers:[{number:number,password:password,inputs:[input]}] }, "Evaluation"); if (error.length > 0) return res.json({errmsg:error}); - AssessmentModel.newAnswer(ObjectId(aid), number, password, input, (err,ret) => { + EvaluationModel.newAnswer(ObjectId(eid), number, password, input, (err,ret) => { access.checkRequest(res,err,ret,"Cannot send answer", () => { res.json({}); }); }); }); -router.put("/assessments/end", access.ajax, (req,res) => { - let aid = req.body["aid"]; +router.put("/evaluations/end", access.ajax, (req,res) => { + let eid = req.body["eid"]; let number = req.body["number"]; let password = req.body["password"]; - let error = validator({ _id:aid, papers:[{number:number,password:password}] }, "Assessment"); + let error = validator({ _id:eid, papers:[{number:number,password:password}] }, "Evaluation"); if (error.length > 0) return res.json({errmsg:error}); // Destroy pwd, set endTime - AssessmentModel.endAssessment(ObjectId(aid), number, password, (err,ret) => { - access.checkRequest(res,err,ret,"Cannot end assessment", () => { + EvaluationModel.endEvaluation(ObjectId(eid), number, password, (err,ret) => { + access.checkRequest(res,err,ret,"Cannot end evaluation", () => { res.clearCookie('password'); res.json({}); }); diff --git a/routes/pages.js b/routes/pages.js index c92f2e7..2acefc0 100644 --- a/routes/pages.js +++ b/routes/pages.js @@ -1,12 +1,12 @@ let router = require("express").Router(); const access = require("../utils/access"); const UserModel = require("../models/user"); -const AssessmentModel = require("../models/assessment"); +const EvaluationModel = require("../models/evaluation"); const CourseModel = require("../models/course"); // Actual pages (least specific last) -// List initials and count assessments +// List initials and count evaluations router.get("/", (req,res) => { UserModel.getAll( (err,userArray) => { if (!!err) @@ -75,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", () => { - AssessmentModel.getByCourse(course._id, (err2,assessmentArray) => { + EvaluationModel.getByCourse(course._id, (err2,evaluationArray) => { if (!!err) return res.json(err); access.getUser(req, res, (err2,user) => { @@ -86,7 +86,7 @@ router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)", (req,res) => { res.render("course", { title: "course " + initials + "/" + code, course: course, - assessmentArray: assessmentArray, + evaluationArray: evaluationArray, teacher: isTeacher, initials: initials, }); @@ -108,17 +108,17 @@ router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/grade", (req,res) => }); }); -// Display assessment (exam or open status) -router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z0-9._-]+)", (req,res) => { +// Display evaluation (exam or open status) +router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:evaluationName([a-z0-9._-]+)", (req,res) => { let initials = req.params["initials"]; let code = req.params["courseCode"]; - let name = req.params["assessmentName"]; - AssessmentModel.getByRefs(initials, code, name, (err,assessment) => { - access.checkRequest(res, err, assessment, "Assessment not found", () => { - if (!assessment.active) - return res.json({errmsg: "Assessment is idle"}); - delete assessment["papers"]; //always remove recorded students answers - if (assessment.mode == "exam") + let name = req.params["evaluationName"]; + EvaluationModel.getByRefs(initials, code, name, (err,evaluation) => { + access.checkRequest(res, err, evaluation, "Evaluation not found", () => { + if (!evaluation.active) + return res.json({errmsg: "Evaluation is idle"}); + delete evaluation["papers"]; //always remove recorded students answers + if (evaluation.mode == "exam") { if (!!req.headers['user-agent'].match(/(SpecialAgent|HeadlessChrome|PhantomJS)/)) { @@ -126,24 +126,24 @@ router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z return res.json({errmsg: "Headless browser detected"}); } // Strip questions if exam mode (stepwise process) - delete assessment["questions"]; + delete evaluation["questions"]; } - res.render("assessment", { - title: "assessment " + initials + "/" + code + "/" + name, - assessment: assessment, + res.render("evaluation", { + title: "evaluation " + initials + "/" + code + "/" + name, + evaluation: evaluation, }); }); }); }); // Monitor: --> after identification (password), always send secret with requests -router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z0-9._-]+)/monitor", (req,res) => { +router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:evaluationName([a-z0-9._-]+)/monitor", (req,res) => { let initials = req.params["initials"]; let code = req.params["courseCode"]; - let name = req.params["assessmentName"]; + let name = req.params["evaluationName"]; // TODO: if (main) teacher, also send secret, saving one request res.render("monitor", { - title: "monitor assessment " + code + "/" + name, + title: "monitor evaluation " + code + "/" + name, initials: initials, courseCode: code, examName: name, diff --git a/setup/database.js b/setup/database.js index b9a392d..2070e71 100644 --- a/setup/database.js +++ b/setup/database.js @@ -6,8 +6,8 @@ // courses // unique (code,uid) // index (code,uid) -// assessments +// evaluations // unique (cid, name) // index (cid, name) -// db.assessments.createIndex( { cid: 1, name: 1 } ); +// db.evaluations.createIndex( { cid: 1, name: 1 } ); // https://docs.mongodb.com/manual/core/index-compound/ diff --git a/sockets.js b/sockets.js index 30beb32..dafe6d9 100644 --- a/sockets.js +++ b/sockets.js @@ -1,6 +1,6 @@ const message = require("./public/javascripts/utils/socketMessages"); const params = require("./config/parameters"); -const AssessmentModel = require("./models/assessment"); +const EvaluationModel = require("./models/evaluation"); const ObjectId = require("bson-objectid"); module.exports = function(io) @@ -20,10 +20,10 @@ module.exports = function(io) { const number = socket.handshake.query.number; const password = socket.handshake.query.password; - AssessmentModel.checkPassword(ObjectId(aid), number, password, (err,ret) => { + EvaluationModel.checkPassword(ObjectId(aid), number, password, (err,ret) => { if (!!err || !ret) return; //wrong password, or some unexpected error... - AssessmentModel.newConnection(ObjectId(aid), number); + EvaluationModel.newConnection(ObjectId(aid), number); socket.broadcast.to(aid + "_teacher").emit(message.studentConnect, {number: number}); socket.join(aid + "_student"); socket.on(message.newAnswer, m => { //got answer from student client @@ -42,7 +42,7 @@ module.exports = function(io) socket.broadcast.to(aid + "_teacher").emit(message.studentFullscreen, m); }); socket.on("disconnect", () => { //notify monitor + server - AssessmentModel.setDiscoTime(ObjectId(aid), number); + EvaluationModel.setDiscoTime(ObjectId(aid), number); socket.broadcast.to(aid + "_teacher").emit(message.studentDisconnect, {number: number}); }); }); diff --git a/views/course.pug b/views/course.pug index 34bf462..9d84018 100644 --- a/views/course.pug +++ b/views/course.pug @@ -7,74 +7,19 @@ block append stylesheets block content .container#course if teacher - #newAssessment.modal + #newevaluation.modal .modal-content - form(@submit.prevent="addAssessment") + form(@submit.prevent="addevaluation") .input-field - input#assessmentName(type="text" v-model="newAssessment.name" required) - label(for="assessmentName") Name + input#evaluationName(type="text" v-model="newEvaluation.name" required) + label(for="evaluationName") Name .modal-footer .center-align - a.waves-effect.waves-light.btn(href="#!" @click="addAssessment()") + a.waves-effect.waves-light.btn(href="#!" @click="addEvaluation()") span Submit i.material-icons.right send - .row(v-show="mode=='view'") - - - - - - - - - #assessmentEdit - form - p - input#active(type="checkbox" v-model="assessment.active") - label(for="active") Assessment is active - div - h4 Questions mode: - span(title="Exam mode, secured (class only): students cannot lose focus or exit fullscreen") - input#secure(name="status" type="radio" value="secure" v-model="assessment.mode") - label(for="secure") secure - span(title="Exam mode, watched (class only): teachers are notified when students lose focus or resize window") - input#watch(name="status" type="radio" value="watch" v-model="assessment.mode") - label(for="watch") watch - span(title="Exam mode, unwatched: students can browse the web freely") - input#exam(name="status" type="radio" value="exam" v-model="assessment.mode") - label(for="exam") exam - span(title="Questions list open to the world (useful mode after an exam, or for a 'questions bank'") - input#open(name="status" type="radio" value="open" v-model="assessment.mode") - label(for="open") open - p - input#fixed(type="checkbox" v-model="assessment.fixed") - label(for="fixed") Fixed questions order - div - h4 Display type: - span(title="Show only one question at a time (with potential sub-questions)") - input#displayOne(name="display" type="radio" value="one" v-model="assessment.display") - label(for="displayOne") one - span(title="Always show all questions (with an optional navigator)") - input#displayAll(name="display" type="radio" value="all" v-model="assessment.display") - label(for="displayAll") all - .input-field - input#time(type="number" v-model.number="assessment.time") - label(for="time") Time (minutes) - .input-field - textarea#introduction.materialize-textarea(v-model="assessment.introduction") - label(for="introduction") Introduction - .input-field - textarea#assessmentEdition.materialize-textarea(v-model="assessmentText") - label(for="assessmentEdition") Assessment in text format - - - - - - - - .col.s12.m10.offset-m1 - if teacher + .row + .col.s12.m10.offset-m1 h4.title(@click="toggleDisplay('students')") Students .card(v-show="display=='students'") .center-align @@ -91,11 +36,13 @@ block content td {{ student.number }} td {{ student.name }} td {{ student.group }} - h4.title(@click="toggleDisplay('assessments')") Assessments - .card(v-show="display=='assessments'") + .row + .col.s12.m10.offset-m1 + h4.title(@click="toggleDisplay('evaluations')") evaluations + .card(v-show="display=='evaluations'") if teacher .center-align - a.on-left.waves-effect.waves-light.btn.modal-trigger(href="#newAssessment") New assessment + a.on-left.waves-effect.waves-light.btn.modal-trigger(href="#newevaluation") New evaluation input#password(type="password" v-model="monitorPwd" @keyup.enter="setPassword" placeholder="Password" title="Monitoring password") table @@ -104,54 +51,74 @@ block content th Name th Mode th #Questions - th Time tbody - 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 }} - td {{ assessment.questions.reduce( (a,b) => { return b.active ? a+1 : a; }, 0) }} - td {{ assessment.time }} + tr.evaluation(v-for="(evaluation,i) in evaluationArray" :class="{idle:!evaluation.active}" + @click.left="actionevaluation(i)" @contextmenu.prevent="deleteevaluation(evaluation)") + td {{ evaluation.name }} + td {{ evaluation.mode }} + td {{ evaluation.questions.length }} if teacher - .row(v-show="mode=='edit'") + .row .col.s12.m10.offset-m1 - - - - - // TODO: always edit mode, with a modal preview - - - - h4 {{ assessment.name }} - .card - .center-align - button.waves-effect.waves-light.btn.on-left(@click="materialOpenModal('assessmentSettings')") Settings - button.waves-effect.waves-light.btn.on-left(@click="materialOpenModal('assessmentEdit')") Content - button.waves-effect.waves-light.btn(@click="redirect(assessment.name)") View - #questionList - .introduction(v-html="assessment.introduction") - .question(v-for="(question,i) in assessment.questions" :class="{questionInactive:!question.active}") - .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(time default 0 = untimed) - label Time for the question + h4 {{ evaluation.name }} + .card(v-show="mode=='view'") .center-align - button.waves-effect.waves-light.btn.on-left(@click="mode='view'") Cancel - button.waves-effect.waves-light.btn(@click="updateAssessment") Send + button.waves-effect.waves-light.btn.on-left(@click="mode='edit'") Edit + button.waves-effect.waves-light.btn(@click="redirect(evaluation.name)") View + div + .introduction(v-html="evaluation.introduction") + statements(:questions="evaluation.questions" :display="solution") + .card(v-show="mode=='edit'") + form(@submit.prevent) + p + input#active(type="checkbox" v-model="evaluation.active") + label(for="active") evaluation is active + div + h4 Questions mode: + span(title="Exam mode, secured (class only): students cannot lose focus or exit fullscreen") + input#secure(name="status" type="radio" value="secure" v-model="evaluation.mode") + label(for="secure") secure + span(title="Exam mode, watched (class only): teachers are notified when students lose focus or resize window") + input#watch(name="status" type="radio" value="watch" v-model="evaluation.mode") + label(for="watch") watch + span(title="Exam mode, unwatched: students can browse the web freely") + input#exam(name="status" type="radio" value="exam" v-model="evaluation.mode") + label(for="exam") exam + span(title="Questions list open to the world (useful mode after an exam, or for a 'questions bank'") + input#open(name="status" type="radio" value="open" v-model="evaluation.mode") + label(for="open") open + p + input#fixed(type="checkbox" v-model="evaluation.fixed") + label(for="fixed") Fixed questions order + div + h4 Display type: + span(title="Show only one question at a time (with potential sub-questions)") + input#displayOne(name="display" type="radio" value="one" v-model="evaluation.display") + label(for="displayOne") one + span(title="Always show all questions (with an optional navigator)") + input#displayAll(name="display" type="radio" value="all" v-model="evaluation.display") + label(for="displayAll") all + .input-field + input#time(type="number" v-model.number="evaluation.time") + label(for="time") Time (minutes) + .input-field + textarea#introduction.materialize-textarea(v-model="evaluation.introduction") + label(for="introduction") Introduction + .input-field + textarea#evaluationEdition.materialize-textarea(v-model="evaluationText") + label(for="evaluationEdition") evaluation in text format + .center-align + button.waves-effect.waves-light.btn.on-left(@click="updateEvaluation()") Send + button.waves-effect.waves-light.btn(@click="mode='view'") Cancel block append javascripts script(src="//cdnjs.cloudflare.com/ajax/libs/PapaParse/4.3.6/papaparse.min.js") script. - let assessmentArray = !{JSON.stringify(assessmentArray)}; + let evaluationArray = !{JSON.stringify(evaluationArray)}; const course = !{JSON.stringify(course)}; const initials = "#{initials}"; const admin = #{teacher}; script(src="/javascripts/utils/sha1.js") script(src="/javascripts/utils/validation.js") + script(src="/javascripts/components/statements.js") script(src="/javascripts/course.js") diff --git a/views/assessment.pug b/views/evaluation.pug similarity index 100% rename from views/assessment.pug rename to views/evaluation.pug diff --git a/views/grade.pug b/views/grade.pug index d38032a..2c5c03e 100644 --- a/views/grade.pug +++ b/views/grade.pug @@ -32,11 +32,11 @@ block rightMenu thead tr th Number - th(v-for="assessment in assessmentArray") {{ assessment.name }} + th(v-for="evaluation in evaluationArray") {{ evaluation.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)") + td(v-for="(evaluation,i) in evaluationArray" @click="togglePresence(student.number,i)") | {{ grade(i,student.number) }} .modal-footer .center-align @@ -59,17 +59,17 @@ block content thead tr th Name - th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }} + th(v-for="(q,i) in evaluation.questions") Q.{{ (i+1) }} tbody - tr.assessment(v-for="s in studentList(group)") + tr.evaluation(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)")   - h4.title(@click="toggleDisplay('assessment')") Assessment - div(v-show="display=='assessment'") + td(v-for="(q,i) in evaluation.questions" :style="{background-color: getColor(number,i)}" @click="seeDetails(number,i)")   + h4.title(@click="toggleDisplay('evaluation')") evaluation + div(v-show="display=='evaluation'") .card - .introduction(v-html="assessment.introduction") + .introduction(v-html="evaluation.introduction") .card - statements(:questions="assessment.questions" :answers:"answers") + statements(:questions="evaluation.questions" :answers:"answers") diff --git a/views/monitor.pug b/views/monitor.pug index 671b136..c904f00 100644 --- a/views/monitor.pug +++ b/views/monitor.pug @@ -29,17 +29,17 @@ block content thead tr th Name - th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }} + th(v-for="(q,i) in evaluation.questions") Q.{{ (i+1) }} tbody - tr.assessment(v-for="s in studentList(group)" :class="{absent:!s.present}") + tr.evaluation(v-for="s in studentList(group)" :class="{absent:!s.present}") td(:class="{blur:!!s.blur,resize:!!s.resize,disconnect:!!s.disco}" @click="togglePresence(s)") {{ s.name }} - td(v-for="(q,i) in assessment.questions" :style="{backgroundColor: getColor(s.number,i)}" @click="seeDetails(s.number,i)")   - h4.title(@click="toggleDisplay('assessment')") Assessment - div(v-show="display=='assessment'") + td(v-for="(q,i) in evaluation.questions" :style="{backgroundColor: getColor(s.number,i)}" @click="seeDetails(s.number,i)")   + h4.title(@click="toggleDisplay('evaluation')") evaluation + div(v-show="display=='evaluation'") .card - .introduction(v-html="assessment.introduction") + .introduction(v-html="evaluation.introduction") .card - statements(:questions="assessment.questions" :answers="answers") + statements(:questions="evaluation.questions" :answers="answers") block append javascripts script. -- 2.44.0