refactoring, better README (breaking commit...)
[qomet.git] / models / assessment.js
1 const CourseModel = require("../models/course");
2 const UserModel = require("../models/user");
3 const ObjectId = require("bson-objectid");
4 const TokenGen = require("../utils/tokenGenerator");
5 const db = require("../utils/database");
6
7 const AssessmentModel =
8 {
9 /*
10 * Structure:
11 * _id: BSON id
12 * cid: course ID
13 * name: varchar
14 * active: boolean true/false
15 * mode: secure | exam | open (decreasing security)
16 * fixed: bool (questions in fixed order; default: false)
17 * display: "one" or "all" (generally "all" for open questions, but...)
18 * time: 0, //<=0 means "untimed"; otherwise, time in seconds
19 * introduction: "",
20 * coefficient: number, default 1
21 * questions: array of
22 * index: for paper test, like 2.1.a (?!); and quiz: 0, 1, 2, 3...
23 * wording: varchar (HTML)
24 * options: array of varchar --> if present, question type == quiz!
25 * fixed: bool, options in fixed order (default: false)
26 * answer: array of integers (for quiz) or html text (for paper); striped in exam mode
27 * active: boolean, is question in current assessment?
28 * points: points for this question (default 1)
29 * time: 0 (<=0: untimed)
30 * papers : array of
31 * number: student number
32 * inputs: array of {index,answer[array of integers or html text],startTime}
33 * current: index of current question (if relevant: display="one")
34 * startTime
35 * discoTime, totalDisco: last disconnect timestamp (if relevant) + total time
36 * discoCount: number of disconnections
37 * password: random string identifying student for exam session TEMPORARY
38 */
39
40 //////////////////
41 // BASIC FUNCTIONS
42
43 getById: function(aid, callback)
44 {
45 db.assessments.findOne(
46 { _id: aid },
47 callback
48 );
49 },
50
51 getByPath: function(cid, name, callback)
52 {
53 db.assessments.findOne(
54 {
55 cid: cid,
56 name: name,
57 },
58 callback
59 );
60 },
61
62 insert: function(cid, name, callback)
63 {
64 db.assessments.insert(
65 {
66 name: name,
67 cid: cid,
68 active: false,
69 mode: "exam",
70 fixed: false,
71 display: "one",
72 time: 0,
73 introduction: "",
74 coefficient: 1,
75 questions: [ ],
76 papers: [ ],
77 },
78 callback
79 );
80 },
81
82 getByCourse: function(cid, callback)
83 {
84 db.assessments.find(
85 { cid: cid },
86 callback
87 );
88 },
89
90 // arg: full assessment without _id field
91 replace: function(aid, assessment, cb)
92 {
93 // Should be: (but unsupported by mongojs)
94 // db.assessments.replaceOne(
95 // { _id: aid },
96 // assessment,
97 // cb
98 // );
99 // Temporary workaround:
100 db.assessments.update(
101 { _id: aid },
102 { $set: assessment },
103 cb
104 );
105 },
106
107 getQuestions: function(aid, callback)
108 {
109 db.assessments.findOne(
110 {
111 _id: aid,
112 display: "all",
113 },
114 { questions: 1},
115 (err,res) => {
116 callback(err, !!res ? res.questions : null);
117 }
118 );
119 },
120
121 getQuestion: function(aid, index, callback)
122 {
123 db.assessments.findOne(
124 {
125 _id: aid,
126 display: "one",
127 },
128 { questions: 1},
129 (err,res) => {
130 if (!!err || !res)
131 return callback(err, res);
132 const qIdx = res.questions.findIndex( item => { return item.index == index; });
133 if (qIdx === -1)
134 return callback({errmsg: "Question not found"}, null);
135 callback(null, res.questions[qIdx]);
136 }
137 );
138 },
139
140 getPaperByNumber: function(aid, number, callback)
141 {
142 db.assessments.findOne(
143 {
144 _id: aid,
145 "papers.number": number,
146 },
147 (err,a) => {
148 if (!!err || !a)
149 return callback(err,a);
150 for (let p of a.papers)
151 {
152 if (p.number == number)
153 return callback(null,p); //reached for sure
154 }
155 }
156 );
157 },
158
159 // NOTE: no callbacks for 2 next functions, failures are not so important
160 // (because monitored: teachers can see what's going on)
161
162 addDisco: function(aid, number, deltaTime)
163 {
164 db.assessments.update(
165 {
166 _id: aid,
167 "papers.number": number,
168 },
169 { $inc: {
170 "papers.$.discoCount": 1,
171 "papers.$.totalDisco": deltaTime,
172 } },
173 { $set: { "papers.$.discoTime": null } }
174 );
175 },
176
177 setDiscoTime: function(aid, number)
178 {
179 db.assessments.update(
180 {
181 _id: aid,
182 "papers.number": number,
183 },
184 { $set: { "papers.$.discoTime": Date.now() } }
185 );
186 },
187
188 getDiscoTime: function(aid, number, cb)
189 {
190 db.assessments.findOne(
191 { _id: aid },
192 (err,a) => {
193 if (!!err)
194 return cb(err, null);
195 const idx = a.papers.findIndex( item => { return item.number == number; });
196 cb(null, a.papers[idx].discoTime);
197 }
198 );
199 },
200
201 hasInput: function(aid, number, password, idx, cb)
202 {
203 db.assessments.findOne(
204 {
205 _id: aid,
206 "papers.number": number,
207 "papers.password": password,
208 },
209 (err,a) => {
210 if (!!err || !a)
211 return cb(err,a);
212 let papIdx = a.papers.findIndex( item => { return item.number == number; });
213 for (let i of a.papers[papIdx].inputs)
214 {
215 if (i.index == idx)
216 return cb(null,true);
217 }
218 cb(null,false);
219 }
220 );
221 },
222
223 // https://stackoverflow.com/questions/27874469/mongodb-push-in-nested-array
224 setInput: function(aid, number, password, input, callback) //input: index + arrayOfInt (or txt)
225 {
226 db.assessments.update(
227 {
228 _id: aid,
229 "papers.number": number,
230 "papers.password": password,
231 },
232 { $push: { "papers.$.inputs": input } },
233 callback
234 );
235 },
236
237 endAssessment: function(aid, number, password, callback)
238 {
239 db.assessments.update(
240 {
241 _id: aid,
242 "papers.number": number,
243 "papers.password": password,
244 },
245 { $set: {
246 "papers.$.endTime": Date.now(),
247 "papers.$.password": "",
248 } },
249 callback
250 );
251 },
252
253 remove: function(aid, cb)
254 {
255 db.assessments.remove(
256 { _id: aid },
257 cb
258 );
259 },
260
261 removeGroup: function(cid, cb)
262 {
263 db.assessments.remove(
264 { cid: cid },
265 cb
266 );
267 },
268
269 /////////////////////
270 // ADVANCED FUNCTIONS
271
272 getByRefs: function(initials, code, name, cb)
273 {
274 UserModel.getByInitials(initials, (err,user) => {
275 if (!!err || !user)
276 return cb(err || {errmsg: "User not found"});
277 CourseModel.getByPath(user._id, code, (err2,course) => {
278 if (!!err2 || !course)
279 return cb(err2 || {errmsg: "Course not found"});
280 AssessmentModel.getByPath(course._id, name, (err3,assessment) => {
281 if (!!err3 || !assessment)
282 return cb(err3 || {errmsg: "Assessment not found"});
283 cb(null,assessment);
284 });
285 });
286 });
287 },
288
289 checkPassword: function(aid, number, password, cb)
290 {
291 AssessmentModel.getById(aid, (err,assessment) => {
292 if (!!err || !assessment)
293 return cb(err, assessment);
294 const paperIdx = assessment.papers.findIndex( item => { return item.number == number; });
295 if (paperIdx === -1)
296 return cb({errmsg: "Paper not found"}, false);
297 cb(null, assessment.papers[paperIdx].password == password);
298 });
299 },
300
301 add: function(uid, cid, name, cb)
302 {
303 // 1) Check that course is owned by user of ID uid
304 CourseModel.getById(cid, (err,course) => {
305 if (!!err || !course)
306 return cb({errmsg: "Course retrieval failure"});
307 if (!course.uid.equals(uid))
308 return cb({errmsg:"Not your course"},undefined);
309 // 2) Insert new blank assessment
310 AssessmentModel.insert(cid, name, cb);
311 });
312 },
313
314 update: function(uid, assessment, cb)
315 {
316 const aid = ObjectId(assessment._id);
317 // 1) Check that assessment is owned by user of ID uid
318 AssessmentModel.getById(aid, (err,assessmentOld) => {
319 if (!!err || !assessmentOld)
320 return cb({errmsg: "Assessment retrieval failure"});
321 CourseModel.getById(ObjectId(assessmentOld.cid), (err2,course) => {
322 if (!!err2 || !course)
323 return cb({errmsg: "Course retrieval failure"});
324 if (!course.uid.equals(uid))
325 return cb({errmsg:"Not your course"},undefined);
326 // 2) Replace assessment
327 delete assessment["_id"];
328 assessment.cid = ObjectId(assessment.cid);
329 AssessmentModel.replace(aid, assessment, cb);
330 });
331 });
332 },
333
334 // Set password in responses collection
335 startSession: function(aid, number, password, cb)
336 {
337 AssessmentModel.getPaperByNumber(aid, number, (err,paper) => {
338 if (!!err)
339 return cb(err,null);
340 if (!paper && !!password)
341 return cb({errmsg: "Cannot start a new exam before finishing current"},null);
342 if (!!paper)
343 {
344 if (!password)
345 return cb({errmsg: "Missing password"});
346 if (paper.password != password)
347 return cb({errmsg: "Wrong password"});
348 }
349 AssessmentModel.getQuestions(aid, (err2,questions) => {
350 if (!!err2)
351 return cb(err2,null);
352 if (!!paper)
353 return cb(null,{paper:paper});
354 const pwd = TokenGen.generate(12); //arbitrary number, 12 seems enough...
355 db.assessments.update(
356 { _id: aid },
357 { $push: { papers: {
358 number: number,
359 startTime: Date.now(),
360 endTime: undefined,
361 password: password,
362 totalDisco: 0,
363 discoCount: 0,
364 inputs: [ ], //TODO: this is stage 1, stack indexed answers.
365 // then build JSON tree for easier access / correct
366 }}},
367 (err3,ret) => { cb(err3,{password:password}); }
368 );
369 });
370 });
371 },
372
373 newAnswer: function(aid, number, password, input, cb)
374 {
375 // Check that student hasn't already answered
376 AssessmentModel.hasInput(aid, number, password, input.index, (err,ret) => {
377 if (!!err)
378 return cb(err,null);
379 if (!!ret)
380 return cb({errmsg:"Question already answered"},null);
381 AssessmentModel.setInput(aid, number, password, input, (err2,ret2) => {
382 if (!!err2 || !ret2)
383 return cb(err2,ret2);
384 return cb(null,ret2);
385 });
386 });
387 },
388
389 // NOTE: no callbacks for next function, failures are not so important
390 // (because monitored: teachers can see what's going on)
391 newConnection: function(aid, number)
392 {
393 //increment discoCount, reset discoTime to NULL, update totalDisco
394 AssessmentModel.getDiscoTime(aid, number, (err,discoTime) => {
395 if (!!discoTime)
396 AssessmentModel.addDisco(aid, number, Date.now() - discoTime);
397 });
398 },
399 }
400
401 module.exports = AssessmentModel;