@programisto/edrm-exams 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +135 -0
  2. package/dist/bin/www.d.ts +2 -0
  3. package/dist/bin/www.js +9 -0
  4. package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +10 -0
  5. package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +68 -0
  6. package/dist/modules/edrm-exams/lib/openai.d.ts +36 -0
  7. package/dist/modules/edrm-exams/lib/openai.js +82 -0
  8. package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
  9. package/dist/modules/edrm-exams/listeners/correct.listener.js +85 -0
  10. package/dist/modules/edrm-exams/models/candidate.models.d.ts +13 -0
  11. package/dist/modules/edrm-exams/models/candidate.models.js +59 -0
  12. package/dist/modules/edrm-exams/models/company.model.d.ts +8 -0
  13. package/dist/modules/edrm-exams/models/company.model.js +34 -0
  14. package/dist/modules/edrm-exams/models/test-category.models.d.ts +7 -0
  15. package/dist/modules/edrm-exams/models/test-category.models.js +29 -0
  16. package/dist/modules/edrm-exams/models/test-question.model.d.ts +25 -0
  17. package/dist/modules/edrm-exams/models/test-question.model.js +70 -0
  18. package/dist/modules/edrm-exams/models/test-result.model.d.ts +26 -0
  19. package/dist/modules/edrm-exams/models/test-result.model.js +70 -0
  20. package/dist/modules/edrm-exams/models/test.model.d.ts +52 -0
  21. package/dist/modules/edrm-exams/models/test.model.js +123 -0
  22. package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
  23. package/dist/modules/edrm-exams/models/user.model.js +64 -0
  24. package/dist/modules/edrm-exams/routes/company.router.d.ts +7 -0
  25. package/dist/modules/edrm-exams/routes/company.router.js +108 -0
  26. package/dist/modules/edrm-exams/routes/exams-candidate.router.d.ts +7 -0
  27. package/dist/modules/edrm-exams/routes/exams-candidate.router.js +299 -0
  28. package/dist/modules/edrm-exams/routes/exams.router.d.ts +7 -0
  29. package/dist/modules/edrm-exams/routes/exams.router.js +1012 -0
  30. package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
  31. package/dist/modules/edrm-exams/routes/result.router.js +314 -0
  32. package/dist/modules/edrm-exams/routes/user.router.d.ts +7 -0
  33. package/dist/modules/edrm-exams/routes/user.router.js +96 -0
  34. package/package.json +73 -8
@@ -0,0 +1,1012 @@
1
+ import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter as emitter, enduranceEventTypes as eventTypes } from 'endurance-core';
2
+ import Test from '../models/test.model.js';
3
+ import TestQuestion from '../models/test-question.model.js';
4
+ import TestResult from '../models/test-result.model.js';
5
+ import TestCategory from '../models/test-category.models.js';
6
+ import Candidate from '../models/candidate.models.js';
7
+ import { generateLiveMessage } from '../lib/openai.js';
8
+ class ExamsRouter extends EnduranceRouter {
9
+ constructor() {
10
+ super(EnduranceAuthMiddleware.getInstance());
11
+ }
12
+ setupRoutes() {
13
+ const authenticatedOptions = {
14
+ requireAuth: false,
15
+ permissions: []
16
+ };
17
+ // Créer une catégorie
18
+ this.post('/categories', authenticatedOptions, async (req, res) => {
19
+ const { name } = req.body;
20
+ if (!name) {
21
+ return res.status(400).json({ message: 'Error, all params are required' });
22
+ }
23
+ try {
24
+ const newCategory = new TestCategory({ name });
25
+ await newCategory.save();
26
+ res.status(201).json({ message: 'category created with sucess', category: newCategory });
27
+ }
28
+ catch (err) {
29
+ console.error('error when creating category : ', err);
30
+ res.status(500).json({ message: 'Internal server error' });
31
+ }
32
+ });
33
+ // Lister toutes les catégories
34
+ this.get('/categories', authenticatedOptions, async (req, res) => {
35
+ try {
36
+ const categories = await TestCategory.find();
37
+ res.status(200).json({ array: categories });
38
+ }
39
+ catch (err) {
40
+ console.error('error when creating category : ', err);
41
+ res.status(500).json({ message: 'Internal server error' });
42
+ }
43
+ });
44
+ // Obtenir une catégorie par son ID
45
+ this.get('/categorie/:id', authenticatedOptions, async (req, res) => {
46
+ const { id } = req.params;
47
+ try {
48
+ const category = await TestCategory.findById(id);
49
+ if (!category) {
50
+ return res.status(404).json({ message: 'no category founded with this id' });
51
+ }
52
+ res.status(200).json({ array: category });
53
+ }
54
+ catch (err) {
55
+ console.error('error when creating category : ', err);
56
+ res.status(500).json({ message: 'Internal server error' });
57
+ }
58
+ });
59
+ // Créer un test
60
+ this.post('/test', authenticatedOptions, async (req, res) => {
61
+ const { title, description, targetJob, seniorityLevel, categories, state = 'draft' } = req.body;
62
+ const user = req.user;
63
+ if (!title || !targetJob || !seniorityLevel) {
64
+ return res.status(400).json({ message: 'Error, all params are required' });
65
+ }
66
+ try {
67
+ const companyId = user?.companyId;
68
+ const userId = user?._id;
69
+ const processedCategories = await Promise.all(categories?.map(async (category) => {
70
+ let existingCategory = await TestCategory.findOne({ name: category.name });
71
+ if (!existingCategory) {
72
+ existingCategory = await TestCategory.create({ name: category.name });
73
+ }
74
+ return {
75
+ categoryId: existingCategory._id,
76
+ expertiseLevel: category.expertiseLevel
77
+ };
78
+ }) || []);
79
+ const newTest = new Test({
80
+ companyId,
81
+ userId,
82
+ title,
83
+ description,
84
+ targetJob,
85
+ seniorityLevel,
86
+ state,
87
+ categories: processedCategories
88
+ });
89
+ await newTest.save();
90
+ res.status(201).json({ message: 'test created with sucess', data: newTest });
91
+ }
92
+ catch (err) {
93
+ console.error('error when creating test : ', err);
94
+ res.status(500).json({ message: 'Internal server error' });
95
+ }
96
+ });
97
+ // Modifier un test
98
+ this.put('/test/:id', authenticatedOptions, async (req, res) => {
99
+ const { id } = req.params;
100
+ const { title, description, targetJob, seniorityLevel, categories, state } = req.body;
101
+ try {
102
+ const test = await Test.findById(id);
103
+ if (!test) {
104
+ return res.status(404).json({ message: 'Test non trouvé' });
105
+ }
106
+ if (title)
107
+ test.title = title;
108
+ if (description)
109
+ test.description = description;
110
+ if (targetJob)
111
+ test.targetJob = targetJob;
112
+ if (seniorityLevel)
113
+ test.seniorityLevel = seniorityLevel;
114
+ if (state)
115
+ test.state = state;
116
+ if (categories) {
117
+ const processedCategories = await Promise.all(categories.map(async (category) => {
118
+ let existingCategory = await TestCategory.findOne({ name: category.name });
119
+ if (!existingCategory) {
120
+ existingCategory = await TestCategory.create({ name: category.name });
121
+ }
122
+ return {
123
+ categoryId: existingCategory._id,
124
+ expertiseLevel: category.expertiseLevel
125
+ };
126
+ }));
127
+ test.categories = processedCategories;
128
+ }
129
+ await test.save();
130
+ res.status(200).json({ message: 'Test modifié avec succès', data: test });
131
+ }
132
+ catch (err) {
133
+ console.error('Erreur lors de la modification du test : ', err);
134
+ res.status(500).json({ message: 'Erreur interne du serveur' });
135
+ }
136
+ });
137
+ // Supprimer un test
138
+ this.delete('/test/:id', authenticatedOptions, async (req, res) => {
139
+ const { id } = req.params;
140
+ try {
141
+ const test = await Test.findById(id);
142
+ if (!test) {
143
+ return res.status(404).json({ message: 'Test not found' });
144
+ }
145
+ for (let i = 0; i < test.questions.length; i++) {
146
+ await TestQuestion.findByIdAndDelete(test.questions[i]);
147
+ }
148
+ await TestResult.deleteMany({ testId: id });
149
+ await Test.findByIdAndDelete(id);
150
+ res.status(200).json({ message: 'test deleted with sucess' });
151
+ }
152
+ catch (err) {
153
+ console.error('error when deleting user : ', err);
154
+ res.status(500).json({ message: 'Internal server error' });
155
+ }
156
+ });
157
+ // Obtenir un test par son ID
158
+ this.get('/test/:id', authenticatedOptions, async (req, res) => {
159
+ const { id } = req.params;
160
+ try {
161
+ const test = await Test.findById(id);
162
+ if (!test) {
163
+ return res.status(404).json({ message: 'no test founded with this id' });
164
+ }
165
+ const questions = [];
166
+ for (const questionRef of test.questions) {
167
+ console.log(questionRef);
168
+ const question = await TestQuestion.findById(questionRef.questionId);
169
+ if (question) {
170
+ console.log(question);
171
+ questions.push(question);
172
+ }
173
+ }
174
+ res.status(200).json({ test, questions });
175
+ }
176
+ catch (err) {
177
+ console.error('error when geting test : ', err);
178
+ res.status(500).json({ message: 'Internal server error' });
179
+ }
180
+ });
181
+ // Lister tous les tests
182
+ this.get('/', authenticatedOptions, async (req, res) => {
183
+ try {
184
+ const page = parseInt(req.query.page) || 1;
185
+ const limit = parseInt(req.query.limit) || 10;
186
+ const skip = (page - 1) * limit;
187
+ const search = req.query.search || '';
188
+ const targetJob = req.query.targetJob || 'all';
189
+ const seniorityLevel = req.query.seniorityLevel || 'all';
190
+ const state = req.query.state || 'all';
191
+ const sortBy = req.query.sortBy || 'updatedAt';
192
+ const sortOrder = req.query.sortOrder || 'desc';
193
+ // Construction de la requête de recherche
194
+ const query = {};
195
+ // Filtres
196
+ if (targetJob !== 'all') {
197
+ query.targetJob = targetJob;
198
+ }
199
+ if (seniorityLevel !== 'all') {
200
+ query.seniorityLevel = seniorityLevel;
201
+ }
202
+ if (state !== 'all') {
203
+ query.state = state;
204
+ }
205
+ // Recherche sur testName et targetJob
206
+ if (search) {
207
+ query.$or = [
208
+ { title: { $regex: search, $options: 'i' } },
209
+ { description: { $regex: search, $options: 'i' } },
210
+ { targetJob: { $regex: search, $options: 'i' } },
211
+ { seniorityLevel: { $regex: search, $options: 'i' } }
212
+ ];
213
+ }
214
+ // Construction du tri
215
+ const allowedSortFields = ['testName', 'targetJob', 'seniorityLevel', 'updatedAt'];
216
+ const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'updatedAt';
217
+ const sortOptions = {
218
+ [sortField]: sortOrder === 'asc' ? 1 : -1
219
+ };
220
+ const [tests, total] = await Promise.all([
221
+ Test.find(query)
222
+ .sort(sortOptions)
223
+ .skip(skip)
224
+ .limit(limit)
225
+ .exec(),
226
+ Test.countDocuments(query)
227
+ ]);
228
+ // Récupérer les noms des catégories pour chaque test
229
+ const testsWithCategories = await Promise.all(tests.map(async (test) => {
230
+ const testObj = test.toObject();
231
+ if (testObj.categories && testObj.categories.length > 0) {
232
+ const categoriesWithNames = await Promise.all(testObj.categories.map(async (category) => {
233
+ const categoryDoc = await TestCategory.findById(category.categoryId);
234
+ return {
235
+ ...category,
236
+ categoryName: categoryDoc?.name || 'Catégorie inconnue'
237
+ };
238
+ }));
239
+ testObj.categories = categoriesWithNames;
240
+ }
241
+ return testObj;
242
+ }));
243
+ const totalPages = Math.ceil(total / limit);
244
+ return res.json({
245
+ data: testsWithCategories,
246
+ pagination: {
247
+ currentPage: page,
248
+ totalPages,
249
+ totalItems: total,
250
+ itemsPerPage: limit,
251
+ hasNextPage: page < totalPages,
252
+ hasPreviousPage: page > 1
253
+ }
254
+ });
255
+ }
256
+ catch (err) {
257
+ console.error('error when geting tests : ', err);
258
+ res.status(500).json({ message: 'Internal server error' });
259
+ }
260
+ });
261
+ // Supprimer une catégorie d'un test
262
+ this.delete('/test/removeCategory/:testId', authenticatedOptions, async (req, res) => {
263
+ const { testId } = req.params;
264
+ const { categoryName } = req.body;
265
+ try {
266
+ const category = await TestCategory.findOne({ name: categoryName });
267
+ if (!category)
268
+ return res.status(404).json({ message: 'Category not found' });
269
+ const test = await Test.findByIdAndUpdate(testId, { $pull: { categories: { categoryId: category._id } } }, { new: true });
270
+ if (!test)
271
+ return res.status(404).json({ message: 'Test not found' });
272
+ res.status(200).json({ message: 'Category removed', test });
273
+ }
274
+ catch (err) {
275
+ console.error('Error when removing category from test:', err);
276
+ res.status(500).json({ message: 'Internal server error' });
277
+ }
278
+ });
279
+ // Ajouter une catégorie à un test
280
+ this.put('/test/addCategory/:testId', authenticatedOptions, async (req, res) => {
281
+ const { testId } = req.params;
282
+ const { categoryName, expertiseLevel } = req.body;
283
+ try {
284
+ let category = await TestCategory.findOne({ name: categoryName });
285
+ if (!category) {
286
+ category = new TestCategory({ name: categoryName });
287
+ await category.save();
288
+ }
289
+ const test = await Test.findById(testId);
290
+ if (!test) {
291
+ return res.status(404).json({ message: 'Test not found' });
292
+ }
293
+ const categoryExists = test.categories.some(cat => cat.categoryId.equals(category._id));
294
+ if (categoryExists) {
295
+ return res.status(200).json({ message: 'Category already exists in the test' });
296
+ }
297
+ test.categories.push({ categoryId: category._id, expertiseLevel });
298
+ await test.save();
299
+ res.status(200).json({ message: 'Category added successfully', data: test });
300
+ }
301
+ catch (err) {
302
+ console.error('Error when adding category to test:', err);
303
+ res.status(500).json({ message: 'Internal server error' });
304
+ }
305
+ });
306
+ // Obtenir une question par son ID
307
+ this.get('/test/question/:questionId', authenticatedOptions, async (req, res) => {
308
+ const { questionId } = req.params;
309
+ const question = await TestQuestion.findById(questionId);
310
+ if (!question) {
311
+ return res.status(404).json({ message: 'no question founded with this id' });
312
+ }
313
+ res.status(200).json({ data: question });
314
+ });
315
+ // Obtenir toutes les questions d'un test
316
+ this.get('/test/questions/:testId', authenticatedOptions, async (req, res) => {
317
+ const { testId } = req.params;
318
+ try {
319
+ const test = await Test.findById(testId);
320
+ if (!test) {
321
+ return res.status(404).json({ message: 'Test not found' });
322
+ }
323
+ const questions = [];
324
+ for (const questionId of test.questions) {
325
+ const question = await TestQuestion.findById(questionId);
326
+ if (question) {
327
+ questions.push(question);
328
+ }
329
+ }
330
+ res.status(200).json({ array: questions });
331
+ }
332
+ catch (err) {
333
+ console.error('Error when getting question:', err);
334
+ res.status(500).json({ message: 'Internal server error' });
335
+ }
336
+ });
337
+ // Supprimer une question d'un test
338
+ this.delete('/test/question/:testId/:questionId', authenticatedOptions, async (req, res) => {
339
+ const { testId, questionId } = req.params;
340
+ const question = await TestQuestion.findByIdAndDelete(questionId);
341
+ const test = await Test.findById(testId);
342
+ if (!question) {
343
+ return res.status(404).json({ message: 'no question founded with this id' });
344
+ }
345
+ if (!test) {
346
+ return res.status(404).json({ message: 'no test founded with this id' });
347
+ }
348
+ test.questions = test.questions.filter(id => id.toString() !== questionId);
349
+ await test.save();
350
+ res.status(200).json({ message: 'question deleted with sucess' });
351
+ });
352
+ // Supprimer toutes les questions d'un test
353
+ this.delete('/test/questions/:testId', authenticatedOptions, async (req, res) => {
354
+ const { testId } = req.params;
355
+ const test = await Test.findById(testId);
356
+ if (!test) {
357
+ return res.status(404).json({ message: 'no test founded with this id' });
358
+ }
359
+ for (const questionId of test.questions) {
360
+ await TestQuestion.findByIdAndDelete(questionId);
361
+ }
362
+ test.questions = [];
363
+ await test.save();
364
+ res.status(200).json({ message: 'questions deleted with sucess' });
365
+ });
366
+ // Modifier une question
367
+ this.put('/test/modifyQuestion/:id', authenticatedOptions, async (req, res) => {
368
+ const { id } = req.params;
369
+ const { instruction, maxScore, time, possibleResponses, textType } = req.body;
370
+ try {
371
+ const question = await TestQuestion.findById(id);
372
+ if (!question) {
373
+ return res.status(404).json({ message: 'no question founded with this id' });
374
+ }
375
+ if (instruction) {
376
+ question.instruction = instruction;
377
+ }
378
+ if (maxScore) {
379
+ question.maxScore = maxScore;
380
+ }
381
+ if (time) {
382
+ question.time = time;
383
+ }
384
+ if (textType) {
385
+ question.textType = textType;
386
+ }
387
+ if (possibleResponses) {
388
+ question.possibleResponses = possibleResponses;
389
+ }
390
+ await question.save();
391
+ res.status(200).json({ message: 'question modified with sucess' });
392
+ }
393
+ catch (err) {
394
+ console.error('error when modify question : ', err);
395
+ res.status(500).json({ message: 'Internal server error' });
396
+ }
397
+ });
398
+ // Ajouter une question à un test
399
+ this.put('/test/addCustomQuestion/:id', authenticatedOptions, async (req, res) => {
400
+ const { id } = req.params;
401
+ const { questionType, instruction, maxScore, time } = req.body;
402
+ try {
403
+ const test = await Test.findById(id);
404
+ if (!test) {
405
+ return res.status(404).json({ message: 'no test founded with this id' });
406
+ }
407
+ const question = new TestQuestion({
408
+ questionType,
409
+ instruction,
410
+ maxScore,
411
+ time
412
+ });
413
+ await question.save();
414
+ test.questions.push({ questionId: question._id, order: test.questions.length });
415
+ await test.save();
416
+ res.status(200).json({ message: 'question added in test', test });
417
+ }
418
+ catch (err) {
419
+ console.error('error when add question in test : ', err);
420
+ res.status(500).json({ message: 'Internal server error' });
421
+ }
422
+ });
423
+ // Ajouter une question à un test
424
+ this.put('/test/addQuestion/:id', authenticatedOptions, async (req, res) => {
425
+ const { id } = req.params;
426
+ const { questionType, category, expertiseLevel } = req.body;
427
+ try {
428
+ const test = await Test.findById(id);
429
+ if (!test) {
430
+ return res.status(404).json({ message: 'no test founded with this id' });
431
+ }
432
+ const otherQuestionsIds = test.questions.map(question => question.questionId);
433
+ const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
434
+ const generatedQuestion = await generateLiveMessage('createQuestion', {
435
+ job: test.targetJob,
436
+ seniority: test.seniorityLevel,
437
+ questionType,
438
+ category,
439
+ expertiseLevel,
440
+ otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
441
+ }, true);
442
+ const question = new TestQuestion(JSON.parse(generatedQuestion));
443
+ await question.save();
444
+ test.questions.push({ questionId: question._id, order: test.questions.length });
445
+ await test.save();
446
+ res.status(200).json({ message: 'question added in test', test });
447
+ }
448
+ catch (err) {
449
+ console.error('error when add question in test : ', err);
450
+ res.status(500).json({ message: 'Internal server error' });
451
+ }
452
+ });
453
+ // Mélanger les questions d'un test
454
+ this.get('/test/shuffle/:testId', authenticatedOptions, async (req, res) => {
455
+ const { testId } = req.params;
456
+ try {
457
+ const test = await Test.findById(testId);
458
+ if (!test) {
459
+ return res.status(404).json({ message: 'Test not found' });
460
+ }
461
+ for (let i = test.questions.length - 1; i > 0; i--) {
462
+ const j = Math.floor(Math.random() * (i + 1));
463
+ [test.questions[i], test.questions[j]] = [test.questions[j], test.questions[i]];
464
+ }
465
+ await test.save();
466
+ res.status(200).json({ message: 'Questions shuffled', test });
467
+ }
468
+ catch (err) {
469
+ console.error('Error when shuffling questions:', err);
470
+ res.status(500).json({ message: 'Internal server error' });
471
+ }
472
+ });
473
+ // Ajouter un texte d'invitation à un test
474
+ this.put('/test/addInvitationText/:id', authenticatedOptions, async (req, res) => {
475
+ const { id } = req.params;
476
+ const { invitationText } = req.body;
477
+ try {
478
+ const test = await Test.findById(id);
479
+ if (!test) {
480
+ return res.status(404).json({ message: 'no test founded with this id' });
481
+ }
482
+ test.invitationText = invitationText;
483
+ await test.save();
484
+ res.status(200).json({
485
+ message: 'invitation text added in test',
486
+ invitationText
487
+ });
488
+ }
489
+ catch (err) {
490
+ console.error('error when add invitation text in test : ', err);
491
+ res.status(500).json({ message: 'Internal server error' });
492
+ }
493
+ });
494
+ // Obtenir un résultat par son ID
495
+ this.get('/result/:id', authenticatedOptions, async (req, res) => {
496
+ const { id } = req.params;
497
+ try {
498
+ const result = await TestResult.findById(id);
499
+ if (!result) {
500
+ return res.status(404).json({ message: 'no result founded with this id' });
501
+ }
502
+ res.status(200).json({ message: 'result', data: result });
503
+ }
504
+ catch (err) {
505
+ console.error('error when geting result : ', err);
506
+ res.status(500).json({ message: 'Internal server error' });
507
+ }
508
+ });
509
+ // Lister tous les résultats
510
+ this.get('/results/', authenticatedOptions, async (req, res) => {
511
+ try {
512
+ const results = await TestResult.find();
513
+ if (!results) {
514
+ return res.status(404).json({ message: 'no results founded' });
515
+ }
516
+ res.status(200).json({ array: results });
517
+ }
518
+ catch (err) {
519
+ console.error('error when geting results : ', err);
520
+ res.status(500).json({ message: 'Internal server error' });
521
+ }
522
+ });
523
+ // Créer un résultat
524
+ this.post('/invite', authenticatedOptions, async (req, res) => {
525
+ const { candidateId, testId } = req.body;
526
+ if (!candidateId || !testId) {
527
+ return res.status(400).json({ message: 'Error, all params are required' });
528
+ }
529
+ try {
530
+ const test = await Test.findById(testId);
531
+ if (!test) {
532
+ return res.status(404).json({ message: 'Test not found' });
533
+ }
534
+ const categories = test.categories.map(cat => ({ categoryId: cat.categoryId }));
535
+ const newResult = new TestResult({
536
+ candidateId,
537
+ testId,
538
+ categories,
539
+ state: 'pending',
540
+ invitationDate: Date.now()
541
+ });
542
+ await newResult.save();
543
+ // Récupérer l'email du candidat
544
+ const candidate = await Candidate.findById(candidateId);
545
+ if (!candidate) {
546
+ return res.status(404).json({ message: 'Candidate not found' });
547
+ }
548
+ const email = candidate.email;
549
+ // Construire le lien d'invitation
550
+ const testLink = process.env.TEST_INVITATION_LINK || '';
551
+ // Récupérer les credentials d'envoi
552
+ const emailUser = process.env.EMAIL_USER_TURING;
553
+ const emailPassword = process.env.EMAIL_PASSWORD_TURING;
554
+ // Envoyer l'email via l'event emitter
555
+ await emitter.emit(eventTypes.SEND_EMAIL, {
556
+ template: 'test-invitation-turing',
557
+ to: email,
558
+ from: emailUser,
559
+ subject: 'Vous êtes invité à passer un test technique - École de Turing',
560
+ emailUser,
561
+ emailPassword,
562
+ data: {
563
+ testLink
564
+ }
565
+ });
566
+ res.status(201).json({ message: 'result created with sucess', data: newResult });
567
+ }
568
+ catch (err) {
569
+ console.error('error when creating result : ', err);
570
+ res.status(500).json({ message: 'Internal server error' });
571
+ }
572
+ });
573
+ // Obtenir la question suivante
574
+ this.get('/result/getNextQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
575
+ const { id, idCurrentQuestion } = req.params;
576
+ try {
577
+ const result = await TestResult.findById(id);
578
+ if (!result) {
579
+ return res.status(404).json({ message: 'Result not found' });
580
+ }
581
+ const test = await Test.findById(result.testId);
582
+ if (!test) {
583
+ return res.status(404).json({ message: 'Test not found' });
584
+ }
585
+ const questionIndex = test.questions.indexOf(idCurrentQuestion);
586
+ if (questionIndex < test.questions.length) {
587
+ const nextQuestion = test.questions[questionIndex + 1];
588
+ res.status(200).json({ data: nextQuestion });
589
+ }
590
+ else {
591
+ res.status(200).json({ data: null });
592
+ }
593
+ }
594
+ catch (err) {
595
+ console.error('error when geting the next question : ', err);
596
+ res.status(500).json({ message: 'Internal server error' });
597
+ }
598
+ });
599
+ // Vérifier si c'est la dernière question
600
+ this.get('/result/isLastQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
601
+ const { id, idCurrentQuestion } = req.params;
602
+ try {
603
+ const result = await TestResult.findById(id);
604
+ if (!result) {
605
+ return res.status(404).json({ message: 'Result not found' });
606
+ }
607
+ const test = await Test.findById(result.testId);
608
+ if (!test) {
609
+ return res.status(404).json({ message: 'Test not found' });
610
+ }
611
+ const questionIndex = test.questions.indexOf(idCurrentQuestion);
612
+ if (questionIndex === test.questions.length - 1) {
613
+ res.status(200).json({ data: true });
614
+ }
615
+ else {
616
+ res.status(200).json({ data: false });
617
+ }
618
+ }
619
+ catch (err) {
620
+ console.error('error when geting the next question : ', err);
621
+ res.status(500).json({ message: 'Internal server error' });
622
+ }
623
+ });
624
+ // Obtenir une question
625
+ this.get('/result/question/:questionId', authenticatedOptions, async (req, res) => {
626
+ const { questionId } = req.params;
627
+ try {
628
+ const question = await TestQuestion.findById(questionId);
629
+ if (!question) {
630
+ return res.status(404).json({ message: 'not found' });
631
+ }
632
+ res.status(200).json({ data: question });
633
+ }
634
+ catch (err) {
635
+ console.error('error when geting the question : ', err);
636
+ res.status(500).json({ message: 'Internal server error' });
637
+ }
638
+ });
639
+ // Envoyer une réponse
640
+ this.put('/result/sendResponse/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
641
+ const { id, idCurrentQuestion } = req.params;
642
+ const { candidateResponse } = req.body;
643
+ try {
644
+ const result = await TestResult.findById(id);
645
+ if (!result) {
646
+ return res.status(404).json({ message: 'Result not found' });
647
+ }
648
+ const test = await Test.findById(result.testId);
649
+ if (!test) {
650
+ return res.status(404).json({ message: 'Test not found' });
651
+ }
652
+ if (!result.responses) {
653
+ result.state = 'inProgress';
654
+ result.responses = [];
655
+ }
656
+ result.responses.push({
657
+ questionId: idCurrentQuestion,
658
+ response: candidateResponse,
659
+ score: 0,
660
+ comment: ' '
661
+ });
662
+ await result.save();
663
+ const questionIndex = test.questions.indexOf(idCurrentQuestion);
664
+ if (questionIndex === test.questions.length - 1) {
665
+ emitter.emit(eventTypes.CORRECT_TEST, result);
666
+ result.state = 'finish';
667
+ await result.save();
668
+ }
669
+ res.status(200).json({ response: candidateResponse });
670
+ }
671
+ catch (err) {
672
+ console.error('error when sending result : ', err);
673
+ res.status(500).json({ message: 'Internal server error' });
674
+ }
675
+ });
676
+ // Corriger un test
677
+ this.post('/result/correct/:id', authenticatedOptions, async (req, res) => {
678
+ const { id } = req.params;
679
+ try {
680
+ const result = await TestResult.findById(id);
681
+ if (!result) {
682
+ return res.status(404).json({ message: 'Result not found' });
683
+ }
684
+ emitter.emit(eventTypes.CORRECT_TEST, result);
685
+ res.status(200).json({ message: 'Result in correction' });
686
+ }
687
+ catch (err) {
688
+ console.error('error when correcting result : ', err);
689
+ res.status(500).json({ message: 'Internal server error' });
690
+ }
691
+ });
692
+ // Calculer le score
693
+ this.put('/result/calculateScore/:id', authenticatedOptions, async (req, res) => {
694
+ const { id } = req.params;
695
+ try {
696
+ const result = await TestResult.findById(id);
697
+ if (!result) {
698
+ return res.status(404).json({ message: 'Result not found' });
699
+ }
700
+ result.state = 'finish';
701
+ let finalscore = 0;
702
+ for (const response of result.responses) {
703
+ const question = await TestQuestion.findById(response.questionId);
704
+ if (!question)
705
+ continue;
706
+ const score = await generateLiveMessage('correctQuestion', {
707
+ question: {
708
+ _id: question._id.toString(),
709
+ instruction: question.instruction,
710
+ possibleResponses: question.possibleResponses,
711
+ questionType: question.questionType,
712
+ maxScore: question.maxScore
713
+ },
714
+ result: {
715
+ responses: [{
716
+ questionId: response.questionId.toString(),
717
+ response: response.response
718
+ }]
719
+ }
720
+ }, true);
721
+ const parsedResult = JSON.parse(score);
722
+ finalscore += parsedResult.score;
723
+ response.score = parsedResult.score;
724
+ response.comment = parsedResult.comment;
725
+ }
726
+ result.score = finalscore;
727
+ await result.save();
728
+ res.status(200).json({ data: finalscore });
729
+ }
730
+ catch (err) {
731
+ console.error('error when calculate the score : ', err);
732
+ res.status(500).json({ message: 'Internal server error' });
733
+ }
734
+ });
735
+ // Obtenir le score maximum
736
+ this.get('/maxscore/:resultId', authenticatedOptions, async (req, res) => {
737
+ const { resultId } = req.params;
738
+ try {
739
+ const result = await TestResult.findById(resultId);
740
+ if (!result) {
741
+ return res.status(404).json({ message: 'Result not found' });
742
+ }
743
+ const test = await Test.findById(result.testId);
744
+ if (!test) {
745
+ return res.status(404).json({ message: 'Test not found' });
746
+ }
747
+ let maxScore = 0;
748
+ for (const questionId of test.questions) {
749
+ const question = await TestQuestion.findById(questionId);
750
+ if (question) {
751
+ maxScore += question.maxScore;
752
+ }
753
+ }
754
+ res.status(200).json({ data: maxScore });
755
+ }
756
+ catch (err) {
757
+ console.error('error when geting score : ', err);
758
+ res.status(500).json({ message: 'Internal server error' });
759
+ }
760
+ });
761
+ // Obtenir le score d'un résultat
762
+ this.get('/result/score/:id', authenticatedOptions, async (req, res) => {
763
+ const { id } = req.params;
764
+ try {
765
+ const result = await TestResult.findById(id);
766
+ if (!result) {
767
+ return res.status(404).json({ message: 'Result not found' });
768
+ }
769
+ res.status(200).json({ data: result.score });
770
+ }
771
+ catch (err) {
772
+ console.error('error when geting score : ', err);
773
+ res.status(500).json({ message: 'Internal server error' });
774
+ }
775
+ });
776
+ // Générer plusieurs questions pour un test
777
+ this.put('/test/generateQuestions/:id', authenticatedOptions, async (req, res) => {
778
+ const { id } = req.params;
779
+ const { numberOfQuestions, category } = req.body;
780
+ if (!numberOfQuestions || numberOfQuestions <= 0) {
781
+ return res.status(400).json({ message: 'Le nombre de questions doit être positif' });
782
+ }
783
+ try {
784
+ const test = await Test.findById(id);
785
+ if (!test) {
786
+ return res.status(404).json({ message: 'Test non trouvé' });
787
+ }
788
+ let categoriesToUse = [];
789
+ if (category && category !== 'ALL') {
790
+ const categoryInfo = test.categories.find(cat => cat.categoryId.toString() === category);
791
+ if (categoryInfo) {
792
+ categoriesToUse = [{
793
+ categoryId: categoryInfo.categoryId.toString(),
794
+ expertiseLevel: categoryInfo.expertiseLevel.toString()
795
+ }];
796
+ }
797
+ }
798
+ else {
799
+ // Si category est 'ALL' ou absent, on utilise toutes les catégories du test
800
+ categoriesToUse = test.categories.map(cat => ({
801
+ categoryId: cat.categoryId.toString(),
802
+ expertiseLevel: cat.expertiseLevel.toString()
803
+ }));
804
+ }
805
+ if (categoriesToUse.length === 0) {
806
+ return res.status(400).json({ message: 'Aucune catégorie disponible pour générer des questions' });
807
+ }
808
+ const questionsPerCategory = Math.ceil(numberOfQuestions / categoriesToUse.length);
809
+ const generatedQuestions = [];
810
+ for (const categoryInfo of categoriesToUse) {
811
+ const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
812
+ if (!categoryDoc)
813
+ continue;
814
+ const otherQuestionsIds = test.questions.map(question => question.questionId);
815
+ const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
816
+ for (let i = 0; i < questionsPerCategory; i++) {
817
+ const generatedQuestion = await generateLiveMessage('createQuestion', {
818
+ job: test.targetJob,
819
+ seniority: test.seniorityLevel,
820
+ category: categoryDoc.name,
821
+ questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
822
+ expertiseLevel: categoryInfo.expertiseLevel,
823
+ otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
824
+ }, true);
825
+ const question = new TestQuestion(JSON.parse(generatedQuestion));
826
+ await question.save();
827
+ generatedQuestions.push(question);
828
+ test.questions.push({ questionId: question._id, order: test.questions.length });
829
+ }
830
+ }
831
+ await test.save();
832
+ res.status(200).json({ message: 'Questions générées avec succès', questions: generatedQuestions, test });
833
+ }
834
+ catch (err) {
835
+ console.error('Erreur lors de la génération des questions : ', err);
836
+ res.status(500).json({ message: 'Erreur interne du serveur' });
837
+ }
838
+ });
839
+ // Lister tous les candidats invités à un test
840
+ this.get('/test/:testId/candidates', authenticatedOptions, async (req, res) => {
841
+ const { testId } = req.params;
842
+ const page = parseInt(req.query.page) || 1;
843
+ const limit = parseInt(req.query.limit) || 10;
844
+ const skip = (page - 1) * limit;
845
+ const search = req.query.search || '';
846
+ const state = req.query.state || 'all';
847
+ try {
848
+ const test = await Test.findById(testId);
849
+ if (!test) {
850
+ return res.status(404).json({ message: 'Test non trouvé' });
851
+ }
852
+ // Construction de la requête
853
+ const query = { testId };
854
+ if (state !== 'all') {
855
+ query.state = state;
856
+ }
857
+ // Recherche sur les candidats
858
+ if (search) {
859
+ const candidates = await Candidate.find({
860
+ $or: [
861
+ { firstName: { $regex: search, $options: 'i' } },
862
+ { lastName: { $regex: search, $options: 'i' } },
863
+ { email: { $regex: search, $options: 'i' } }
864
+ ]
865
+ });
866
+ const candidateIds = candidates.map(c => c._id);
867
+ query.candidateId = { $in: candidateIds };
868
+ }
869
+ const [results, total] = await Promise.all([
870
+ TestResult.find(query)
871
+ .sort({ invitationDate: -1 })
872
+ .skip(skip)
873
+ .limit(limit)
874
+ .exec(),
875
+ TestResult.countDocuments(query)
876
+ ]);
877
+ // Récupérer les données des candidats
878
+ const candidateIds = results.map(result => result.candidateId);
879
+ const candidates = await Candidate.find({ _id: { $in: candidateIds } });
880
+ const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
881
+ // Calculer le maxScore du test
882
+ let maxScore = 0;
883
+ if (test.questions && test.questions.length > 0) {
884
+ const questionIds = test.questions.map((q) => q.questionId || q);
885
+ const questions = await TestQuestion.find({ _id: { $in: questionIds } }).lean();
886
+ maxScore = questions.reduce((sum, q) => sum + (q.maxScore || 0), 0);
887
+ }
888
+ // Combiner les résultats avec les données des candidats
889
+ const resultsWithCandidates = results.map(result => {
890
+ const candidate = candidatesMap.get(result.candidateId.toString());
891
+ return {
892
+ ...result.toObject(),
893
+ candidate: candidate
894
+ ? {
895
+ firstName: candidate.firstName,
896
+ lastName: candidate.lastName,
897
+ email: candidate.email
898
+ }
899
+ : null,
900
+ maxScore
901
+ };
902
+ });
903
+ const totalPages = Math.ceil(total / limit);
904
+ return res.json({
905
+ data: resultsWithCandidates,
906
+ pagination: {
907
+ currentPage: page,
908
+ totalPages,
909
+ totalItems: total,
910
+ itemsPerPage: limit,
911
+ hasNextPage: page < totalPages,
912
+ hasPreviousPage: page > 1
913
+ }
914
+ });
915
+ }
916
+ catch (err) {
917
+ console.error('Erreur lors de la récupération des candidats : ', err);
918
+ res.status(500).json({ message: 'Erreur interne du serveur' });
919
+ }
920
+ });
921
+ // Renvoyer l'email d'invitation à un candidat
922
+ this.post('/reinvite/:resultId', authenticatedOptions, async (req, res) => {
923
+ const { resultId } = req.params;
924
+ try {
925
+ const result = await TestResult.findById(resultId);
926
+ if (!result) {
927
+ return res.status(404).json({ message: 'Result not found' });
928
+ }
929
+ // Récupérer l'email du candidat
930
+ const candidate = await Candidate.findById(result.candidateId);
931
+ if (!candidate) {
932
+ return res.status(404).json({ message: 'Candidate not found' });
933
+ }
934
+ // Récupérer les informations du test
935
+ const test = await Test.findById(result.testId);
936
+ if (!test) {
937
+ return res.status(404).json({ message: 'Test not found' });
938
+ }
939
+ const email = candidate.email;
940
+ const emailUser = process.env.EMAIL_USER_TURING;
941
+ const emailPassword = process.env.EMAIL_PASSWORD_TURING;
942
+ // Construire le lien d'invitation
943
+ const testLink = process.env.TEST_INVITATION_LINK || '';
944
+ // Envoyer l'email via l'event emitter
945
+ await emitter.emit(eventTypes.SEND_EMAIL, {
946
+ template: 'test-invitation-turing',
947
+ to: email,
948
+ from: emailUser,
949
+ subject: 'Vous êtes invité à passer un test technique - École de Turing',
950
+ emailUser,
951
+ emailPassword,
952
+ data: {
953
+ testLink
954
+ }
955
+ });
956
+ // Mettre à jour la date d'invitation
957
+ result.set('invitationDate', new Date());
958
+ await result.save();
959
+ res.status(200).json({ message: 'Invitation email sent successfully' });
960
+ }
961
+ catch (err) {
962
+ console.error('Error when resending invitation : ', err);
963
+ res.status(500).json({ message: 'Internal server error' });
964
+ }
965
+ });
966
+ // Correction manuelle d'une réponse à une question d'un testResult
967
+ this.put('/result/:testResultId/response/:questionId', authenticatedOptions, async (req, res) => {
968
+ try {
969
+ const { testResultId, questionId } = req.params;
970
+ const { score, comment } = req.body;
971
+ // Récupérer le résultat de test
972
+ const result = await TestResult.findById(testResultId);
973
+ if (!result) {
974
+ return res.status(404).json({ message: 'TestResult non trouvé' });
975
+ }
976
+ // Trouver la réponse à corriger
977
+ const response = (result.responses || []).find((r) => r.questionId.toString() === questionId);
978
+ if (!response) {
979
+ return res.status(404).json({ message: 'Réponse à cette question non trouvée dans ce testResult' });
980
+ }
981
+ // Récupérer la question pour vérifier le maxScore
982
+ const question = await TestQuestion.findById(questionId);
983
+ if (!question) {
984
+ return res.status(404).json({ message: 'Question non trouvée' });
985
+ }
986
+ const maxScore = question.maxScore;
987
+ if (typeof score === 'number' && score > maxScore) {
988
+ return res.status(400).json({ message: `Le score ne peut pas dépasser le maximum autorisé (${maxScore}) pour cette question.` });
989
+ }
990
+ // Surcharger le score et le commentaire
991
+ if (typeof score === 'number')
992
+ response.score = score;
993
+ if (typeof comment === 'string')
994
+ response.comment = comment;
995
+ // Recalculer le score global
996
+ result.score = (result.responses || []).reduce((sum, r) => sum + (r.score || 0), 0);
997
+ await result.save();
998
+ return res.status(200).json({
999
+ message: 'Correction manuelle enregistrée',
1000
+ response,
1001
+ scoreGlobal: result.score
1002
+ });
1003
+ }
1004
+ catch (err) {
1005
+ console.error('Erreur lors de la correction manuelle :', err);
1006
+ res.status(500).json({ message: 'Erreur interne du serveur' });
1007
+ }
1008
+ });
1009
+ }
1010
+ }
1011
+ const router = new ExamsRouter();
1012
+ export default router;