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