refactoring, better README (breaking commit...)
[qomet.git] / models / assessment.js
index d0e0d0b..c7cb0fd 100644 (file)
-const AssessmentEntity = require("../entities/assessment");
-const CourseEntity = require("../entities/course");
+const CourseModel = require("../models/course");
+const UserModel = require("../models/user");
 const ObjectId = require("bson-objectid");
-const UserEntity = require("../entities/user");
 const TokenGen = require("../utils/tokenGenerator");
+const db = require("../utils/database");
 
 const AssessmentModel =
 {
+       /*
+        * Structure:
+        *   _id: BSON id
+        *   cid: course ID
+        *   name: varchar
+        *   active: boolean true/false
+        *   mode: secure | exam | open (decreasing security)
+        *   fixed: bool (questions in fixed order; default: false)
+        *   display: "one" or "all" (generally "all" for open questions, but...)
+        *   time: 0, //<=0 means "untimed"; otherwise, time in seconds
+        *   introduction: "",
+        *   coefficient: number, default 1
+        *   questions: array of
+        *     index: for paper test, like 2.1.a (?!); and quiz: 0, 1, 2, 3...
+        *     wording: varchar (HTML)
+        *     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?
+        *     points: points for this question (default 1)
+        *     time: 0 (<=0: untimed)
+        *   papers : array of
+        *     number: student number
+        *     inputs: array of {index,answer[array of integers or html text],startTime}
+        *     current: index of current question (if relevant: display="one")
+        *     startTime
+        *     discoTime, totalDisco: last disconnect timestamp (if relevant) + total time
+        *     discoCount: number of disconnections
+        *     password: random string identifying student for exam session TEMPORARY
+        */
+
+       //////////////////
+       // BASIC FUNCTIONS
+
+       getById: function(aid, callback)
+       {
+               db.assessments.findOne(
+                       { _id: aid },
+                       callback
+               );
+       },
+
+       getByPath: function(cid, name, callback)
+       {
+               db.assessments.findOne(
+                       {
+                               cid: cid,
+                               name: name,
+                       },
+                       callback
+               );
+       },
+
+       insert: function(cid, name, callback)
+       {
+               db.assessments.insert(
+                       {
+                               name: name,
+                               cid: cid,
+                               active: false,
+                               mode: "exam",
+                               fixed: false,
+                               display: "one",
+                               time: 0,
+                               introduction: "",
+                               coefficient: 1,
+                               questions: [ ],
+                               papers: [ ],
+                       },
+                       callback
+               );
+       },
+
+       getByCourse: function(cid, callback)
+       {
+               db.assessments.find(
+                       { cid: cid },
+                       callback
+               );
+       },
+
+       // arg: full assessment without _id field
+       replace: function(aid, assessment, cb)
+       {
+               // Should be: (but unsupported by mongojs)
+//             db.assessments.replaceOne(
+//                     { _id: aid },
+//                     assessment,
+//                     cb
+//             );
+               // Temporary workaround:
+               db.assessments.update(
+                       { _id: aid },
+                       { $set: assessment },
+                       cb
+               );
+       },
+
+       getQuestions: function(aid, callback)
+       {
+               db.assessments.findOne(
+                       {
+                               _id: aid,
+                               display: "all",
+                       },
+                       { questions: 1},
+                       (err,res) => {
+                               callback(err, !!res ? res.questions : null);
+                       }
+               );
+       },
+
+       getQuestion: function(aid, index, callback)
+       {
+               db.assessments.findOne(
+                       {
+                               _id: aid,
+                               display: "one",
+                       },
+                       { questions: 1},
+                       (err,res) => {
+                               if (!!err || !res)
+                                       return callback(err, res);
+                               const qIdx = res.questions.findIndex( item => { return item.index == index; });
+                               if (qIdx === -1)
+                                       return callback({errmsg: "Question not found"}, null);
+                               callback(null, res.questions[qIdx]);
+                       }
+               );
+       },
+
+       getPaperByNumber: function(aid, number, callback)
+       {
+               db.assessments.findOne(
+                       {
+                               _id: aid,
+                               "papers.number": number,
+                       },
+                       (err,a) => {
+                               if (!!err || !a)
+                                       return callback(err,a);
+                               for (let p of a.papers)
+                               {
+                                       if (p.number == number)
+                                               return callback(null,p); //reached for sure
+                               }
+                       }
+               );
+       },
+
+       // 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)
+       {
+               db.assessments.update(
+                       {
+                               _id: aid,
+                               "papers.number": number,
+                       },
+                       { $inc: {
+                               "papers.$.discoCount": 1,
+                               "papers.$.totalDisco": deltaTime,
+                       } },
+                       { $set: { "papers.$.discoTime": null } }
+               );
+       },
+
+       setDiscoTime: function(aid, number)
+       {
+               db.assessments.update(
+                       {
+                               _id: aid,
+                               "papers.number": number,
+                       },
+                       { $set: { "papers.$.discoTime": Date.now() } }
+               );
+       },
+
+       getDiscoTime: function(aid, number, cb)
+       {
+               db.assessments.findOne(
+                       { _id: aid },
+                       (err,a) => {
+                               if (!!err)
+                                       return cb(err, null);
+                               const idx = a.papers.findIndex( item => { return item.number == number; });
+                               cb(null, a.papers[idx].discoTime);
+                       }
+               );
+       },
+
+       hasInput: function(aid, number, password, idx, cb)
+       {
+               db.assessments.findOne(
+                       {
+                               _id: aid,
+                               "papers.number": number,
+                               "papers.password": password,
+                       },
+                       (err,a) => {
+                               if (!!err || !a)
+                                       return cb(err,a);
+                               let papIdx = a.papers.findIndex( item => { return item.number == number; });
+                               for (let i of a.papers[papIdx].inputs)
+                               {
+                                       if (i.index == idx)
+                                               return cb(null,true);
+                               }
+                               cb(null,false);
+                       }
+               );
+       },
+
+       // https://stackoverflow.com/questions/27874469/mongodb-push-in-nested-array
+       setInput: function(aid, number, password, input, callback) //input: index + arrayOfInt (or txt)
+       {
+               db.assessments.update(
+                       {
+                               _id: aid,
+                               "papers.number": number,
+                               "papers.password": password,
+                       },
+                       { $push: { "papers.$.inputs": input } },
+                       callback
+               );
+       },
+
+       endAssessment: function(aid, number, password, callback)
+       {
+               db.assessments.update(
+                       {
+                               _id: aid,
+                               "papers.number": number,
+                               "papers.password": password,
+                       },
+                       { $set: {
+                               "papers.$.endTime": Date.now(),
+                               "papers.$.password": "",
+                       } },
+                       callback
+               );
+       },
+
+       remove: function(aid, cb)
+       {
+               db.assessments.remove(
+                       { _id: aid },
+                       cb
+               );
+       },
+
+       removeGroup: function(cid, cb)
+       {
+               db.assessments.remove(
+                       { cid: cid },
+                       cb
+               );
+       },
+
+       /////////////////////
+       // ADVANCED FUNCTIONS
+
        getByRefs: function(initials, code, name, cb)
        {
-               UserEntity.getByInitials(initials, (err,user) => {
+               UserModel.getByInitials(initials, (err,user) => {
                        if (!!err || !user)
                                return cb(err || {errmsg: "User not found"});
-                       CourseEntity.getByPath(user._id, code, (err2,course) => {
+                       CourseModel.getByPath(user._id, code, (err2,course) => {
                                if (!!err2 || !course)
                                        return cb(err2 || {errmsg: "Course not found"});
-                               AssessmentEntity.getByPath(course._id, name, (err3,assessment) => {
+                               AssessmentModel.getByPath(course._id, name, (err3,assessment) => {
                                        if (!!err3 || !assessment)
                                                return cb(err3 || {errmsg: "Assessment not found"});
                                        cb(null,assessment);
@@ -25,7 +288,7 @@ const AssessmentModel =
 
        checkPassword: function(aid, number, password, cb)
        {
-               AssessmentEntity.getById(aid, (err,assessment) => {
+               AssessmentModel.getById(aid, (err,assessment) => {
                        if (!!err || !assessment)
                                return cb(err, assessment);
                        const paperIdx = assessment.papers.findIndex( item => { return item.number == number; });
@@ -38,13 +301,13 @@ const AssessmentModel =
        add: function(uid, cid, name, cb)
        {
                // 1) Check that course is owned by user of ID uid
-               CourseEntity.getById(cid, (err,course) => {
+               CourseModel.getById(cid, (err,course) => {
                        if (!!err || !course)
                                return cb({errmsg: "Course retrieval failure"});
                        if (!course.uid.equals(uid))
                                return cb({errmsg:"Not your course"},undefined);
                        // 2) Insert new blank assessment
-                       AssessmentEntity.insert(cid, name, cb);
+                       AssessmentModel.insert(cid, name, cb);
                });
        },
 
@@ -52,10 +315,10 @@ const AssessmentModel =
        {
                const aid = ObjectId(assessment._id);
                // 1) Check that assessment is owned by user of ID uid
-               AssessmentEntity.getById(aid, (err,assessmentOld) => {
+               AssessmentModel.getById(aid, (err,assessmentOld) => {
                        if (!!err || !assessmentOld)
                                return cb({errmsg: "Assessment retrieval failure"});
-                       CourseEntity.getById(ObjectId(assessmentOld.cid), (err2,course) => {
+                       CourseModel.getById(ObjectId(assessmentOld.cid), (err2,course) => {
                                if (!!err2 || !course)
                                        return cb({errmsg: "Course retrieval failure"});
                                if (!course.uid.equals(uid))
@@ -63,7 +326,7 @@ const AssessmentModel =
                                // 2) Replace assessment
                                delete assessment["_id"];
                                assessment.cid = ObjectId(assessment.cid);
-                               AssessmentEntity.replace(aid, assessment, cb);
+                               AssessmentModel.replace(aid, assessment, cb);
                        });
                });
        },
@@ -71,7 +334,7 @@ const AssessmentModel =
        // Set password in responses collection
        startSession: function(aid, number, password, cb)
        {
-               AssessmentEntity.getPaperByNumber(aid, number, (err,paper) => {
+               AssessmentModel.getPaperByNumber(aid, number, (err,paper) => {
                        if (!!err)
                                return cb(err,null);
                        if (!paper && !!password)
@@ -83,18 +346,26 @@ const AssessmentModel =
                                if (paper.password != password)
                                        return cb({errmsg: "Wrong password"});
                        }
-                       AssessmentEntity.getQuestions(aid, (err,questions) => {
-                               if (!!err)
-                                       return cb(err,null);
+                       AssessmentModel.getQuestions(aid, (err2,questions) => {
+                               if (!!err2)
+                                       return cb(err2,null);
                                if (!!paper)
-                                       return cb(null,{paper:paper,questions:questions});
+                                       return cb(null,{paper:paper});
                                const pwd = TokenGen.generate(12); //arbitrary number, 12 seems enough...
-                               AssessmentEntity.startSession(aid, number, pwd, (err2,ret) => {
-                                       cb(err2, {
-                                               questions: questions,
-                                               password: pwd,
-                                       });
-                               });
+                               db.assessments.update(
+                                       { _id: aid },
+                                       { $push: { papers: {
+                                               number: number,
+                                               startTime: Date.now(),
+                                               endTime: undefined,
+                                               password: password,
+                                               totalDisco: 0,
+                                               discoCount: 0,
+                                               inputs: [ ], //TODO: this is stage 1, stack indexed answers.
+                                               // then build JSON tree for easier access / correct
+                                       }}},
+                                       (err3,ret) => { cb(err3,{password:password}); }
+                               );
                        });
                });
        },
@@ -102,12 +373,12 @@ const AssessmentModel =
        newAnswer: function(aid, number, password, input, cb)
        {
                // Check that student hasn't already answered
-               AssessmentEntity.hasInput(aid, number, password, input.index, (err,ret) => {
+               AssessmentModel.hasInput(aid, number, password, input.index, (err,ret) => {
                        if (!!err)
                                return cb(err,null);
                        if (!!ret)
                                return cb({errmsg:"Question already answered"},null);
-                       AssessmentEntity.setInput(aid, number, password, input, (err2,ret2) => {
+                       AssessmentModel.setInput(aid, number, password, input, (err2,ret2) => {
                                if (!!err2 || !ret2)
                                        return cb(err2,ret2);
                                return cb(null,ret2);
@@ -115,17 +386,16 @@ const AssessmentModel =
                });
        },
 
-       // NOTE: no callbacks for 2 next functions, failures are not so important
+       // NOTE: no callbacks for next function, failures are not so important
        // (because monitored: teachers can see what's going on)
-
        newConnection: function(aid, number)
        {
                //increment discoCount, reset discoTime to NULL, update totalDisco
-               AssessmentEntity.getDiscoTime(aid, number, (err,discoTime) => {
+               AssessmentModel.getDiscoTime(aid, number, (err,discoTime) => {
                        if (!!discoTime)
-                               AssessmentEntity.addDisco(aid, number, Date.now() - discoTime);
+                               AssessmentModel.addDisco(aid, number, Date.now() - discoTime);
                });
        },
-};
+}
 
 module.exports = AssessmentModel;