@programisto/edrm-storage 0.3.1
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 +13 -0
- package/dist/modules/edrm-exams/lib/openai/correctQuestion.txt +9 -0
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +6 -0
- package/dist/modules/edrm-exams/lib/openai.d.ts +37 -0
- package/dist/modules/edrm-exams/lib/openai.js +135 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.d.ts +2 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.js +167 -0
- package/dist/modules/edrm-exams/models/candidate.model.d.ts +21 -0
- package/dist/modules/edrm-exams/models/candidate.model.js +75 -0
- package/dist/modules/edrm-exams/models/candidate.models.d.ts +21 -0
- package/dist/modules/edrm-exams/models/candidate.models.js +75 -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/contact.model.d.ts +14 -0
- package/dist/modules/edrm-exams/models/contact.model.js +60 -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-job.model.d.ts +7 -0
- package/dist/modules/edrm-exams/models/test-job.model.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 +47 -0
- package/dist/modules/edrm-exams/models/test.model.js +133 -0
- package/dist/modules/edrm-exams/models/user.model.d.ts +18 -0
- package/dist/modules/edrm-exams/models/user.model.js +73 -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 +448 -0
- package/dist/modules/edrm-exams/routes/exams.router.d.ts +8 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +1343 -0
- package/dist/modules/edrm-exams/routes/result.router.d.ts +7 -0
- package/dist/modules/edrm-exams/routes/result.router.js +370 -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/dist/modules/edrm-storage/config/edrm-storage.config.d.ts +29 -0
- package/dist/modules/edrm-storage/config/edrm-storage.config.js +31 -0
- package/dist/modules/edrm-storage/config/environment.example.d.ts +54 -0
- package/dist/modules/edrm-storage/config/environment.example.js +130 -0
- package/dist/modules/edrm-storage/examples/usage.example.d.ts +52 -0
- package/dist/modules/edrm-storage/examples/usage.example.js +156 -0
- package/dist/modules/edrm-storage/index.d.ts +5 -0
- package/dist/modules/edrm-storage/index.js +8 -0
- package/dist/modules/edrm-storage/integration/edrm-storage-integration.d.ts +53 -0
- package/dist/modules/edrm-storage/integration/edrm-storage-integration.js +132 -0
- package/dist/modules/edrm-storage/interfaces/storage-provider.interface.d.ts +35 -0
- package/dist/modules/edrm-storage/interfaces/storage-provider.interface.js +1 -0
- package/dist/modules/edrm-storage/migrations/edrm-storage.migration.d.ts +6 -0
- package/dist/modules/edrm-storage/migrations/edrm-storage.migration.js +151 -0
- package/dist/modules/edrm-storage/models/file.model.d.ts +78 -0
- package/dist/modules/edrm-storage/models/file.model.js +190 -0
- package/dist/modules/edrm-storage/providers/s3-storage.provider.d.ts +18 -0
- package/dist/modules/edrm-storage/providers/s3-storage.provider.js +95 -0
- package/dist/modules/edrm-storage/routes/edrm-storage.router.d.ts +8 -0
- package/dist/modules/edrm-storage/routes/edrm-storage.router.js +155 -0
- package/dist/modules/edrm-storage/scripts/quick-start.d.ts +7 -0
- package/dist/modules/edrm-storage/scripts/quick-start.js +114 -0
- package/dist/modules/edrm-storage/services/edrm-storage.service.d.ts +29 -0
- package/dist/modules/edrm-storage/services/edrm-storage.service.js +188 -0
- package/dist/modules/edrm-storage/tests/edrm-storage.service.test.d.ts +1 -0
- package/dist/modules/edrm-storage/tests/edrm-storage.service.test.js +143 -0
- package/dist/modules/edrm-storage/tests/integration.test.d.ts +1 -0
- package/dist/modules/edrm-storage/tests/integration.test.js +141 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter as emitter, enduranceEventTypes as eventTypes } from '@programisto/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 TestJob from '../models/test-job.model.js';
|
|
7
|
+
import Candidate from '../models/candidate.model.js';
|
|
8
|
+
import ContactModel from '../models/contact.model.js';
|
|
9
|
+
import { generateLiveMessage, generateLiveMessageAssistant } from '../lib/openai.js';
|
|
10
|
+
// Fonction utilitaire pour récupérer le nom du job
|
|
11
|
+
async function getJobName(targetJob) {
|
|
12
|
+
// Si c'est déjà une string (ancien format), on la retourne directement
|
|
13
|
+
if (typeof targetJob === 'string') {
|
|
14
|
+
return targetJob;
|
|
15
|
+
}
|
|
16
|
+
// Si c'est un ObjectId, on récupère le job
|
|
17
|
+
if (targetJob && typeof targetJob === 'object' && targetJob._id) {
|
|
18
|
+
const job = await TestJob.findById(targetJob._id);
|
|
19
|
+
return job ? job.name : 'Job inconnu';
|
|
20
|
+
}
|
|
21
|
+
// Si c'est juste un ObjectId
|
|
22
|
+
if (targetJob && typeof targetJob === 'object' && targetJob.toString) {
|
|
23
|
+
const job = await TestJob.findById(targetJob);
|
|
24
|
+
return job ? job.name : 'Job inconnu';
|
|
25
|
+
}
|
|
26
|
+
return 'Job inconnu';
|
|
27
|
+
}
|
|
28
|
+
// Fonction pour migrer automatiquement un test si nécessaire
|
|
29
|
+
async function migrateTestIfNeeded(test) {
|
|
30
|
+
if (typeof test.targetJob === 'string') {
|
|
31
|
+
await test.migrateTargetJob();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class ExamsRouter extends EnduranceRouter {
|
|
35
|
+
constructor() {
|
|
36
|
+
super(EnduranceAuthMiddleware.getInstance());
|
|
37
|
+
}
|
|
38
|
+
async generateAndSaveQuestion(test, categoryInfo, useAssistant = false) {
|
|
39
|
+
try {
|
|
40
|
+
const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
|
|
41
|
+
if (!categoryDoc) {
|
|
42
|
+
console.error('Catégorie non trouvée:', categoryInfo.categoryId);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Récupérer les questions existantes pour éviter les doublons
|
|
46
|
+
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
47
|
+
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
48
|
+
const jobName = await getJobName(test.targetJob);
|
|
49
|
+
const questionParams = {
|
|
50
|
+
job: jobName,
|
|
51
|
+
seniority: test.seniorityLevel,
|
|
52
|
+
category: categoryDoc.name,
|
|
53
|
+
questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
|
|
54
|
+
expertiseLevel: categoryInfo.expertiseLevel,
|
|
55
|
+
otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
|
|
56
|
+
};
|
|
57
|
+
let generatedQuestion;
|
|
58
|
+
if (useAssistant) {
|
|
59
|
+
generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', questionParams, true);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
generatedQuestion = await generateLiveMessage('createQuestion', questionParams, true);
|
|
63
|
+
}
|
|
64
|
+
if (generatedQuestion === 'Brain freezed, I cannot generate a live message right now.') {
|
|
65
|
+
console.error('Échec de génération de question pour la catégorie:', categoryDoc.name);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const question = new TestQuestion(JSON.parse(generatedQuestion));
|
|
69
|
+
await question.save();
|
|
70
|
+
// Ajouter la question au test et sauvegarder immédiatement
|
|
71
|
+
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
72
|
+
await test.save();
|
|
73
|
+
return question;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('Erreur lors de la génération/sauvegarde de la question:', error);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
setupRoutes() {
|
|
81
|
+
const authenticatedOptions = {
|
|
82
|
+
requireAuth: false,
|
|
83
|
+
permissions: []
|
|
84
|
+
};
|
|
85
|
+
// Créer une catégorie
|
|
86
|
+
this.post('/categories', authenticatedOptions, async (req, res) => {
|
|
87
|
+
const { name } = req.body;
|
|
88
|
+
if (!name) {
|
|
89
|
+
return res.status(400).json({ message: 'Error, all params are required' });
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const newCategory = new TestCategory({ name });
|
|
93
|
+
await newCategory.save();
|
|
94
|
+
res.status(201).json({ message: 'category created with sucess', category: newCategory });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error('error when creating category : ', err);
|
|
98
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// Lister toutes les catégories
|
|
102
|
+
this.get('/categories', authenticatedOptions, async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const categories = await TestCategory.find();
|
|
105
|
+
res.status(200).json({ array: categories });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error('error when creating category : ', err);
|
|
109
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// Obtenir une catégorie par son ID
|
|
113
|
+
this.get('/categorie/:id', authenticatedOptions, async (req, res) => {
|
|
114
|
+
const { id } = req.params;
|
|
115
|
+
try {
|
|
116
|
+
const category = await TestCategory.findById(id);
|
|
117
|
+
if (!category) {
|
|
118
|
+
return res.status(404).json({ message: 'no category founded with this id' });
|
|
119
|
+
}
|
|
120
|
+
res.status(200).json({ array: category });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error('error when creating category : ', err);
|
|
124
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Créer un job type
|
|
128
|
+
this.post('/jobs', authenticatedOptions, async (req, res) => {
|
|
129
|
+
const { name } = req.body;
|
|
130
|
+
if (!name) {
|
|
131
|
+
return res.status(400).json({ message: 'Error, name is required' });
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const newJob = new TestJob({ name });
|
|
135
|
+
await newJob.save();
|
|
136
|
+
res.status(201).json({ message: 'job created with success', job: newJob });
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.error('error when creating job : ', err);
|
|
140
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Lister tous les jobs
|
|
144
|
+
this.get('/jobs', authenticatedOptions, async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const jobs = await TestJob.find();
|
|
147
|
+
res.status(200).json({ array: jobs });
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.error('error when getting jobs : ', err);
|
|
151
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// Obtenir un job par son ID
|
|
155
|
+
this.get('/jobs/:id', authenticatedOptions, async (req, res) => {
|
|
156
|
+
const { id } = req.params;
|
|
157
|
+
try {
|
|
158
|
+
const job = await TestJob.findById(id);
|
|
159
|
+
if (!job) {
|
|
160
|
+
return res.status(404).json({ message: 'no job founded with this id' });
|
|
161
|
+
}
|
|
162
|
+
res.status(200).json({ array: job });
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.error('error when getting job : ', err);
|
|
166
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Migrer tous les tests avec l'ancien format targetJob
|
|
170
|
+
this.post('/migrate-targetjobs', authenticatedOptions, async (req, res) => {
|
|
171
|
+
try {
|
|
172
|
+
const tests = await Test.find();
|
|
173
|
+
let migratedCount = 0;
|
|
174
|
+
let errorCount = 0;
|
|
175
|
+
for (const test of tests) {
|
|
176
|
+
try {
|
|
177
|
+
// Vérifier si le test a besoin de migration
|
|
178
|
+
if (typeof test.targetJob === 'string') {
|
|
179
|
+
await test.migrateTargetJob();
|
|
180
|
+
migratedCount++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
console.error(`Erreur lors de la migration du test ${test._id}:`, error);
|
|
185
|
+
errorCount++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
res.status(200).json({
|
|
189
|
+
message: `Migration terminée. ${migratedCount} tests migrés, ${errorCount} erreurs.`,
|
|
190
|
+
migratedCount,
|
|
191
|
+
errorCount
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error('Erreur lors de la migration :', err);
|
|
196
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// Créer un test
|
|
200
|
+
this.post('/test', authenticatedOptions, async (req, res) => {
|
|
201
|
+
const { title, description, targetJob, seniorityLevel, categories, state = 'draft' } = req.body;
|
|
202
|
+
const user = req.user;
|
|
203
|
+
if (!title || !targetJob || !seniorityLevel) {
|
|
204
|
+
return res.status(400).json({ message: 'Error, all params are required' });
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const companyId = user?.companyId;
|
|
208
|
+
const userId = user?._id;
|
|
209
|
+
// Traiter le targetJob - si c'est une string, on cherche ou crée le TestJob
|
|
210
|
+
let targetJobId;
|
|
211
|
+
if (typeof targetJob === 'string') {
|
|
212
|
+
let existingJob = await TestJob.findOne({ name: targetJob });
|
|
213
|
+
if (!existingJob) {
|
|
214
|
+
existingJob = new TestJob({ name: targetJob });
|
|
215
|
+
await existingJob.save();
|
|
216
|
+
}
|
|
217
|
+
targetJobId = existingJob._id;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
targetJobId = targetJob;
|
|
221
|
+
}
|
|
222
|
+
const processedCategories = await Promise.all(categories?.map(async (category) => {
|
|
223
|
+
let existingCategory = await TestCategory.findOne({ name: category.name });
|
|
224
|
+
if (!existingCategory) {
|
|
225
|
+
existingCategory = await TestCategory.create({ name: category.name });
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
categoryId: existingCategory._id,
|
|
229
|
+
expertiseLevel: category.expertiseLevel
|
|
230
|
+
};
|
|
231
|
+
}) || []);
|
|
232
|
+
const newTest = new Test({
|
|
233
|
+
companyId,
|
|
234
|
+
userId,
|
|
235
|
+
title,
|
|
236
|
+
description,
|
|
237
|
+
targetJob: targetJobId,
|
|
238
|
+
seniorityLevel,
|
|
239
|
+
state,
|
|
240
|
+
categories: processedCategories
|
|
241
|
+
});
|
|
242
|
+
await newTest.save();
|
|
243
|
+
res.status(201).json({ message: 'test created with sucess', data: newTest });
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error('error when creating test : ', err);
|
|
247
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
// Modifier un test
|
|
251
|
+
this.put('/test/:id', authenticatedOptions, async (req, res) => {
|
|
252
|
+
const { id } = req.params;
|
|
253
|
+
const { title, description, targetJob, seniorityLevel, categories, state, questions } = req.body;
|
|
254
|
+
try {
|
|
255
|
+
const test = await Test.findById(id);
|
|
256
|
+
if (!test) {
|
|
257
|
+
return res.status(404).json({ message: 'Test non trouvé' });
|
|
258
|
+
}
|
|
259
|
+
if (title)
|
|
260
|
+
test.title = title;
|
|
261
|
+
if (description)
|
|
262
|
+
test.description = description;
|
|
263
|
+
if (targetJob) {
|
|
264
|
+
// Traiter le targetJob - si c'est une string, on cherche ou crée le TestJob
|
|
265
|
+
if (typeof targetJob === 'string') {
|
|
266
|
+
let existingJob = await TestJob.findOne({ name: targetJob });
|
|
267
|
+
if (!existingJob) {
|
|
268
|
+
existingJob = new TestJob({ name: targetJob });
|
|
269
|
+
await existingJob.save();
|
|
270
|
+
}
|
|
271
|
+
test.targetJob = existingJob._id;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
test.targetJob = targetJob;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (seniorityLevel)
|
|
278
|
+
test.seniorityLevel = seniorityLevel;
|
|
279
|
+
if (state)
|
|
280
|
+
test.state = state;
|
|
281
|
+
if (categories) {
|
|
282
|
+
const processedCategories = await Promise.all(categories.map(async (category) => {
|
|
283
|
+
let existingCategory = await TestCategory.findOne({ name: category.name });
|
|
284
|
+
if (!existingCategory) {
|
|
285
|
+
existingCategory = await TestCategory.create({ name: category.name });
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
categoryId: existingCategory._id,
|
|
289
|
+
expertiseLevel: category.expertiseLevel
|
|
290
|
+
};
|
|
291
|
+
}));
|
|
292
|
+
test.categories = processedCategories;
|
|
293
|
+
}
|
|
294
|
+
if (questions) {
|
|
295
|
+
// Vérifier que toutes les questions existent
|
|
296
|
+
const questionIds = questions.map((q) => q.questionId);
|
|
297
|
+
const existingQuestions = await TestQuestion.find({ _id: { $in: questionIds } });
|
|
298
|
+
if (existingQuestions.length !== questionIds.length) {
|
|
299
|
+
return res.status(400).json({
|
|
300
|
+
message: 'Certaines questions spécifiées n\'existent pas',
|
|
301
|
+
providedQuestions: questionIds.length,
|
|
302
|
+
foundQuestions: existingQuestions.length
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// Mettre à jour les questions avec leur ordre
|
|
306
|
+
test.questions = questions.map((q) => ({
|
|
307
|
+
questionId: q.questionId,
|
|
308
|
+
order: q.order || 0
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
await test.save();
|
|
312
|
+
res.status(200).json({ message: 'Test modifié avec succès', data: test });
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
console.error('Erreur lors de la modification du test : ', err);
|
|
316
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
// Supprimer un test
|
|
320
|
+
this.delete('/test/:id', authenticatedOptions, async (req, res) => {
|
|
321
|
+
const { id } = req.params;
|
|
322
|
+
try {
|
|
323
|
+
const test = await Test.findById(id);
|
|
324
|
+
if (!test) {
|
|
325
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
326
|
+
}
|
|
327
|
+
for (let i = 0; i < test.questions.length; i++) {
|
|
328
|
+
await TestQuestion.findByIdAndDelete(test.questions[i].questionId);
|
|
329
|
+
}
|
|
330
|
+
await TestResult.deleteMany({ testId: id });
|
|
331
|
+
await Test.findByIdAndDelete(id);
|
|
332
|
+
res.status(200).json({ message: 'test deleted with sucess' });
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
console.error('error when deleting user : ', err);
|
|
336
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
// Obtenir un test par son ID
|
|
340
|
+
this.get('/test/:id', authenticatedOptions, async (req, res) => {
|
|
341
|
+
const { id } = req.params;
|
|
342
|
+
try {
|
|
343
|
+
const test = await Test.findById(id);
|
|
344
|
+
if (!test) {
|
|
345
|
+
return res.status(404).json({ message: 'no test founded with this id' });
|
|
346
|
+
}
|
|
347
|
+
// Migration automatique si nécessaire
|
|
348
|
+
await migrateTestIfNeeded(test);
|
|
349
|
+
const questions = [];
|
|
350
|
+
for (const questionRef of test.questions) {
|
|
351
|
+
console.log(questionRef);
|
|
352
|
+
const question = await TestQuestion.findById(questionRef.questionId);
|
|
353
|
+
if (question) {
|
|
354
|
+
console.log(question);
|
|
355
|
+
questions.push(question);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Récupérer le nom du job pour l'affichage
|
|
359
|
+
const testObj = test.toObject();
|
|
360
|
+
testObj.targetJobName = await getJobName(testObj.targetJob);
|
|
361
|
+
res.status(200).json({ test: testObj, questions });
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
console.error('error when geting test : ', err);
|
|
365
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
// Lister tous les tests
|
|
369
|
+
this.get('/', authenticatedOptions, async (req, res) => {
|
|
370
|
+
try {
|
|
371
|
+
const page = parseInt(req.query.page) || 1;
|
|
372
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
373
|
+
const skip = (page - 1) * limit;
|
|
374
|
+
const search = req.query.search || '';
|
|
375
|
+
const targetJob = req.query.targetJob || 'all';
|
|
376
|
+
const seniorityLevel = req.query.seniorityLevel || 'all';
|
|
377
|
+
const state = req.query.state || 'all';
|
|
378
|
+
const sortBy = req.query.sortBy || 'updatedAt';
|
|
379
|
+
const sortOrder = req.query.sortOrder || 'desc';
|
|
380
|
+
// Construction de la requête de recherche
|
|
381
|
+
const query = {};
|
|
382
|
+
// Filtres
|
|
383
|
+
if (targetJob !== 'all') {
|
|
384
|
+
// Si on filtre par targetJob, on cherche d'abord le TestJob correspondant
|
|
385
|
+
const jobType = await TestJob.findOne({ name: targetJob });
|
|
386
|
+
if (jobType) {
|
|
387
|
+
query.targetJob = jobType._id;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// Si le job n'existe pas, on ne retourne aucun résultat
|
|
391
|
+
query.targetJob = null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (seniorityLevel !== 'all') {
|
|
395
|
+
query.seniorityLevel = seniorityLevel;
|
|
396
|
+
}
|
|
397
|
+
if (state !== 'all') {
|
|
398
|
+
query.state = state;
|
|
399
|
+
}
|
|
400
|
+
// Recherche sur testName et targetJob
|
|
401
|
+
if (search) {
|
|
402
|
+
// Pour la recherche sur targetJob, on cherche d'abord les jobs qui correspondent
|
|
403
|
+
const matchingJobs = await TestJob.find({ name: { $regex: search, $options: 'i' } });
|
|
404
|
+
const jobIds = matchingJobs.map(job => job._id);
|
|
405
|
+
query.$or = [
|
|
406
|
+
{ title: { $regex: search, $options: 'i' } },
|
|
407
|
+
{ description: { $regex: search, $options: 'i' } },
|
|
408
|
+
{ targetJob: { $in: jobIds } },
|
|
409
|
+
{ seniorityLevel: { $regex: search, $options: 'i' } }
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
// Construction du tri
|
|
413
|
+
const allowedSortFields = ['testName', 'targetJob', 'seniorityLevel', 'updatedAt'];
|
|
414
|
+
const sortField = allowedSortFields.includes(sortBy) ? sortBy : 'updatedAt';
|
|
415
|
+
const sortOptions = {
|
|
416
|
+
[sortField]: sortOrder === 'asc' ? 1 : -1
|
|
417
|
+
};
|
|
418
|
+
const [tests, total] = await Promise.all([
|
|
419
|
+
Test.find(query)
|
|
420
|
+
.sort(sortOptions)
|
|
421
|
+
.skip(skip)
|
|
422
|
+
.limit(limit)
|
|
423
|
+
.exec(),
|
|
424
|
+
Test.countDocuments(query)
|
|
425
|
+
]);
|
|
426
|
+
// Récupérer les noms des catégories et des jobs pour chaque test
|
|
427
|
+
const testsWithCategories = await Promise.all(tests.map(async (test) => {
|
|
428
|
+
// Migration automatique si nécessaire
|
|
429
|
+
await migrateTestIfNeeded(test);
|
|
430
|
+
const testObj = test.toObject();
|
|
431
|
+
// Récupérer le nom du job
|
|
432
|
+
testObj.targetJobName = await getJobName(testObj.targetJob);
|
|
433
|
+
if (testObj.categories && testObj.categories.length > 0) {
|
|
434
|
+
const categoriesWithNames = await Promise.all(testObj.categories.map(async (category) => {
|
|
435
|
+
const categoryDoc = await TestCategory.findById(category.categoryId);
|
|
436
|
+
return {
|
|
437
|
+
...category,
|
|
438
|
+
categoryName: categoryDoc?.name || 'Catégorie inconnue'
|
|
439
|
+
};
|
|
440
|
+
}));
|
|
441
|
+
testObj.categories = categoriesWithNames;
|
|
442
|
+
}
|
|
443
|
+
return testObj;
|
|
444
|
+
}));
|
|
445
|
+
const totalPages = Math.ceil(total / limit);
|
|
446
|
+
return res.json({
|
|
447
|
+
data: testsWithCategories,
|
|
448
|
+
pagination: {
|
|
449
|
+
currentPage: page,
|
|
450
|
+
totalPages,
|
|
451
|
+
totalItems: total,
|
|
452
|
+
itemsPerPage: limit,
|
|
453
|
+
hasNextPage: page < totalPages,
|
|
454
|
+
hasPreviousPage: page > 1
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
console.error('error when geting tests : ', err);
|
|
460
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
// Supprimer une catégorie d'un test
|
|
464
|
+
this.delete('/test/removeCategory/:testId', authenticatedOptions, async (req, res) => {
|
|
465
|
+
const { testId } = req.params;
|
|
466
|
+
const { categoryName } = req.body;
|
|
467
|
+
try {
|
|
468
|
+
const category = await TestCategory.findOne({ name: categoryName });
|
|
469
|
+
if (!category)
|
|
470
|
+
return res.status(404).json({ message: 'Category not found' });
|
|
471
|
+
const test = await Test.findByIdAndUpdate(testId, { $pull: { categories: { categoryId: category._id } } }, { new: true });
|
|
472
|
+
if (!test)
|
|
473
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
474
|
+
res.status(200).json({ message: 'Category removed', test });
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
console.error('Error when removing category from test:', err);
|
|
478
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
// Ajouter une catégorie à un test
|
|
482
|
+
this.put('/test/addCategory/:testId', authenticatedOptions, async (req, res) => {
|
|
483
|
+
const { testId } = req.params;
|
|
484
|
+
const { categoryName, expertiseLevel } = req.body;
|
|
485
|
+
try {
|
|
486
|
+
let category = await TestCategory.findOne({ name: categoryName });
|
|
487
|
+
if (!category) {
|
|
488
|
+
category = new TestCategory({ name: categoryName });
|
|
489
|
+
await category.save();
|
|
490
|
+
}
|
|
491
|
+
const test = await Test.findById(testId);
|
|
492
|
+
if (!test) {
|
|
493
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
494
|
+
}
|
|
495
|
+
const categoryExists = test.categories.some(cat => cat.categoryId.equals(category._id));
|
|
496
|
+
if (categoryExists) {
|
|
497
|
+
return res.status(200).json({ message: 'Category already exists in the test' });
|
|
498
|
+
}
|
|
499
|
+
test.categories.push({ categoryId: category._id, expertiseLevel });
|
|
500
|
+
await test.save();
|
|
501
|
+
res.status(200).json({ message: 'Category added successfully', data: test });
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
console.error('Error when adding category to test:', err);
|
|
505
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
// Obtenir une question par son ID
|
|
509
|
+
this.get('/test/question/:questionId', authenticatedOptions, async (req, res) => {
|
|
510
|
+
const { questionId } = req.params;
|
|
511
|
+
const question = await TestQuestion.findById(questionId);
|
|
512
|
+
if (!question) {
|
|
513
|
+
return res.status(404).json({ message: 'no question founded with this id' });
|
|
514
|
+
}
|
|
515
|
+
res.status(200).json({ data: question });
|
|
516
|
+
});
|
|
517
|
+
// Obtenir toutes les questions d'un test
|
|
518
|
+
this.get('/test/questions/:testId', authenticatedOptions, async (req, res) => {
|
|
519
|
+
const { testId } = req.params;
|
|
520
|
+
try {
|
|
521
|
+
const test = await Test.findById(testId);
|
|
522
|
+
if (!test) {
|
|
523
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
524
|
+
}
|
|
525
|
+
const questions = [];
|
|
526
|
+
for (const questionId of test.questions) {
|
|
527
|
+
const question = await TestQuestion.findById(questionId);
|
|
528
|
+
if (question) {
|
|
529
|
+
questions.push(question);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
res.status(200).json({ array: questions });
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
console.error('Error when getting question:', err);
|
|
536
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
// Supprimer une question d'un test
|
|
540
|
+
this.delete('/test/question/:testId/:questionId', authenticatedOptions, async (req, res) => {
|
|
541
|
+
const { testId, questionId } = req.params;
|
|
542
|
+
const question = await TestQuestion.findByIdAndDelete(questionId);
|
|
543
|
+
const test = await Test.findById(testId);
|
|
544
|
+
if (!question) {
|
|
545
|
+
return res.status(404).json({ message: 'no question founded with this id' });
|
|
546
|
+
}
|
|
547
|
+
if (!test) {
|
|
548
|
+
return res.status(404).json({ message: 'no test founded with this id' });
|
|
549
|
+
}
|
|
550
|
+
// Supprimer la question du tableau questions en filtrant par questionId
|
|
551
|
+
test.questions = test.questions.filter(q => q.questionId.toString() !== questionId);
|
|
552
|
+
// Recalculer les ordres pour que ça se suive
|
|
553
|
+
test.questions.forEach((q, index) => {
|
|
554
|
+
q.order = index + 1;
|
|
555
|
+
});
|
|
556
|
+
await test.save();
|
|
557
|
+
res.status(200).json({ message: 'question deleted with sucess' });
|
|
558
|
+
});
|
|
559
|
+
// Supprimer toutes les questions d'un test
|
|
560
|
+
this.delete('/test/questions/:testId', authenticatedOptions, async (req, res) => {
|
|
561
|
+
const { testId } = req.params;
|
|
562
|
+
const test = await Test.findById(testId);
|
|
563
|
+
if (!test) {
|
|
564
|
+
return res.status(404).json({ message: 'no test founded with this id' });
|
|
565
|
+
}
|
|
566
|
+
for (const questionId of test.questions) {
|
|
567
|
+
await TestQuestion.findByIdAndDelete(questionId);
|
|
568
|
+
}
|
|
569
|
+
test.questions = [];
|
|
570
|
+
await test.save();
|
|
571
|
+
res.status(200).json({ message: 'questions deleted with sucess' });
|
|
572
|
+
});
|
|
573
|
+
// Modifier une question
|
|
574
|
+
this.put('/test/modifyQuestion/:id', authenticatedOptions, async (req, res) => {
|
|
575
|
+
const { id } = req.params;
|
|
576
|
+
const { instruction, maxScore, time, possibleResponses, textType } = req.body;
|
|
577
|
+
try {
|
|
578
|
+
const question = await TestQuestion.findById(id);
|
|
579
|
+
if (!question) {
|
|
580
|
+
return res.status(404).json({ message: 'no question founded with this id' });
|
|
581
|
+
}
|
|
582
|
+
if (instruction) {
|
|
583
|
+
question.instruction = instruction;
|
|
584
|
+
}
|
|
585
|
+
if (maxScore) {
|
|
586
|
+
question.maxScore = maxScore;
|
|
587
|
+
}
|
|
588
|
+
if (time) {
|
|
589
|
+
question.time = time;
|
|
590
|
+
}
|
|
591
|
+
if (textType) {
|
|
592
|
+
question.textType = textType;
|
|
593
|
+
}
|
|
594
|
+
if (possibleResponses) {
|
|
595
|
+
question.possibleResponses = possibleResponses;
|
|
596
|
+
}
|
|
597
|
+
await question.save();
|
|
598
|
+
res.status(200).json({ message: 'question modified with sucess' });
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
console.error('error when modify question : ', err);
|
|
602
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
// Ajouter une question à un test
|
|
606
|
+
this.put('/test/addCustomQuestion/:id', authenticatedOptions, async (req, res) => {
|
|
607
|
+
const { id } = req.params;
|
|
608
|
+
const { questionType, instruction, maxScore, time } = req.body;
|
|
609
|
+
try {
|
|
610
|
+
const test = await Test.findById(id);
|
|
611
|
+
if (!test) {
|
|
612
|
+
return res.status(404).json({ message: 'no test founded with this id' });
|
|
613
|
+
}
|
|
614
|
+
const question = new TestQuestion({
|
|
615
|
+
questionType,
|
|
616
|
+
instruction,
|
|
617
|
+
maxScore,
|
|
618
|
+
time
|
|
619
|
+
});
|
|
620
|
+
await question.save();
|
|
621
|
+
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
622
|
+
await test.save();
|
|
623
|
+
res.status(200).json({ message: 'question added in test', test });
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
console.error('error when add question in test : ', err);
|
|
627
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
// Ajouter une question à un test
|
|
631
|
+
this.put('/test/addQuestion/:id', authenticatedOptions, async (req, res) => {
|
|
632
|
+
const { id } = req.params;
|
|
633
|
+
const { questionType, category, expertiseLevel } = req.body;
|
|
634
|
+
try {
|
|
635
|
+
const test = await Test.findById(id);
|
|
636
|
+
if (!test) {
|
|
637
|
+
return res.status(404).json({ message: 'no test founded with this id' });
|
|
638
|
+
}
|
|
639
|
+
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
640
|
+
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
641
|
+
const jobName = await getJobName(test.targetJob);
|
|
642
|
+
const generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', {
|
|
643
|
+
job: jobName,
|
|
644
|
+
seniority: test.seniorityLevel,
|
|
645
|
+
questionType,
|
|
646
|
+
category,
|
|
647
|
+
expertiseLevel,
|
|
648
|
+
otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
|
|
649
|
+
}, true);
|
|
650
|
+
const question = new TestQuestion(JSON.parse(generatedQuestion));
|
|
651
|
+
await question.save();
|
|
652
|
+
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
653
|
+
await test.save();
|
|
654
|
+
res.status(200).json({ message: 'question added in test', test });
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
console.error('error when add question in test : ', err);
|
|
658
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
// Mélanger les questions d'un test
|
|
662
|
+
this.get('/test/shuffle/:testId', authenticatedOptions, async (req, res) => {
|
|
663
|
+
const { testId } = req.params;
|
|
664
|
+
try {
|
|
665
|
+
const test = await Test.findById(testId);
|
|
666
|
+
if (!test) {
|
|
667
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
668
|
+
}
|
|
669
|
+
for (let i = test.questions.length - 1; i > 0; i--) {
|
|
670
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
671
|
+
[test.questions[i], test.questions[j]] = [test.questions[j], test.questions[i]];
|
|
672
|
+
}
|
|
673
|
+
await test.save();
|
|
674
|
+
res.status(200).json({ message: 'Questions shuffled', test });
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
console.error('Error when shuffling questions:', err);
|
|
678
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
// Ajouter un texte d'invitation à un test
|
|
682
|
+
this.put('/test/addInvitationText/:id', authenticatedOptions, async (req, res) => {
|
|
683
|
+
const { id } = req.params;
|
|
684
|
+
const { invitationText } = req.body;
|
|
685
|
+
try {
|
|
686
|
+
const test = await Test.findById(id);
|
|
687
|
+
if (!test) {
|
|
688
|
+
return res.status(404).json({ message: 'no test founded with this id' });
|
|
689
|
+
}
|
|
690
|
+
test.invitationText = invitationText;
|
|
691
|
+
await test.save();
|
|
692
|
+
res.status(200).json({
|
|
693
|
+
message: 'invitation text added in test',
|
|
694
|
+
invitationText
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
console.error('error when add invitation text in test : ', err);
|
|
699
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
// Obtenir un résultat par son ID
|
|
703
|
+
this.get('/result/:id', authenticatedOptions, async (req, res) => {
|
|
704
|
+
const { id } = req.params;
|
|
705
|
+
try {
|
|
706
|
+
const result = await TestResult.findById(id);
|
|
707
|
+
if (!result) {
|
|
708
|
+
return res.status(404).json({ message: 'no result founded with this id' });
|
|
709
|
+
}
|
|
710
|
+
res.status(200).json({ message: 'result', data: result });
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
console.error('error when geting result : ', err);
|
|
714
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
// Lister tous les résultats
|
|
718
|
+
this.get('/results/', authenticatedOptions, async (req, res) => {
|
|
719
|
+
try {
|
|
720
|
+
const results = await TestResult.find();
|
|
721
|
+
if (!results) {
|
|
722
|
+
return res.status(404).json({ message: 'no results founded' });
|
|
723
|
+
}
|
|
724
|
+
res.status(200).json({ array: results });
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
console.error('error when geting results : ', err);
|
|
728
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
// Créer un résultat
|
|
732
|
+
this.post('/invite', authenticatedOptions, async (req, res) => {
|
|
733
|
+
const { candidateId, testId } = req.body;
|
|
734
|
+
if (!candidateId || !testId) {
|
|
735
|
+
return res.status(400).json({ message: 'Error, all params are required' });
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const test = await Test.findById(testId);
|
|
739
|
+
if (!test) {
|
|
740
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
741
|
+
}
|
|
742
|
+
const categories = test.categories.map(cat => ({ categoryId: cat.categoryId }));
|
|
743
|
+
const newResult = new TestResult({
|
|
744
|
+
candidateId,
|
|
745
|
+
testId,
|
|
746
|
+
categories,
|
|
747
|
+
state: 'pending',
|
|
748
|
+
invitationDate: Date.now()
|
|
749
|
+
});
|
|
750
|
+
await newResult.save();
|
|
751
|
+
// Récupérer l'email du candidat
|
|
752
|
+
const candidate = await Candidate.findById(candidateId);
|
|
753
|
+
if (!candidate) {
|
|
754
|
+
return res.status(404).json({ message: 'Candidate not found' });
|
|
755
|
+
}
|
|
756
|
+
// Récupérer le contact pour obtenir l'email
|
|
757
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
758
|
+
if (!contact) {
|
|
759
|
+
return res.status(404).json({ message: 'Contact not found' });
|
|
760
|
+
}
|
|
761
|
+
const email = contact.email;
|
|
762
|
+
// Construire le lien d'invitation
|
|
763
|
+
const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
|
|
764
|
+
// Récupérer les credentials d'envoi
|
|
765
|
+
const emailUser = process.env.EMAIL_USER;
|
|
766
|
+
const emailPassword = process.env.EMAIL_PASSWORD;
|
|
767
|
+
// Envoyer l'email via l'event emitter
|
|
768
|
+
await emitter.emit(eventTypes.SEND_EMAIL, {
|
|
769
|
+
template: 'test-invitation',
|
|
770
|
+
to: email,
|
|
771
|
+
from: emailUser,
|
|
772
|
+
emailUser,
|
|
773
|
+
emailPassword,
|
|
774
|
+
data: {
|
|
775
|
+
firstname: contact.firstname,
|
|
776
|
+
testName: test?.title || '',
|
|
777
|
+
testLink
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
res.status(201).json({ message: 'result created with sucess', data: newResult });
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
console.error('error when creating result : ', err);
|
|
784
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
// Obtenir la question suivante
|
|
788
|
+
this.get('/result/getNextQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
|
|
789
|
+
const { id, idCurrentQuestion } = req.params;
|
|
790
|
+
try {
|
|
791
|
+
const result = await TestResult.findById(id);
|
|
792
|
+
if (!result) {
|
|
793
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
794
|
+
}
|
|
795
|
+
const test = await Test.findById(result.testId);
|
|
796
|
+
if (!test) {
|
|
797
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
798
|
+
}
|
|
799
|
+
const questionIndex = test.questions.indexOf(idCurrentQuestion);
|
|
800
|
+
if (questionIndex < test.questions.length) {
|
|
801
|
+
const nextQuestion = test.questions[questionIndex + 1];
|
|
802
|
+
res.status(200).json({ data: nextQuestion });
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
res.status(200).json({ data: null });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
console.error('error when geting the next question : ', err);
|
|
810
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
// Vérifier si c'est la dernière question
|
|
814
|
+
this.get('/result/isLastQuestion/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
|
|
815
|
+
const { id, idCurrentQuestion } = req.params;
|
|
816
|
+
try {
|
|
817
|
+
const result = await TestResult.findById(id);
|
|
818
|
+
if (!result) {
|
|
819
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
820
|
+
}
|
|
821
|
+
const test = await Test.findById(result.testId);
|
|
822
|
+
if (!test) {
|
|
823
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
824
|
+
}
|
|
825
|
+
const questionIndex = test.questions.indexOf(idCurrentQuestion);
|
|
826
|
+
if (questionIndex === test.questions.length - 1) {
|
|
827
|
+
res.status(200).json({ data: true });
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
res.status(200).json({ data: false });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
console.error('error when geting the next question : ', err);
|
|
835
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
// Obtenir une question
|
|
839
|
+
this.get('/result/question/:questionId', authenticatedOptions, async (req, res) => {
|
|
840
|
+
const { questionId } = req.params;
|
|
841
|
+
try {
|
|
842
|
+
const question = await TestQuestion.findById(questionId);
|
|
843
|
+
if (!question) {
|
|
844
|
+
return res.status(404).json({ message: 'not found' });
|
|
845
|
+
}
|
|
846
|
+
res.status(200).json({ data: question });
|
|
847
|
+
}
|
|
848
|
+
catch (err) {
|
|
849
|
+
console.error('error when geting the question : ', err);
|
|
850
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
// Envoyer une réponse
|
|
854
|
+
this.put('/result/sendResponse/:id/:idCurrentQuestion', authenticatedOptions, async (req, res) => {
|
|
855
|
+
const { id, idCurrentQuestion } = req.params;
|
|
856
|
+
const { candidateResponse } = req.body;
|
|
857
|
+
try {
|
|
858
|
+
const result = await TestResult.findById(id);
|
|
859
|
+
if (!result) {
|
|
860
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
861
|
+
}
|
|
862
|
+
const test = await Test.findById(result.testId);
|
|
863
|
+
if (!test) {
|
|
864
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
865
|
+
}
|
|
866
|
+
if (!result.responses) {
|
|
867
|
+
result.state = 'inProgress';
|
|
868
|
+
result.responses = [];
|
|
869
|
+
}
|
|
870
|
+
result.responses.push({
|
|
871
|
+
questionId: idCurrentQuestion,
|
|
872
|
+
response: candidateResponse,
|
|
873
|
+
score: 0,
|
|
874
|
+
comment: ' '
|
|
875
|
+
});
|
|
876
|
+
await result.save();
|
|
877
|
+
const questionIndex = test.questions.indexOf(idCurrentQuestion);
|
|
878
|
+
if (questionIndex === test.questions.length - 1) {
|
|
879
|
+
emitter.emit(eventTypes.CORRECT_TEST, result);
|
|
880
|
+
result.state = 'finish';
|
|
881
|
+
await result.save();
|
|
882
|
+
}
|
|
883
|
+
res.status(200).json({ response: candidateResponse });
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
console.error('error when sending result : ', err);
|
|
887
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
// Corriger un test
|
|
891
|
+
this.post('/result/correct/:id', authenticatedOptions, async (req, res) => {
|
|
892
|
+
const { id } = req.params;
|
|
893
|
+
try {
|
|
894
|
+
const result = await TestResult.findById(id);
|
|
895
|
+
if (!result) {
|
|
896
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
897
|
+
}
|
|
898
|
+
emitter.emit(eventTypes.CORRECT_TEST, result);
|
|
899
|
+
res.status(200).json({ message: 'Result in correction' });
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
902
|
+
console.error('error when correcting result : ', err);
|
|
903
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
// Calculer le score
|
|
907
|
+
this.put('/result/calculateScore/:id', authenticatedOptions, async (req, res) => {
|
|
908
|
+
const { id } = req.params;
|
|
909
|
+
try {
|
|
910
|
+
const result = await TestResult.findById(id);
|
|
911
|
+
if (!result) {
|
|
912
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
913
|
+
}
|
|
914
|
+
result.state = 'finish';
|
|
915
|
+
let finalscore = 0;
|
|
916
|
+
for (const response of result.responses) {
|
|
917
|
+
const question = await TestQuestion.findById(response.questionId);
|
|
918
|
+
if (!question)
|
|
919
|
+
continue;
|
|
920
|
+
const score = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CORRECT_QUESTION || '', 'correctQuestion', {
|
|
921
|
+
question: {
|
|
922
|
+
_id: question._id.toString(),
|
|
923
|
+
instruction: question.instruction,
|
|
924
|
+
possibleResponses: question.possibleResponses,
|
|
925
|
+
questionType: question.questionType,
|
|
926
|
+
maxScore: question.maxScore
|
|
927
|
+
},
|
|
928
|
+
result: {
|
|
929
|
+
responses: [{
|
|
930
|
+
questionId: response.questionId.toString(),
|
|
931
|
+
response: response.response
|
|
932
|
+
}]
|
|
933
|
+
}
|
|
934
|
+
}, true);
|
|
935
|
+
const parsedResult = JSON.parse(score);
|
|
936
|
+
finalscore += parsedResult.score;
|
|
937
|
+
response.score = parsedResult.score;
|
|
938
|
+
response.comment = parsedResult.comment;
|
|
939
|
+
}
|
|
940
|
+
result.score = finalscore;
|
|
941
|
+
await result.save();
|
|
942
|
+
res.status(200).json({ data: finalscore });
|
|
943
|
+
}
|
|
944
|
+
catch (err) {
|
|
945
|
+
console.error('error when calculate the score : ', err);
|
|
946
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
// Obtenir le score maximum
|
|
950
|
+
this.get('/maxscore/:resultId', authenticatedOptions, async (req, res) => {
|
|
951
|
+
const { resultId } = req.params;
|
|
952
|
+
try {
|
|
953
|
+
const result = await TestResult.findById(resultId);
|
|
954
|
+
if (!result) {
|
|
955
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
956
|
+
}
|
|
957
|
+
const test = await Test.findById(result.testId);
|
|
958
|
+
if (!test) {
|
|
959
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
960
|
+
}
|
|
961
|
+
let maxScore = 0;
|
|
962
|
+
for (const questionId of test.questions) {
|
|
963
|
+
const question = await TestQuestion.findById(questionId);
|
|
964
|
+
if (question) {
|
|
965
|
+
maxScore += question.maxScore;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
res.status(200).json({ data: maxScore });
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
console.error('error when geting score : ', err);
|
|
972
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
// Obtenir le score d'un résultat
|
|
976
|
+
this.get('/result/score/:id', authenticatedOptions, async (req, res) => {
|
|
977
|
+
const { id } = req.params;
|
|
978
|
+
try {
|
|
979
|
+
const result = await TestResult.findById(id);
|
|
980
|
+
if (!result) {
|
|
981
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
982
|
+
}
|
|
983
|
+
res.status(200).json({ data: result.score });
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
console.error('error when geting score : ', err);
|
|
987
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
// Générer plusieurs questions pour un test
|
|
991
|
+
this.put('/test/generateQuestions/:id', authenticatedOptions, async (req, res) => {
|
|
992
|
+
const { id } = req.params;
|
|
993
|
+
const { numberOfQuestions, category } = req.body;
|
|
994
|
+
if (!numberOfQuestions || numberOfQuestions <= 0) {
|
|
995
|
+
return res.status(400).json({ message: 'Le nombre de questions doit être positif' });
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
const test = await Test.findById(id);
|
|
999
|
+
if (!test) {
|
|
1000
|
+
return res.status(404).json({ message: 'Test non trouvé' });
|
|
1001
|
+
}
|
|
1002
|
+
let categoriesToUse = [];
|
|
1003
|
+
if (category && category !== 'ALL') {
|
|
1004
|
+
const categoryInfo = test.categories.find(cat => cat.categoryId.toString() === category);
|
|
1005
|
+
if (categoryInfo) {
|
|
1006
|
+
categoriesToUse = [{
|
|
1007
|
+
categoryId: categoryInfo.categoryId.toString(),
|
|
1008
|
+
expertiseLevel: categoryInfo.expertiseLevel.toString()
|
|
1009
|
+
}];
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
// Si category est 'ALL' ou absent, on utilise toutes les catégories du test
|
|
1014
|
+
categoriesToUse = test.categories.map(cat => ({
|
|
1015
|
+
categoryId: cat.categoryId.toString(),
|
|
1016
|
+
expertiseLevel: cat.expertiseLevel.toString()
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
if (categoriesToUse.length === 0) {
|
|
1020
|
+
return res.status(400).json({ message: 'Aucune catégorie disponible pour générer des questions' });
|
|
1021
|
+
}
|
|
1022
|
+
const generatedQuestions = [];
|
|
1023
|
+
let questionsGenerated = 0;
|
|
1024
|
+
let attempts = 0;
|
|
1025
|
+
const maxAttempts = numberOfQuestions * 3; // Limite pour éviter les boucles infinies
|
|
1026
|
+
// Si on spécifie une catégorie, on génère toutes les questions pour cette catégorie
|
|
1027
|
+
if (category && category !== 'ALL') {
|
|
1028
|
+
const categoryInfo = categoriesToUse[0];
|
|
1029
|
+
while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
|
|
1030
|
+
attempts++;
|
|
1031
|
+
const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
|
|
1032
|
+
if (question) {
|
|
1033
|
+
generatedQuestions.push(question);
|
|
1034
|
+
questionsGenerated++;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
// Pour ALL, répartition aléatoire sur toutes les catégories
|
|
1040
|
+
const shuffledCategories = [...categoriesToUse].sort(() => Math.random() - 0.5);
|
|
1041
|
+
while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
|
|
1042
|
+
attempts++;
|
|
1043
|
+
// Sélectionner une catégorie aléatoire
|
|
1044
|
+
const randomCategoryIndex = Math.floor(Math.random() * shuffledCategories.length);
|
|
1045
|
+
const categoryInfo = shuffledCategories[randomCategoryIndex];
|
|
1046
|
+
const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
|
|
1047
|
+
if (question) {
|
|
1048
|
+
generatedQuestions.push(question);
|
|
1049
|
+
questionsGenerated++;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// Vérifier qu'au moins une question a été générée
|
|
1054
|
+
if (generatedQuestions.length === 0) {
|
|
1055
|
+
return res.status(500).json({
|
|
1056
|
+
message: 'Aucune question n\'a pu être générée. Veuillez réessayer plus tard.'
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
res.status(200).json({
|
|
1060
|
+
message: `${generatedQuestions.length} question(s) générée(s) avec succès`,
|
|
1061
|
+
questions: generatedQuestions,
|
|
1062
|
+
test
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
catch (err) {
|
|
1066
|
+
console.error('Erreur lors de la génération des questions : ', err);
|
|
1067
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
// Lister tous les candidats invités à un test
|
|
1071
|
+
this.get('/test/:testId/candidates', authenticatedOptions, async (req, res) => {
|
|
1072
|
+
const { testId } = req.params;
|
|
1073
|
+
const page = parseInt(req.query.page) || 1;
|
|
1074
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
1075
|
+
const skip = (page - 1) * limit;
|
|
1076
|
+
const search = req.query.search || '';
|
|
1077
|
+
const state = req.query.state || 'all';
|
|
1078
|
+
const sortBy = req.query.sortBy || 'invitationDate';
|
|
1079
|
+
const sortOrder = req.query.sortOrder || 'desc';
|
|
1080
|
+
try {
|
|
1081
|
+
const test = await Test.findById(testId);
|
|
1082
|
+
if (!test) {
|
|
1083
|
+
return res.status(404).json({ message: 'Test non trouvé' });
|
|
1084
|
+
}
|
|
1085
|
+
// Construction de la requête
|
|
1086
|
+
const query = { testId };
|
|
1087
|
+
if (state !== 'all') {
|
|
1088
|
+
query.state = state;
|
|
1089
|
+
}
|
|
1090
|
+
// Recherche sur les candidats via leurs contacts
|
|
1091
|
+
if (search) {
|
|
1092
|
+
// D'abord, rechercher dans les contacts
|
|
1093
|
+
const contacts = await ContactModel.find({
|
|
1094
|
+
$or: [
|
|
1095
|
+
{ firstname: { $regex: search, $options: 'i' } },
|
|
1096
|
+
{ lastname: { $regex: search, $options: 'i' } },
|
|
1097
|
+
{ email: { $regex: search, $options: 'i' } }
|
|
1098
|
+
]
|
|
1099
|
+
});
|
|
1100
|
+
// Ensuite, récupérer les candidats qui ont ces contacts
|
|
1101
|
+
const contactIds = contacts.map(c => c._id);
|
|
1102
|
+
const candidates = await Candidate.find({
|
|
1103
|
+
contact: { $in: contactIds }
|
|
1104
|
+
});
|
|
1105
|
+
const candidateIds = candidates.map(c => c._id);
|
|
1106
|
+
query.candidateId = { $in: candidateIds };
|
|
1107
|
+
}
|
|
1108
|
+
// Déterminer l'ordre de tri
|
|
1109
|
+
const sortDirection = sortOrder === 'asc' ? 1 : -1;
|
|
1110
|
+
// Si on trie par lastName, on récupère tous les résultats puis on trie après
|
|
1111
|
+
// Sinon on peut trier directement dans la requête MongoDB
|
|
1112
|
+
let results, total;
|
|
1113
|
+
if (sortBy === 'lastName') {
|
|
1114
|
+
// Récupérer tous les résultats sans pagination pour pouvoir trier par lastName
|
|
1115
|
+
const allResults = await TestResult.find(query).exec();
|
|
1116
|
+
total = allResults.length;
|
|
1117
|
+
// Récupérer les données des candidats pour le tri
|
|
1118
|
+
const candidateIds = allResults.map(result => result.candidateId);
|
|
1119
|
+
const candidates = await Candidate.find({ _id: { $in: candidateIds } });
|
|
1120
|
+
const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
|
|
1121
|
+
// Combiner les résultats avec les données des candidats et trier
|
|
1122
|
+
const resultsWithCandidates = await Promise.all(allResults.map(async (result) => {
|
|
1123
|
+
const candidate = candidatesMap.get(result.candidateId.toString());
|
|
1124
|
+
if (!candidate) {
|
|
1125
|
+
return {
|
|
1126
|
+
...result.toObject(),
|
|
1127
|
+
candidate: null,
|
|
1128
|
+
lastName: ''
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
1132
|
+
return {
|
|
1133
|
+
...result.toObject(),
|
|
1134
|
+
candidate: contact
|
|
1135
|
+
? {
|
|
1136
|
+
firstName: contact.firstname,
|
|
1137
|
+
lastName: contact.lastname,
|
|
1138
|
+
email: contact.email
|
|
1139
|
+
}
|
|
1140
|
+
: null,
|
|
1141
|
+
lastName: contact ? contact.lastname : ''
|
|
1142
|
+
};
|
|
1143
|
+
}));
|
|
1144
|
+
// Trier par lastName
|
|
1145
|
+
resultsWithCandidates.sort((a, b) => {
|
|
1146
|
+
const lastNameA = (a.lastName || '').toLowerCase();
|
|
1147
|
+
const lastNameB = (b.lastName || '').toLowerCase();
|
|
1148
|
+
return sortDirection === 1
|
|
1149
|
+
? lastNameA.localeCompare(lastNameB)
|
|
1150
|
+
: lastNameB.localeCompare(lastNameA);
|
|
1151
|
+
});
|
|
1152
|
+
// Appliquer la pagination
|
|
1153
|
+
results = resultsWithCandidates.slice(skip, skip + limit);
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
// Tri direct dans MongoDB pour invitationDate
|
|
1157
|
+
const sortObject = {};
|
|
1158
|
+
sortObject[sortBy] = sortDirection;
|
|
1159
|
+
[results, total] = await Promise.all([
|
|
1160
|
+
TestResult.find(query)
|
|
1161
|
+
.sort(sortObject)
|
|
1162
|
+
.skip(skip)
|
|
1163
|
+
.limit(limit)
|
|
1164
|
+
.exec(),
|
|
1165
|
+
TestResult.countDocuments(query)
|
|
1166
|
+
]);
|
|
1167
|
+
}
|
|
1168
|
+
// Calculer le maxScore du test
|
|
1169
|
+
let maxScore = 0;
|
|
1170
|
+
if (test.questions && test.questions.length > 0) {
|
|
1171
|
+
const questionIds = test.questions.map((q) => q.questionId || q);
|
|
1172
|
+
const questions = await TestQuestion.find({ _id: { $in: questionIds } }).lean();
|
|
1173
|
+
maxScore = questions.reduce((sum, q) => sum + (q.maxScore || 0), 0);
|
|
1174
|
+
}
|
|
1175
|
+
// Si on a déjà traité les candidats pour le tri par lastName, on utilise directement les résultats
|
|
1176
|
+
let resultsWithCandidates;
|
|
1177
|
+
if (sortBy === 'lastName') {
|
|
1178
|
+
// Les résultats sont déjà traités avec les données des candidats
|
|
1179
|
+
resultsWithCandidates = results.map(result => ({
|
|
1180
|
+
...result,
|
|
1181
|
+
maxScore
|
|
1182
|
+
}));
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
// Récupérer les données des candidats
|
|
1186
|
+
const candidateIds = results.map(result => result.candidateId);
|
|
1187
|
+
const candidates = await Candidate.find({ _id: { $in: candidateIds } });
|
|
1188
|
+
const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
|
|
1189
|
+
// Combiner les résultats avec les données des candidats
|
|
1190
|
+
resultsWithCandidates = await Promise.all(results.map(async (result) => {
|
|
1191
|
+
const candidate = candidatesMap.get(result.candidateId.toString());
|
|
1192
|
+
if (!candidate) {
|
|
1193
|
+
return {
|
|
1194
|
+
...result.toObject(),
|
|
1195
|
+
candidate: null,
|
|
1196
|
+
maxScore
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
// Récupérer le contact pour obtenir les informations personnelles
|
|
1200
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
1201
|
+
return {
|
|
1202
|
+
...result.toObject(),
|
|
1203
|
+
candidate: contact
|
|
1204
|
+
? {
|
|
1205
|
+
firstName: contact.firstname,
|
|
1206
|
+
lastName: contact.lastname,
|
|
1207
|
+
email: contact.email
|
|
1208
|
+
}
|
|
1209
|
+
: null,
|
|
1210
|
+
maxScore
|
|
1211
|
+
};
|
|
1212
|
+
}));
|
|
1213
|
+
}
|
|
1214
|
+
const totalPages = Math.ceil(total / limit);
|
|
1215
|
+
return res.json({
|
|
1216
|
+
data: resultsWithCandidates,
|
|
1217
|
+
pagination: {
|
|
1218
|
+
currentPage: page,
|
|
1219
|
+
totalPages,
|
|
1220
|
+
totalItems: total,
|
|
1221
|
+
itemsPerPage: limit,
|
|
1222
|
+
hasNextPage: page < totalPages,
|
|
1223
|
+
hasPreviousPage: page > 1
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
catch (err) {
|
|
1228
|
+
console.error('Erreur lors de la récupération des candidats : ', err);
|
|
1229
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
// Renvoyer l'email d'invitation à un candidat
|
|
1233
|
+
this.post('/reinvite/:resultId', authenticatedOptions, async (req, res) => {
|
|
1234
|
+
const { resultId } = req.params;
|
|
1235
|
+
try {
|
|
1236
|
+
const result = await TestResult.findById(resultId);
|
|
1237
|
+
if (!result) {
|
|
1238
|
+
return res.status(404).json({ message: 'Result not found' });
|
|
1239
|
+
}
|
|
1240
|
+
// Récupérer le candidat et son contact
|
|
1241
|
+
const candidate = await Candidate.findById(result.candidateId);
|
|
1242
|
+
if (!candidate) {
|
|
1243
|
+
return res.status(404).json({ message: 'Candidate not found' });
|
|
1244
|
+
}
|
|
1245
|
+
// Récupérer le contact pour obtenir l'email
|
|
1246
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
1247
|
+
if (!contact) {
|
|
1248
|
+
return res.status(404).json({ message: 'Contact not found' });
|
|
1249
|
+
}
|
|
1250
|
+
// Récupérer les informations du test
|
|
1251
|
+
const test = await Test.findById(result.testId);
|
|
1252
|
+
if (!test) {
|
|
1253
|
+
return res.status(404).json({ message: 'Test not found' });
|
|
1254
|
+
}
|
|
1255
|
+
const email = contact.email;
|
|
1256
|
+
const emailUser = process.env.EMAIL_USER;
|
|
1257
|
+
const emailPassword = process.env.EMAIL_PASSWORD;
|
|
1258
|
+
// Construire le lien d'invitation
|
|
1259
|
+
const testLink = (process.env.TEST_INVITATION_LINK || '') + email;
|
|
1260
|
+
// Envoyer l'email via l'event emitter
|
|
1261
|
+
await emitter.emit(eventTypes.SEND_EMAIL, {
|
|
1262
|
+
template: 'test-invitation',
|
|
1263
|
+
to: email,
|
|
1264
|
+
from: emailUser,
|
|
1265
|
+
emailUser,
|
|
1266
|
+
emailPassword,
|
|
1267
|
+
data: {
|
|
1268
|
+
testLink
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
// Mettre à jour la date d'invitation
|
|
1272
|
+
result.set('invitationDate', new Date());
|
|
1273
|
+
await result.save();
|
|
1274
|
+
res.status(200).json({ message: 'Invitation email sent successfully' });
|
|
1275
|
+
}
|
|
1276
|
+
catch (err) {
|
|
1277
|
+
console.error('Error when resending invitation : ', err);
|
|
1278
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
// Correction manuelle d'une réponse à une question d'un testResult
|
|
1282
|
+
this.put('/result/:testResultId/response/:questionId', authenticatedOptions, async (req, res) => {
|
|
1283
|
+
try {
|
|
1284
|
+
const { testResultId, questionId } = req.params;
|
|
1285
|
+
const { score, comment } = req.body;
|
|
1286
|
+
// Récupérer le résultat de test
|
|
1287
|
+
const result = await TestResult.findById(testResultId);
|
|
1288
|
+
if (!result) {
|
|
1289
|
+
return res.status(404).json({ message: 'TestResult non trouvé' });
|
|
1290
|
+
}
|
|
1291
|
+
// Trouver la réponse à corriger
|
|
1292
|
+
const response = (result.responses || []).find((r) => r.questionId.toString() === questionId);
|
|
1293
|
+
if (!response) {
|
|
1294
|
+
return res.status(404).json({ message: 'Réponse à cette question non trouvée dans ce testResult' });
|
|
1295
|
+
}
|
|
1296
|
+
// Récupérer la question pour vérifier le maxScore
|
|
1297
|
+
const question = await TestQuestion.findById(questionId);
|
|
1298
|
+
if (!question) {
|
|
1299
|
+
return res.status(404).json({ message: 'Question non trouvée' });
|
|
1300
|
+
}
|
|
1301
|
+
const maxScore = question.maxScore;
|
|
1302
|
+
if (typeof score === 'number' && score > maxScore) {
|
|
1303
|
+
return res.status(400).json({ message: `Le score ne peut pas dépasser le maximum autorisé (${maxScore}) pour cette question.` });
|
|
1304
|
+
}
|
|
1305
|
+
// Surcharger le score et le commentaire
|
|
1306
|
+
if (typeof score === 'number')
|
|
1307
|
+
response.score = score;
|
|
1308
|
+
if (typeof comment === 'string')
|
|
1309
|
+
response.comment = comment;
|
|
1310
|
+
// Recalculer le score global
|
|
1311
|
+
result.score = (result.responses || []).reduce((sum, r) => sum + (r.score || 0), 0);
|
|
1312
|
+
await result.save();
|
|
1313
|
+
return res.status(200).json({
|
|
1314
|
+
message: 'Correction manuelle enregistrée',
|
|
1315
|
+
response,
|
|
1316
|
+
scoreGlobal: result.score
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
catch (err) {
|
|
1320
|
+
console.error('Erreur lors de la correction manuelle :', err);
|
|
1321
|
+
res.status(500).json({ message: 'Erreur interne du serveur' });
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
// Supprimer un test result
|
|
1325
|
+
this.delete('/result/:id', authenticatedOptions, async (req, res) => {
|
|
1326
|
+
const { id } = req.params;
|
|
1327
|
+
try {
|
|
1328
|
+
const testResult = await TestResult.findById(id);
|
|
1329
|
+
if (!testResult) {
|
|
1330
|
+
return res.status(404).json({ message: 'TestResult not found' });
|
|
1331
|
+
}
|
|
1332
|
+
await TestResult.findByIdAndDelete(id);
|
|
1333
|
+
res.status(200).json({ message: 'TestResult deleted with success' });
|
|
1334
|
+
}
|
|
1335
|
+
catch (err) {
|
|
1336
|
+
console.error('error when deleting testResult : ', err);
|
|
1337
|
+
res.status(500).json({ message: 'Internal server error' });
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const router = new ExamsRouter();
|
|
1343
|
+
export default router;
|