From 73609d3bc662cf4c8a21746c5d1ad736ea0eecbd Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 14 Feb 2018 12:49:30 +0100 Subject: [PATCH] 'update' --- TODO | 2 + models/assessment.js | 1 + public/javascripts/assessment.js | 14 +- public/javascripts/components/statements.js | 208 +++++++++++--------- public/javascripts/course.js | 20 +- public/javascripts/courseList.js | 8 +- public/javascripts/login.js | 7 +- public/javascripts/monitor.js | 2 +- public/stylesheets/statements.css | 8 + routes/assessments.js | 34 ++-- routes/courses.js | 20 +- routes/users.js | 27 ++- 12 files changed, 196 insertions(+), 155 deletions(-) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..0d7d98e --- /dev/null +++ b/TODO @@ -0,0 +1,2 @@ +views: course, monitor, grade +js: monitor (see details), assessment, course, grade diff --git a/models/assessment.js b/models/assessment.js index fd0133c..de3d2d7 100644 --- a/models/assessment.js +++ b/models/assessment.js @@ -26,6 +26,7 @@ const AssessmentModel = * answer: array of integers (for quiz) or html text (for paper); striped in exam mode * active: boolean, is question in current assessment? * points: points for this question (default 1) + * param: parameter (if applicable) * papers : array of * number: student number * inputs: array of {index,answer[array of integers or html text],startTime} diff --git a/public/javascripts/assessment.js b/public/javascripts/assessment.js index 9275879..2a451e4 100644 --- a/public/javascripts/assessment.js +++ b/public/javascripts/assessment.js @@ -90,7 +90,7 @@ new Vue({ }, // stage 0 --> 1 getStudent: function() { - $.ajax("/get/student", { + $.ajax("/courses/student", { method: "GET", data: { number: this.student.number, @@ -163,8 +163,8 @@ new Vue({ }; if (assessment.mode == "open") return initializeStage2(); - $.ajax("/start/assessment", { - method: "GET", + $.ajax("/assessments/start", { + method: "PUT", data: { number: this.student.number, aid: assessment._id @@ -251,8 +251,8 @@ new Vue({ number: this.student.number, password: this.student.password, }; - $.ajax("/send/answer", { - method: "GET", + $.ajax("/assessments/answer", { + method: "PUT", data: answerData, dataType: "json", success: ret => { @@ -282,8 +282,8 @@ new Vue({ this.answers.displayAll = true; return; } - $.ajax("/end/assessment", { - method: "GET", + $.ajax("/assessments/end", { + method: "PUT", data: { aid: assessment._id, number: this.student.number, diff --git a/public/javascripts/components/statements.js b/public/javascripts/components/statements.js index 900a5e7..64057f9 100644 --- a/public/javascripts/components/statements.js +++ b/public/javascripts/components/statements.js @@ -14,10 +14,14 @@ Imaginary example: (using math.js)
Calculer le déterminant de $$\begin{matrix}7 & x\\y & -3\end{matrix}$$
* ... + +--> input of type text (number, or vector, or matrix e.g. in R syntax) +--> parameter stored in question.param (TODO) + */ Vue.component("statements", { - // 'inputs': array of index (as in questions) + input (text or array of ints) + // 'inputs': object with key = question index and value = text or boolean array // display: 'all', 'one', 'solution' // iidx: current level-0 integer index (can match a group of questions / inputs) props: ['questions','inputs','display','iidx'], @@ -28,113 +32,139 @@ Vue.component("statements", { } // Full questions tree is rendered, but some parts hidden depending on display settings render(h) { - let domTree = (this.questions || [ ]).map( (q,i) => { - let questionContent = [ ]; - questionContent.push( - h( - "h4", - { - "class": { - "questionIndex": true, - } - }, - q.index - ) - ); - questionContent.push( - h( - "div", - { - "class": { - wording: true, - - }, - domProps: { - innerHTML: q.wording, + // Prepare questions groups, ordered + let questions = this.questions || [ ] + let questionGroups = _.groupBy(questions, q => { + const dotPos = q.index.indexOf("."); + return dotPos === -1 ? q.index : q.index.substring(0,dotPos); + }); + let domTree = questionGroups.map( (qg,i) => { + // Re-order questions 1.1.1 then 1.1.2 then... + const orderedQg = qg.sort( (a,b) => { + let aParts = a.split('.').map(Number); + let bParts = b.split('.').map(Number); + const La = aParts.length, Lb = bParts.length; + for (let i=0; i { + let questionContent = [ ]; + questionContent.push( + h( + "h4", + { + "class": { + "questionIndex": true, + } }, - } - ) - ); - if (!!q.options) - { - // quiz-like question - let optionsOrder = _.range(q.options.length); - if (!q.fixed) - optionsOrder = _.shuffle(optionsOrder); - let optionList = [ ]; - optionsOrder.forEach( idx => { - let option = [ ]; - option.push( - h( - "input", - { - domProps: { - checked: this.answers.inputs.length > 0 && this.answers.inputs[i][idx], - disabled: monitoring, - }, - attrs: { - id: this.inputId(i,idx), - type: "checkbox", - }, - on: { - change: e => { this.answers.inputs[i][idx] = e.target.checked; }, - }, + q.index + ) + ); + questionContent.push( + h( + "div", + { + "class": { + wording: true, }, - [ '' ] //to work in Firefox 45.9 ESR @ ENSTA... - ) - ); - option.push( - h( - "label", - { - domProps: { - innerHTML: q.options[idx], + domProps: { + innerHTML: q.wording, + }, + } + ) + ); + if (!!q.options) + { + // quiz-like question + let optionsOrder = _.range(q.options.length); + if (!q.fixed) + optionsOrder = _.shuffle(optionsOrder); + let optionList = [ ]; + optionsOrder.forEach( idx => { + let option = [ ]; + option.push( + h( + "input", + { + domProps: { + checked: this.inputs[q.index][idx], + disabled: monitoring, + }, + attrs: { + id: this.inputId(q.index,idx), + type: "checkbox", + }, + on: { + change: e => { this.inputs[q.index][idx] = e.target.checked; }, + }, }, - attrs: { - "for": this.inputId(i,idx), + [ '' ] //to work in Firefox 45.9 ESR @ ENSTA... + ) + ); + option.push( + h( + "label", + { + domProps: { + innerHTML: q.options[idx], + }, + attrs: { + "for": this.inputId(q.index,idx), + }, + } + ) + ); + optionList.push( + h( + "div", + { + "class": { + option: true, + choiceCorrect: this.display == "solution" && q.answer.includes(idx), + choiceWrong: this.display == "solution" && this.inputs[q.index][idx] && !q.answer.includes(idx), + }, }, - } - ) - ); - optionList.push( + option + ) + ); + }); + questionContent.push( h( "div", { "class": { - option: true, - choiceCorrect: this.answers.showSolution && this.questions[i].answer.includes(idx), - choiceWrong: this.answers.showSolution && this.answers.inputs[i][idx] && !q.answer.includes(idx), + optionList: true, }, }, - option + optionList ) ); - }); - questionContent.push( - h( - "div", - { - "class": { - optionList: true, - }, + } + const depth = (q.index.match(/\./g) || []).length; + return h( + "div", + { + "class": { + "question": true, + "depth" + depth: true, }, - optionList - ) + }, + questionContent ); - } - if (this.display == "all" && !this.navigator && i < this.questions.length-1) - questionContent.push( h("hr") ); - const depth = (q.index.match(/\./g) || []).length; + }); return h( "div", { "class": { - "question": true, + "questionGroup": true, "hide": this.display == "one" && this.iidx != i, - "depth" + depth: true, }, - }, - questionContent + } + qgDom ); }); const navigator = h( @@ -158,7 +188,7 @@ Vue.component("statements", { }, }, [ h("span", { "class": { "material-icon": true } }, "fast_rewind") ] - ), //onclick: index = max(0,index-1) + ), h("span",{ },(this.iidx+1).toString()), h( "button", diff --git a/public/javascripts/course.js b/public/javascripts/course.js index 0e6a169..ab17fe3 100644 --- a/public/javascripts/course.js +++ b/public/javascripts/course.js @@ -91,8 +91,8 @@ new Vue({ d.number = d.number.toString(); students.push(d); }); - $.ajax("/import/students", { - method: "POST", + $.ajax("/courses/student-list", { + method: "PUT", data: { cid: this.course._id, students: JSON.stringify(students), @@ -118,9 +118,9 @@ new Vue({ return alert(error); else $('#newAssessment').modal('close'); - $.ajax("/add/assessment", + $.ajax("/assessments", { - method: "GET", + method: "POST", data: { name: this.newAssessment.name, cid: course._id, @@ -143,8 +143,8 @@ new Vue({ Materialize.updateTextFields(); //textareas, time field... }, updateAssessment: function() { - $.ajax("/update/assessment", { - method: "POST", + $.ajax("/assessments", { + method: "PUT", data: {assessment: JSON.stringify(this.assessment)}, dataType: "json", success: res => { @@ -163,9 +163,9 @@ new Vue({ return; if (confirm("Delete assessment '" + assessment.name + "' ?")) { - $.ajax("/remove/assessment", + $.ajax("/assessments", { - method: "GET", + method: "DELETE", data: { qid: this.assessment._id }, dataType: "json", success: res => { @@ -267,9 +267,9 @@ new Vue({ let error = Validator.checkObject({password:hashPwd}, "Course"); if (error.length > 0) return alert(error); - $.ajax("/set/password", + $.ajax("/courses/password", { - method: "GET", + method: "PUT", data: { cid: this.course._id, pwd: hashPwd, diff --git a/public/javascripts/courseList.js b/public/javascripts/courseList.js index 020af55..d90e201 100644 --- a/public/javascripts/courseList.js +++ b/public/javascripts/courseList.js @@ -23,9 +23,9 @@ new Vue({ return alert(error); else $('#newCourse').modal('close'); - $.ajax("/add/course", + $.ajax("/courses", { - method: "GET", + method: "POST", data: this.newCourse, dataType: "json", success: res => { @@ -45,9 +45,9 @@ new Vue({ if (!admin) return; if (confirm("Delete course '" + course.code + "' ?")) - $.ajax("/remove/course", + $.ajax("/courses", { - method: "GET", + method: "DELETE", data: { cid: course._id }, dataType: "json", success: res => { diff --git a/public/javascripts/login.js b/public/javascripts/login.js index 8752ba8..d5cd3ac 100644 --- a/public/javascripts/login.js +++ b/public/javascripts/login.js @@ -8,6 +8,11 @@ const ajaxUrl = { "register": "/register", }; +const ajaxMethod = { + "login": "PUT", + "register": "POST", +}; + const infos = { "login": "Connection token sent. Check your emails!", "register": "Registration complete! Please check your emails.", @@ -58,7 +63,7 @@ new Vue({ showMsg($dialog, "process", "Processing... Please wait"); $.ajax(ajaxUrl[this.stage], { - method: "GET", + method: ajaxMethod[this.stage], data: { email: encodeURIComponent(this.user.email), diff --git a/public/javascripts/monitor.js b/public/javascripts/monitor.js index 6ee6a41..29ce858 100644 --- a/public/javascripts/monitor.js +++ b/public/javascripts/monitor.js @@ -82,7 +82,7 @@ new Vue({ }, // stage 0 --> 1 startMonitoring: function() { - $.ajax("/start/monitoring", { + $.ajax("/assessments/monitor", { method: "GET", data: { password: Sha1.Compute(this.password), diff --git a/public/stylesheets/statements.css b/public/stylesheets/statements.css index 169e67d..5ff0113 100644 --- a/public/stylesheets/statements.css +++ b/public/stylesheets/statements.css @@ -3,11 +3,19 @@ button.sendAnswer { margin: 0 auto; } +.questionGroup { + /* TODO */ +} + .question { margin: 20px 5px; padding: 15px 0; } +.question:not(:last-child) { + border-bottom: 1px solid black; +} + .question label { color: black; } diff --git a/routes/assessments.js b/routes/assessments.js index d9f83ea..03e483e 100644 --- a/routes/assessments.js +++ b/routes/assessments.js @@ -17,9 +17,9 @@ const sanitizeOpts = { }, }; -router.get("/add/assessment", access.ajax, access.logged, (req,res) => { - const name = req.query["name"]; - const cid = req.query["cid"]; +router.post("/assessments", access.ajax, access.logged, (req,res) => { + const name = req.body["name"]; + const cid = req.body["cid"]; let error = validator({cid:cid, name:name}, "Assessment"); if (error.length > 0) return res.json({errmsg:error}); @@ -30,7 +30,7 @@ router.get("/add/assessment", access.ajax, access.logged, (req,res) => { }); }); -router.post("/update/assessment", access.ajax, access.logged, (req,res) => { +router.put("/assessments", access.ajax, access.logged, (req,res) => { const assessment = JSON.parse(req.body["assessment"]); let error = validator(assessment, "Assessment"); if (error.length > 0) @@ -50,9 +50,9 @@ router.post("/update/assessment", access.ajax, access.logged, (req,res) => { }); // Generate and set student password, return it -router.get("/start/assessment", access.ajax, (req,res) => { - let number = req.query["number"]; - let aid = req.query["aid"]; +router.put("/assessments/start", access.ajax, (req,res) => { + let number = req.body["number"]; + let aid = req.body["aid"]; let password = req.cookies["password"]; //potentially from cookies, resuming let error = validator({ _id:aid, papers:[{number:number,password:password || "samplePwd"}] }, "Assessment"); if (error.length > 0) @@ -72,7 +72,7 @@ router.get("/start/assessment", access.ajax, (req,res) => { }); }); -router.get("/start/monitoring", access.ajax, (req,res) => { +router.get("/assessments/monitor", access.ajax, (req,res) => { const password = req.query["password"]; const examName = req.query["aname"]; const courseCode = req.query["ccode"]; @@ -95,11 +95,11 @@ router.get("/start/monitoring", access.ajax, (req,res) => { }); }); -router.get("/send/answer", access.ajax, (req,res) => { - let aid = req.query["aid"]; - let number = req.query["number"]; - let password = req.query["password"]; - let input = JSON.parse(req.query["answer"]); +router.put("/assessments/answer", access.ajax, (req,res) => { + let aid = req.body["aid"]; + 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"); if (error.length > 0) return res.json({errmsg:error}); @@ -110,10 +110,10 @@ router.get("/send/answer", access.ajax, (req,res) => { }); }); -router.get("/end/assessment", access.ajax, (req,res) => { - let aid = req.query["aid"]; - let number = req.query["number"]; - let password = req.query["password"]; +router.put("/assessments/end", access.ajax, (req,res) => { + let aid = req.body["aid"]; + let number = req.body["number"]; + let password = req.body["password"]; let error = validator({ _id:aid, papers:[{number:number,password:password}] }, "Assessment"); if (error.length > 0) return res.json({errmsg:error}); diff --git a/routes/courses.js b/routes/courses.js index 89b38b3..5ca6ad4 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -5,9 +5,9 @@ const sanitizeHtml = require('sanitize-html'); const ObjectId = require("bson-objectid"); const CourseModel = require("../models/course"); -router.get('/add/course', access.ajax, access.logged, (req,res) => { - let code = req.query["code"]; - let description = sanitizeHtml(req.query["description"]); +router.post('/courses', access.ajax, access.logged, (req,res) => { + let code = req.body["code"]; + let description = sanitizeHtml(req.body["description"]); let error = validator({code:code}, "Course"); if (error.length > 0) return res.json({errmsg:error}); @@ -18,9 +18,9 @@ router.get('/add/course', access.ajax, access.logged, (req,res) => { }); }); -router.get("/set/password", access.ajax, access.logged, (req,res) => { - let cid = req.query["cid"]; - let pwd = req.query["pwd"]; +router.put("/courses/password", access.ajax, access.logged, (req,res) => { + let cid = req.body["cid"]; + let pwd = req.body["pwd"]; let error = validator({password:pwd, _id:cid}, "Course"); if (error.length > 0) return res.json({errmsg:error}); @@ -31,7 +31,7 @@ router.get("/set/password", access.ajax, access.logged, (req,res) => { }); }); -router.post('/import/students', access.ajax, access.logged, (req,res) => { +router.put('/courses/student-list', access.ajax, access.logged, (req,res) => { let cid = req.body["cid"]; let students = JSON.parse(req.body["students"]); let error = validator({_id:cid, students: students}, "Course"); @@ -48,9 +48,9 @@ router.post('/import/students', access.ajax, access.logged, (req,res) => { }); }); -router.get('/get/student', access.ajax, (req,res) => { - let number = req.query["number"]; +router.get('/courses/student', access.ajax, (req,res) => { let cid = req.query["cid"]; + let number = req.query["number"]; let error = validator({ _id: cid, students: [{number:number}] }, "Course"); if (error.length > 0) return res.json({errmsg:error}); @@ -61,7 +61,7 @@ router.get('/get/student', access.ajax, (req,res) => { }); }); -router.get('/remove/course', access.ajax, access.logged, (req,res) => { +router.delete('/courses', access.ajax, access.logged, (req,res) => { let cid = req.query["cid"]; let error = validator({_id:cid}, "Course"); if (error.length > 0) diff --git a/routes/users.js b/routes/users.js index 5dab77e..993c15e 100644 --- a/routes/users.js +++ b/routes/users.js @@ -7,7 +7,7 @@ const access = require("../utils/access"); const params = require("../config/parameters"); // to: object user -function sendLoginToken(subject, to, res) +function setAndSendLoginToken(subject, to, res) { // Set login token and send welcome(back) email with auth link let token = TokenGen.generate(params.token.length); @@ -28,12 +28,10 @@ function sendLoginToken(subject, to, res) }); } -router.get('/register', access.ajax, access.unlogged, (req,res) => { - let email = decodeURIComponent(req.query.email); - let name = decodeURIComponent(req.query.name); +router.post('/register', access.ajax, access.unlogged, (req,res) => { const newUser = { - email: email, - name: name, + email: req.body.email, + name: req.body.name, }; let error = validator(newUser, "User"); if (error.length > 0) @@ -45,7 +43,7 @@ router.get('/register', access.ajax, access.unlogged, (req,res) => { UserModel.create(newUser, (err,user) => { access.checkRequest(res, err, user, "Registration failed", () => { user.ip = req.ip; - sendLoginToken("Welcome to " + params.siteURL, user, res); + setAndSendLoginToken("Welcome to " + params.siteURL, user, res); }); }); }); @@ -53,25 +51,22 @@ router.get('/register', access.ajax, access.unlogged, (req,res) => { }); // Login: -router.get('/sendtoken', access.ajax, access.unlogged, (req,res) => { - let email = decodeURIComponent(req.query.email); +router.put('/sendtoken', access.ajax, access.unlogged, (req,res) => { + const email = req.body.email; let error = validator({email:email}, "User"); if (error.length > 0) return res.json({errmsg:error}); UserModel.getByEmail(email, (err,user) => { access.checkRequest(res, err, user, "Unknown user", () => { user.ip = req.ip; - sendLoginToken("Token for " + params.siteURL, user, res); + setAndSendLoginToken("Token for " + params.siteURL, user, res); }); }); }); // Authentication process, optionally with email changing: -router.get('/authenticate', access.unlogged, (req,res) => { - let loginToken = req.query.token; - let error = validator({token:loginToken}, "User"); - if (error.length > 0) - return res.json({errmsg:error}); +router.put('/authenticate/:token([a-z0-9]+)', access.unlogged, (req,res) => { + const loginToken = req.params.token; UserModel.getByLoginToken(loginToken, (err,user) => { access.checkRequest(res, err, user, "Invalid token", () => { if (user.loginToken.ip != req.ip) @@ -101,7 +96,7 @@ router.get('/authenticate', access.unlogged, (req,res) => { }); }); -router.get('/logout', access.logged, (req,res) => { +router.put('/logout', access.logged, (req,res) => { UserModel.removeToken(req.user._id, req.cookies.token, (err,ret) => { access.checkRequest(res, err, ret, "Logout failed", () => { res.clearCookie("initials"); -- 2.44.0