'update'
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 14 Feb 2018 11:49:30 +0000 (12:49 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 14 Feb 2018 11:49:30 +0000 (12:49 +0100)
12 files changed:
TODO [new file with mode: 0644]
models/assessment.js
public/javascripts/assessment.js
public/javascripts/components/statements.js
public/javascripts/course.js
public/javascripts/courseList.js
public/javascripts/login.js
public/javascripts/monitor.js
public/stylesheets/statements.css
routes/assessments.js
routes/courses.js
routes/users.js

diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..0d7d98e
--- /dev/null
+++ b/TODO
@@ -0,0 +1,2 @@
+views: course, monitor, grade
+js: monitor (see details), assessment, course, grade
index fd0133c..de3d2d7 100644 (file)
@@ -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}
index 9275879..2a451e4 100644 (file)
@@ -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,
index 900a5e7..64057f9 100644 (file)
@@ -14,10 +14,14 @@ Imaginary example: (using math.js)
        <div>Calculer le déterminant de 
        $$\begin{matrix}7 & x\\y & -3\end{matrix}$$</div>
        * ...
+
+--> 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<Math.min(La,Lb); i++)
+                               {
+                                       if (aParts[i] != bParts[i])
+                                               return aParts[i] - bParts[i];
+                               }
+                               return La - Lb; //the longer should appear after
+                       });
+                       let qgDom = orderedQg.map( q => {
+                               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",
index 0e6a169..ab17fe3 100644 (file)
@@ -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,
index 020af55..d90e201 100644 (file)
@@ -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 => {
index 8752ba8..d5cd3ac 100644 (file)
@@ -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),
index 6ee6a41..29ce858 100644 (file)
@@ -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),
index 169e67d..5ff0113 100644 (file)
@@ -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;
 }
index d9f83ea..03e483e 100644 (file)
@@ -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});
index 89b38b3..5ca6ad4 100644 (file)
@@ -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)
index 5dab77e..993c15e 100644 (file)
@@ -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");