-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);
});
},
+ checkPassword: function(aid, number, password, cb)
+ {
+ AssessmentModel.getById(aid, (err,assessment) => {
+ if (!!err || !assessment)
+ return cb(err, assessment);
+ const paperIdx = assessment.papers.findIndex( item => { return item.number == number; });
+ 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
- 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);
});
},
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) => {
+ 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))
// 2) Replace assessment
delete assessment["_id"];
assessment.cid = ObjectId(assessment.cid);
- AssessmentEntity.replace(qid, assessment, cb);
+ AssessmentModel.replace(aid, assessment, cb);
});
});
},
// 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)
+ 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)
- 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}); }
+ );
});
});
},
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);
});
},
- endSession: function(aid, number, password, cb)
+ // NOTE: no callbacks for next function, failures are not so important
+ // (because monitored: teachers can see what's going on)
+ newConnection: function(aid, number)
{
- AssessmentEntity.endAssessment(aid, number, password, (err,ret) => {
- if (!!err || !ret)
- return cb(err,ret);
- AssessmentEntity.getConclusion(aid, (err2,conclusion) => {
- cb(err2, {conclusion:conclusion});
- });
+ //increment discoCount, reset discoTime to NULL, update totalDisco
+ AssessmentModel.getDiscoTime(aid, number, (err,discoTime) => {
+ if (!!discoTime)
+ AssessmentModel.addDisco(aid, number, Date.now() - discoTime);
});
},
-};
+}
module.exports = AssessmentModel;