From 71d1ca9c594b64d959c608a2abbff926480abad5 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> 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<answers.length; i++) - assessment.questions[i].answer = answers[i]; + setAnswers: function(m) { + for (let i=0; i<m.answers.length; i++) + assessment.questions[i].answer = m.answers[i]; this.stage = 4; }, }, diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js index 6ebea85..5bf6bdb 100644 --- a/public/javascripts/components/statements.js +++ b/public/javascripts/components/statements.js @@ -11,7 +11,7 @@ Vue.component("statements", { // Full questions tree is rendered, but some parts hidden depending on display settings render(h) { // TODO: render nothing if answers is empty - let domTree = this.questions.map( (q,i) => { + let domTree = (this.questions || [ ]).map( (q,i) => { let questionContent = [ ]; questionContent.push( h( @@ -70,7 +70,7 @@ Vue.component("statements", { "class": { option: true, choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx), - choiceWrong: this.answers.showSolution && this.inputs[i][idx] && !q.answer.includes(idx), + choiceWrong: this.answers.showSolution && this.answers.inputs[i][idx] && !q.answer.includes(idx), }, }, option @@ -93,7 +93,7 @@ Vue.component("statements", { { "class": { "question": true, - "hide": !this.answers.displayAll && this.answers.index != i, + "hide": !this.answers.displayAll && this.answers.indices[this.answers.index] != i, }, }, questionContent @@ -106,7 +106,7 @@ Vue.component("statements", { id: "statements", }, }, - questions + domTree ); }, updated: function() { diff --git a/public/javascripts/course.js b/public/javascripts/course.js index 94e1b84..85172dc 100644 --- a/public/javascripts/course.js +++ b/public/javascripts/course.js @@ -340,8 +340,8 @@ window.onload = function() { return ""; //no grade yet return this.grades[assessmentIndex][studentNumber]; }, - groupId: function(group, hash) { - return (!!hash?"#":"") + "group" + group; + groupId: function(group, prefix) { + return (prefix || "") + "group" + group; }, togglePresence: function(number, index) { // UNIMPLEMENTED diff --git a/public/javascripts/grading.js b/public/javascripts/grading.js new file mode 100644 index 0000000..5264d83 --- /dev/null +++ b/public/javascripts/grading.js @@ -0,0 +1 @@ +//TODO: similar to monitor / course (need to factor some code) diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js index 5d5f237..6c08d7a 100644 --- a/public/javascripts/monitor.js +++ b/public/javascripts/monitor.js @@ -1,18 +1,10 @@ -// TODO: onglets pour chaque groupe + section déroulante questionnaire (chargé avec réponses) -// NOM Prenom (par grp, puis alphabétique) -// réponse : vert si OK (+ choix), rouge si faux, gris si texte (clic pour voir) -// + temps total ? -// click sur en-tête de colonne : tri alphabétique, tri décroissant... -// Affiché si (hash du) mdp du cours est correctement entré -// Doit reprendre les données en base si refresh (sinon : sockets) - let socket = null; //monitor answers in real time new Vue({ el: "#monitor", data: { password: "", //from password field - assessment: null, //obtained after authentication + assessment: { }, //obtained after authentication // Stage 0: unauthenticated (password), // 1: authenticated (password hash validated), start monitoring stage: 0, @@ -22,23 +14,75 @@ new Vue({ inputs: [ ], index : -1, }, + students: [ ], //to know their names + display: "assessment", //or student's answers }, methods: { + // TODO: redundant code, next 4 funcs already exist in course.js + toggleDisplay: function(area) { + if (this.display == area) + this.display = ""; + else + this.display = area; + }, + studentList: function(group) { + return this.students + .filter( s => { return group==0 || s.group == group; }) + .map( s => { return Object.assign({}, s); }) //not altering initial array + .sort( (a,b) => { + let res = a.name.localeCompare(b.name); + if (res == 0) + res += a.forename.localeCompare(b.forename); + return res; + }); + }, + groupList: function() { + let maxGrp = 1; + this.students.forEach( s => { + if (s.group > maxGrp) + maxGrp = s.group; + }); + return _.range(1,maxGrp+1); + }, + groupId: function(group, prefix) { + return (prefix || "") + "group" + group; + }, + 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; }); + if (paperIdx === -1) + return "grey"; //student didn't start yet + const inputIdx = this.assessment.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)) + return "green"; + return "red"; + }, + seeDetails: function(number, i) { + // UNIMPLEMENTED: see question details, with current answer(s) + }, // stage 0 --> 1 startMonitoring: function() { $.ajax("/start/monitoring", { method: "GET", data: { - password: this.password, + password: Sha1.Compute(this.password), aname: examName, - cname: courseName, + ccode: courseCode, initials: initials, }, dataType: "json", success: s => { if (!!s.errmsg) - return this.warning(s.errmsg); - this.assessment = JSON.parse(s.assessment); + return alert(s.errmsg); + this.assessment = s.assessment; + this.answers.inputs = s.assessment.questions.map( q => { return q.answer; }); + this.students = s.students; this.stage = 1; socket = io.connect("/", { query: "aid=" + this.assessment._id + "&secret=" + s.secret @@ -47,10 +91,27 @@ new Vue({ let paperIdx = this.assessment.papers.findIndex( item => { return item.number == m.number; }); - this.assessment.papers[paperIdx].inputs.push(m.input); //answer+index + if (paperIdx === -1) + { + // First answer + paperIdx = this.assessment.papers.length; + this.assessment.papers.push({ + number: m.number, + inputs: [ ], //other fields irrelevant here + }); + } + // TODO: notations not coherent (input / answer... when, which ?) + this.assessment.papers[paperIdx].inputs.push(m.answer); //input+index }); }, }); }, + endMonitoring: function() { + // In the end, send answers to students + socket.emit( + message.allAnswers, + { answers: this.assessment.questions.map( q => { return q.answer; }) } + ); + }, }, }); diff --git a/public/stylesheets/assessment.css b/public/stylesheets/assessment.css index b8a2409..2143f40 100644 --- a/public/stylesheets/assessment.css +++ b/public/stylesheets/assessment.css @@ -53,3 +53,14 @@ button#sendAnswer { .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: 3px; + border-bottom: 1px solid grey; +} diff --git a/public/stylesheets/course.css b/public/stylesheets/course.css index 2d61347..c1c4a9b 100644 --- a/public/stylesheets/course.css +++ b/public/stylesheets/course.css @@ -54,3 +54,14 @@ tr.stats { .conclusion { margin-bottom: 20px; } + +table.in-question { + border: 1px solid black; + width: auto; + margin: 10px auto; +} + +table.in-question th, table.in-question td { + padding: 3px; + border-bottom: 1px solid grey; +} diff --git a/public/stylesheets/monitor.css b/public/stylesheets/monitor.css index 8f414f5..b585800 100644 --- a/public/stylesheets/monitor.css +++ b/public/stylesheets/monitor.css @@ -1 +1,41 @@ -/* TODO */ +/* 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; +} +.conclusion { + padding: 20px 5px; +} +table.in-question { + border: 1px solid black; + width: auto; + margin: 10px auto; +} +table.in-question th, table.in-question td { + padding: 3px; + border-bottom: 1px solid grey; +} diff --git a/routes/assessments.js b/routes/assessments.js index dc749ed..a107d7e 100644 --- a/routes/assessments.js +++ b/routes/assessments.js @@ -9,8 +9,12 @@ const validator = require("../public/javascripts/utils/validation"); const ObjectId = require("bson-objectid"); const sanitizeHtml = require('sanitize-html'); const sanitizeOpts = { - allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img' ]), - allowedAttributes: { code: [ 'class' ] }, + allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img', 'u' ]), + allowedAttributes: { + img: [ 'src' ], + code: [ 'class' ], + table: [ 'class' ], + }, }; router.get("/add/assessment", access.ajax, access.logged, (req,res) => { @@ -69,6 +73,29 @@ router.get("/start/assessment", access.ajax, (req,res) => { }); }); +router.get("/start/monitoring", access.ajax, (req,res) => { + const password = req.query["password"]; + const examName = req.query["aname"]; + const courseCode = req.query["ccode"]; + const initials = req.query["initials"]; + // TODO: sanity checks + CourseModel.getByRefs(initials, courseCode, (err,course) => { + 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", () => { + res.json({ + students: course.students, + assessment: assessment, + secret: params.secret, + }); + }); + }); + }); + }); +}); + router.get("/send/answer", access.ajax, (req,res) => { let aid = req.query["aid"]; let number = req.query["number"]; diff --git a/routes/courses.js b/routes/courses.js index d221858..e07e243 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -74,4 +74,6 @@ 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 15cf1fd..f6c77a2 100644 --- a/routes/pages.js +++ b/routes/pages.js @@ -126,16 +126,17 @@ router.get("/:initials([a-z0-9]+)/:courseCode([a-z0-9._-]+)/:assessmentName([a-z }); }); -// Monitor: --> after identification (password), always send password hash with requests +// 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) => { let initials = req.params["initials"]; let code = req.params["courseCode"]; let name = req.params["assessmentName"]; + // TODO: if (main) teacher, also send secret, saving one request res.render("monitor", { title: "monitor assessment " + code + "/" + name, initials: initials, - code: code, - name: name, + courseCode: code, + examName: name, }); }); diff --git a/sockets.js b/sockets.js index eeda127..2ce8ae9 100644 --- a/sockets.js +++ b/sockets.js @@ -1,6 +1,6 @@ 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"); module.exports = function(io) @@ -10,34 +10,23 @@ module.exports = function(io) socket.join(aid); // Student or monitor connexion const isTeacher = !!socket.handshake.query.secret && socket.handshake.query.secret == params.secret; + if (isTeacher) { socket.on(message.newAnswer, m => { //got answer from student socket.emit(message.newAnswer, m); }); socket.on(message.allAnswers, m => { //send feedback to student (answers) - if (!!students[m.number]) //TODO: namespace here... room quiz - socket.broadcast.to(aid).emit(message.allAnswers, m); + socket.broadcast.to(aid).emit(message.allAnswers, m); }); } else //student { const number = socket.handshake.query.number; const password = socket.handshake.query.password; - AssessmentEntity.checkPassword(ObjectId(aid), number, password, (err,ret) => { + AssessmentModel.checkPassword(ObjectId(aid), number, password, (err,ret) => { if (!!err || !ret) return; //wrong password, or some unexpected error... - // TODO: Prevent socket connection (just ignore) if student already connected -// io.of('/').in(aid).clients((error, clients) => { -// if (error) -// throw error; -// if (clients.some( c => { return c. .. == number; })) -// // Problem: we just have a list of socket IDs (not handshakes) -// }); - // TODO: next is conditional to "student not already taking the exam" - socket.on(message.allAnswers, () => { //got all answers from teacher - socket.emit(message.allAnswers, m); - }); socket.on("disconnect", () => { //TODO: notify monitor (grey low opacity background) //Also send to server: discoTime in assessment.papers ... diff --git a/views/assessment.pug b/views/assessment.pug index b8ed733..0a7e369 100644 --- a/views/assessment.pug +++ b/views/assessment.pug @@ -39,20 +39,20 @@ block content if assessment.mode != "open" button.waves-effect.waves-light.btn.on-left(@click="cancelStudent") Cancel button.waves-effect.waves-light.btn(@click="startAssessment") Start! - #stage1_2_4(v-show="stage==1 || stage==2 || stage == 4") + #stage0_1_4(v-show="[0,1,4].includes(stage)") .card .introduction(v-html="assessment.introduction") - #stage2_4(v-show="stage==2 || stage==4") + #stage2_4(v-show="[2,4].includes(stage)") if assessment.time > 0 - .card - .timer.center(v-if="stage==2") {{ countdown }} + .card(v-if="stage==2") + .timer.center {{ countdown }} .card - button#sendAsnwer.waves-effect.waves-light.btn(@click="sendAnswer") Send + button#sendAnswer.waves-effect.waves-light.btn(@click="sendAnswer") Send statements(:questions="assessment.questions" :answers="answers") #stage3(v-show="stage==3") .card .finish Exam completed ☺ ...don't close the window! - #stage3_4(v-show="stage==3 || stage==4") + #stage3_4(v-show="[3,4].includes(stage)") .card .conclusion(v-html="assessment.conclusion") @@ -60,5 +60,7 @@ block append javascripts script. let assessment = !{JSON.stringify(assessment)}; const monitoring = false; + script(src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js") + script(src="/javascripts/utils/libsRefresh.js") script(src="/javascripts/components/statements.js") script(src="/javascripts/assessment.js") diff --git a/views/course.pug b/views/course.pug index 17bad0d..aa412e2 100644 --- a/views/course.pug +++ b/views/course.pug @@ -144,7 +144,7 @@ block content li.tab a(href="#group0") All li.tab(v-for="group in groupList()") - a(:href="groupId(group,'hash')") G.{{ group }} + a(:href="groupId(group,'#')") G.{{ group }} table.result(:id="groupId(group)" v-for="group in [0].concat(groupList())" @click="showDetails(group)") thead tr diff --git a/views/grading.pug b/views/grading.pug new file mode 100644 index 0000000..58dc5e0 --- /dev/null +++ b/views/grading.pug @@ -0,0 +1,43 @@ +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 Forename + th Name + th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }} + tbody + tr.assessment(v-for="s in studentList(group)") + td {{ s.forename }} + 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'") + .card + .introduction(v-html="assessment.introduction") + .card + statements(:questions="assessment.questions" :answers:"answers") + .card + .conclusion(v-html="assessment.conclusion") + +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/monitor.pug b/views/monitor.pug index 5a64e89..339ca47 100644 --- a/views/monitor.pug +++ b/views/monitor.pug @@ -1,15 +1,10 @@ extends withQuestions - //TODO: step 1: ask password (client side, store hash) - // step 2: when got hash, send request (with hash) to get monitoring page: - // array with results + quiz details (displayed in another tab) + init socket (with hash too) - // buttons "start quiz" and "stop quiz" for teacher only: trigger actions (impacting sockets) - - // TODO: data = papers (modified after socket messages + retrived at start) - // But get examName by server at loading +block append stylesheets + link(rel="stylesheet" href="/stylesheets/monitor.css") block content - .container#assessment + .container#monitor .row .col.s12.m10.offset-m1.l8.offset-l2.xl6.offset-xl3 h4= examName @@ -20,15 +15,43 @@ block content input#password(type="password" v-model="password" @keyup.enter="startMonitoring()") button.waves-effect.waves-light.btn(@click="startMonitoring()") Send #stage1(v-show="stage==1") - .card - .introduction(v-html="assessment.introduction") - .card - statements(:questions="assessment.questions" :answers:"answers") - .card - .conclusion(v-html="assessment.conclusion") + button.waves-effect.waves-light.btn(@click="endMonitoring()") Send feedback + h4.title(@click="toggleDisplay('answers')") Anwers + // TODO: aussi afficher stats, permettre tri par colonnes + .card(v-show="display=='answers'") + 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 Forename + th Name + th(v-for="(q,i) in assessment.questions") Q.{{ (i+1) }} + tbody + tr.assessment(v-for="s in studentList(group)") + td {{ s.forename }} + td {{ 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'") + .card + .introduction(v-html="assessment.introduction") + .card + statements(:questions="assessment.questions" :answers="answers") + .card + .conclusion(v-html="assessment.conclusion") block append javascripts script. + const examName = "#{examName}"; + const courseCode = "#{courseCode}"; + const initials = "#{initials}"; const monitoring = true; + script(src="/javascripts/utils/libsRefresh.js") script(src="/javascripts/components/statements.js") + script(src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js") + script(src="/javascripts/utils/sha1.js") script(src="/javascripts/monitor.js") -- 2.44.0