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