@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.
- package/README.md +135 -0
- package/dist/bin/www.d.ts +2 -0
- package/dist/bin/www.js +9 -0
- package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +10 -0
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +68 -0
- package/dist/modules/edrm-exams/lib/openai.d.ts +36 -0
- package/dist/modules/edrm-exams/lib/openai.js +82 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.js +85 -0
- package/dist/modules/edrm-exams/models/candidate.models.d.ts +13 -0
- package/dist/modules/edrm-exams/models/candidate.models.js +59 -0
- package/dist/modules/edrm-exams/models/company.model.d.ts +8 -0
- package/dist/modules/edrm-exams/models/company.model.js +34 -0
- package/dist/modules/edrm-exams/models/test-category.models.d.ts +7 -0
- package/dist/modules/edrm-exams/models/test-category.models.js +29 -0
- package/dist/modules/edrm-exams/models/test-question.model.d.ts +25 -0
- package/dist/modules/edrm-exams/models/test-question.model.js +70 -0
- package/dist/modules/edrm-exams/models/test-result.model.d.ts +26 -0
- package/dist/modules/edrm-exams/models/test-result.model.js +70 -0
- package/dist/modules/edrm-exams/models/test.model.d.ts +52 -0
- package/dist/modules/edrm-exams/models/test.model.js +123 -0
- package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
- package/dist/modules/edrm-exams/models/user.model.js +64 -0
- package/dist/modules/edrm-exams/routes/company.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/company.router.js +108 -0
- package/dist/modules/edrm-exams/routes/exams-candidate.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/exams-candidate.router.js +299 -0
- package/dist/modules/edrm-exams/routes/exams.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +1012 -0
- package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/result.router.js +314 -0
- package/dist/modules/edrm-exams/routes/user.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/user.router.js +96 -0
- 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;
|