@programisto/edrm-exams 0.2.13 → 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/dist/modules/edrm-exams/listeners/correct.listener.js +36 -3
- 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.model.d.ts +3 -8
- package/dist/modules/edrm-exams/models/test.model.js +25 -15
- package/dist/modules/edrm-exams/routes/exams-candidate.router.js +22 -3
- package/dist/modules/edrm-exams/routes/exams.router.js +154 -10
- package/dist/modules/edrm-exams/routes/result.router.js +33 -4
- package/package.json +1 -1
|
@@ -48,16 +48,49 @@ async function correctTest(options) {
|
|
|
48
48
|
}, true);
|
|
49
49
|
console.log('Correction result:', { scoreResponse });
|
|
50
50
|
const parsedResult = JSON.parse(scoreResponse);
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Valider le score retourné par l'IA
|
|
52
|
+
let validScore = 0;
|
|
53
|
+
if (parsedResult.score !== undefined && parsedResult.score !== null) {
|
|
54
|
+
const score = parseFloat(parsedResult.score.toString());
|
|
55
|
+
if (!isNaN(score) && isFinite(score) && score >= 0) {
|
|
56
|
+
validScore = score;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.warn('Invalid score returned by AI:', parsedResult.score);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finalscore += validScore;
|
|
63
|
+
dbResponse.score = validScore;
|
|
53
64
|
dbResponse.comment = parsedResult.comment || '';
|
|
54
65
|
}
|
|
66
|
+
// S'assurer que finalscore est un nombre valide
|
|
67
|
+
if (isNaN(finalscore) || !isFinite(finalscore)) {
|
|
68
|
+
console.warn('Invalid finalscore calculated, setting to 0:', finalscore);
|
|
69
|
+
finalscore = 0;
|
|
70
|
+
}
|
|
71
|
+
// S'assurer que maxScore est un nombre valide
|
|
72
|
+
if (isNaN(maxScore) || !isFinite(maxScore)) {
|
|
73
|
+
console.warn('Invalid maxScore calculated, setting to 0:', maxScore);
|
|
74
|
+
maxScore = 0;
|
|
75
|
+
}
|
|
55
76
|
// Mettre à jour le score final et l'état
|
|
56
77
|
result.score = finalscore;
|
|
57
78
|
result.state = TestState.Finish;
|
|
58
79
|
// Forcer la sauvegarde des sous-documents responses
|
|
59
80
|
result.markModified('responses');
|
|
60
|
-
|
|
81
|
+
// Calculer le pourcentage de score en évitant la division par zéro
|
|
82
|
+
let scorePercentage = 0;
|
|
83
|
+
if (maxScore > 0) {
|
|
84
|
+
scorePercentage = Math.ceil((finalscore / maxScore) * 100);
|
|
85
|
+
}
|
|
86
|
+
else if (finalscore > 0) {
|
|
87
|
+
// Si maxScore est 0 mais qu'il y a un score, on met 100%
|
|
88
|
+
scorePercentage = 100;
|
|
89
|
+
}
|
|
90
|
+
// S'assurer que le score est un nombre valide
|
|
91
|
+
if (isNaN(scorePercentage) || !isFinite(scorePercentage)) {
|
|
92
|
+
scorePercentage = 0;
|
|
93
|
+
}
|
|
61
94
|
// Sauvegarder les modifications avec findByIdAndUpdate pour éviter les conflits de version
|
|
62
95
|
await TestResult.findByIdAndUpdate(result._id, {
|
|
63
96
|
$set: {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EnduranceSchema } from '@programisto/endurance-core';
|
|
2
|
+
declare class TestJob extends EnduranceSchema {
|
|
3
|
+
name: string;
|
|
4
|
+
static getModel(): import("@typegoose/typegoose").ReturnModelType<typeof TestJob, import("@typegoose/typegoose/lib/types").BeAnObject>;
|
|
5
|
+
}
|
|
6
|
+
declare const TestJobModel: import("@typegoose/typegoose").ReturnModelType<typeof TestJob, import("@typegoose/typegoose/lib/types").BeAnObject>;
|
|
7
|
+
export default TestJobModel;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { EnduranceSchema, EnduranceModelType } from '@programisto/endurance-core';
|
|
11
|
+
let TestJob = class TestJob extends EnduranceSchema {
|
|
12
|
+
name;
|
|
13
|
+
static getModel() {
|
|
14
|
+
return TestJobModel;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
__decorate([
|
|
18
|
+
EnduranceModelType.prop({ required: true }),
|
|
19
|
+
__metadata("design:type", String)
|
|
20
|
+
], TestJob.prototype, "name", void 0);
|
|
21
|
+
TestJob = __decorate([
|
|
22
|
+
EnduranceModelType.modelOptions({
|
|
23
|
+
options: {
|
|
24
|
+
allowMixed: EnduranceModelType.Severity.ALLOW
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
], TestJob);
|
|
28
|
+
const TestJobModel = EnduranceModelType.getModelForClass(TestJob);
|
|
29
|
+
export default TestJobModel;
|
|
@@ -2,19 +2,13 @@ import { EnduranceSchema } from '@programisto/endurance-core';
|
|
|
2
2
|
import Company from './company.model.js';
|
|
3
3
|
import TestQuestion from './test-question.model.js';
|
|
4
4
|
import TestCategory from './test-category.models.js';
|
|
5
|
+
import TestJob from './test-job.model.js';
|
|
5
6
|
import User from './user.model.js';
|
|
6
7
|
declare enum TestState {
|
|
7
8
|
Draft = "draft",
|
|
8
9
|
Published = "published",
|
|
9
10
|
Archived = "archived"
|
|
10
11
|
}
|
|
11
|
-
declare enum JobType {
|
|
12
|
-
FrontEnd = "front-end",
|
|
13
|
-
BackEnd = "back-end",
|
|
14
|
-
Fullstack = "fullstack",
|
|
15
|
-
DevOps = "devops",
|
|
16
|
-
Data = "data"
|
|
17
|
-
}
|
|
18
12
|
declare enum SeniorityLevel {
|
|
19
13
|
Student = "student",
|
|
20
14
|
Junior = "junior",
|
|
@@ -44,9 +38,10 @@ declare class Test extends EnduranceSchema {
|
|
|
44
38
|
duration?: number;
|
|
45
39
|
passingScore?: number;
|
|
46
40
|
categories?: TestCategoryWithExpertise[];
|
|
47
|
-
targetJob:
|
|
41
|
+
targetJob: typeof TestJob;
|
|
48
42
|
seniorityLevel: SeniorityLevel;
|
|
49
43
|
static getModel(): import("@typegoose/typegoose").ReturnModelType<typeof Test, import("@typegoose/typegoose/lib/types.js").BeAnObject>;
|
|
44
|
+
migrateTargetJob(): Promise<void>;
|
|
50
45
|
}
|
|
51
46
|
declare const TestModel: import("@typegoose/typegoose").ReturnModelType<typeof Test, import("@typegoose/typegoose/lib/types.js").BeAnObject>;
|
|
52
47
|
export default TestModel;
|
|
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { EnduranceSchema, EnduranceModelType } from '@programisto/endurance-core';
|
|
11
11
|
import Company from './company.model.js';
|
|
12
|
+
import TestJob from './test-job.model.js';
|
|
12
13
|
import User from './user.model.js';
|
|
13
14
|
var TestState;
|
|
14
15
|
(function (TestState) {
|
|
@@ -19,19 +20,6 @@ var TestState;
|
|
|
19
20
|
// eslint-disable-next-line no-unused-vars
|
|
20
21
|
TestState["Archived"] = "archived";
|
|
21
22
|
})(TestState || (TestState = {}));
|
|
22
|
-
var JobType;
|
|
23
|
-
(function (JobType) {
|
|
24
|
-
// eslint-disable-next-line no-unused-vars
|
|
25
|
-
JobType["FrontEnd"] = "front-end";
|
|
26
|
-
// eslint-disable-next-line no-unused-vars
|
|
27
|
-
JobType["BackEnd"] = "back-end";
|
|
28
|
-
// eslint-disable-next-line no-unused-vars
|
|
29
|
-
JobType["Fullstack"] = "fullstack";
|
|
30
|
-
// eslint-disable-next-line no-unused-vars
|
|
31
|
-
JobType["DevOps"] = "devops";
|
|
32
|
-
// eslint-disable-next-line no-unused-vars
|
|
33
|
-
JobType["Data"] = "data";
|
|
34
|
-
})(JobType || (JobType = {}));
|
|
35
23
|
var SeniorityLevel;
|
|
36
24
|
(function (SeniorityLevel) {
|
|
37
25
|
// eslint-disable-next-line no-unused-vars
|
|
@@ -67,6 +55,28 @@ let Test = class Test extends EnduranceSchema {
|
|
|
67
55
|
static getModel() {
|
|
68
56
|
return TestModel;
|
|
69
57
|
}
|
|
58
|
+
// Méthode pour migrer automatiquement les anciennes données
|
|
59
|
+
async migrateTargetJob() {
|
|
60
|
+
const testData = this;
|
|
61
|
+
// Si targetJob est une string (ancien format), on la migre
|
|
62
|
+
if (typeof testData.targetJob === 'string') {
|
|
63
|
+
try {
|
|
64
|
+
// Chercher si le job existe déjà
|
|
65
|
+
let jobType = await TestJob.findOne({ name: testData.targetJob });
|
|
66
|
+
// Si pas trouvé, on le crée
|
|
67
|
+
if (!jobType) {
|
|
68
|
+
jobType = new TestJob({ name: testData.targetJob });
|
|
69
|
+
await jobType.save();
|
|
70
|
+
}
|
|
71
|
+
// Mettre à jour la référence
|
|
72
|
+
this.targetJob = jobType._id;
|
|
73
|
+
await this.save();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('Erreur lors de la migration du targetJob:', error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
70
80
|
};
|
|
71
81
|
__decorate([
|
|
72
82
|
EnduranceModelType.prop({ required: true }),
|
|
@@ -105,8 +115,8 @@ __decorate([
|
|
|
105
115
|
__metadata("design:type", Array)
|
|
106
116
|
], Test.prototype, "categories", void 0);
|
|
107
117
|
__decorate([
|
|
108
|
-
EnduranceModelType.prop({
|
|
109
|
-
__metadata("design:type",
|
|
118
|
+
EnduranceModelType.prop({ ref: () => TestJob, required: true }),
|
|
119
|
+
__metadata("design:type", Object)
|
|
110
120
|
], Test.prototype, "targetJob", void 0);
|
|
111
121
|
__decorate([
|
|
112
122
|
EnduranceModelType.prop({ required: true, enum: SeniorityLevel }),
|
|
@@ -3,7 +3,26 @@ import CandidateModel from '../models/candidate.model.js';
|
|
|
3
3
|
import ContactModel from '../models/contact.model.js';
|
|
4
4
|
import TestResult from '../models/test-result.model.js';
|
|
5
5
|
import Test from '../models/test.model.js';
|
|
6
|
+
import TestJob from '../models/test-job.model.js';
|
|
6
7
|
import jwt from 'jsonwebtoken';
|
|
8
|
+
// Fonction utilitaire pour récupérer le nom du job
|
|
9
|
+
async function getJobName(targetJob) {
|
|
10
|
+
// Si c'est déjà une string (ancien format), on la retourne directement
|
|
11
|
+
if (typeof targetJob === 'string') {
|
|
12
|
+
return targetJob;
|
|
13
|
+
}
|
|
14
|
+
// Si c'est un ObjectId, on récupère le job
|
|
15
|
+
if (targetJob && typeof targetJob === 'object' && targetJob._id) {
|
|
16
|
+
const job = await TestJob.findById(targetJob._id);
|
|
17
|
+
return job ? job.name : 'Job inconnu';
|
|
18
|
+
}
|
|
19
|
+
// Si c'est juste un ObjectId
|
|
20
|
+
if (targetJob && typeof targetJob === 'object' && targetJob.toString) {
|
|
21
|
+
const job = await TestJob.findById(targetJob);
|
|
22
|
+
return job ? job.name : 'Job inconnu';
|
|
23
|
+
}
|
|
24
|
+
return 'Job inconnu';
|
|
25
|
+
}
|
|
7
26
|
class CandidateRouter extends EnduranceRouter {
|
|
8
27
|
constructor() {
|
|
9
28
|
super(EnduranceAuthMiddleware.getInstance());
|
|
@@ -383,7 +402,7 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
383
402
|
const categoriesDocs = await TestCategory.find({ _id: { $in: allCategoryIds } }).lean();
|
|
384
403
|
const categoriesMap = new Map(categoriesDocs.map(cat => [cat._id.toString(), cat.name]));
|
|
385
404
|
// Combiner les résultats avec les informations des tests et des catégories
|
|
386
|
-
const resultsWithTests = results.map(result => {
|
|
405
|
+
const resultsWithTests = await Promise.all(results.map(async (result) => {
|
|
387
406
|
const test = testsMap.get(result.testId.toString());
|
|
388
407
|
let categoriesWithNames = [];
|
|
389
408
|
if (test && test.categories) {
|
|
@@ -398,13 +417,13 @@ class CandidateRouter extends EnduranceRouter {
|
|
|
398
417
|
? {
|
|
399
418
|
title: test.title,
|
|
400
419
|
description: test.description,
|
|
401
|
-
targetJob: test.targetJob,
|
|
420
|
+
targetJob: await getJobName(test.targetJob),
|
|
402
421
|
seniorityLevel: test.seniorityLevel,
|
|
403
422
|
categories: categoriesWithNames
|
|
404
423
|
}
|
|
405
424
|
: null
|
|
406
425
|
};
|
|
407
|
-
});
|
|
426
|
+
}));
|
|
408
427
|
const totalPages = Math.ceil(total / limit);
|
|
409
428
|
return res.json({
|
|
410
429
|
data: resultsWithTests,
|
|
@@ -3,9 +3,34 @@ import Test from '../models/test.model.js';
|
|
|
3
3
|
import TestQuestion from '../models/test-question.model.js';
|
|
4
4
|
import TestResult from '../models/test-result.model.js';
|
|
5
5
|
import TestCategory from '../models/test-category.models.js';
|
|
6
|
+
import TestJob from '../models/test-job.model.js';
|
|
6
7
|
import Candidate from '../models/candidate.model.js';
|
|
7
8
|
import ContactModel from '../models/contact.model.js';
|
|
8
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
|
+
}
|
|
9
34
|
class ExamsRouter extends EnduranceRouter {
|
|
10
35
|
constructor() {
|
|
11
36
|
super(EnduranceAuthMiddleware.getInstance());
|
|
@@ -20,8 +45,9 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
20
45
|
// Récupérer les questions existantes pour éviter les doublons
|
|
21
46
|
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
22
47
|
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
48
|
+
const jobName = await getJobName(test.targetJob);
|
|
23
49
|
const questionParams = {
|
|
24
|
-
job:
|
|
50
|
+
job: jobName,
|
|
25
51
|
seniority: test.seniorityLevel,
|
|
26
52
|
category: categoryDoc.name,
|
|
27
53
|
questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
|
|
@@ -98,6 +124,78 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
98
124
|
res.status(500).json({ message: 'Internal server error' });
|
|
99
125
|
}
|
|
100
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
|
+
});
|
|
101
199
|
// Créer un test
|
|
102
200
|
this.post('/test', authenticatedOptions, async (req, res) => {
|
|
103
201
|
const { title, description, targetJob, seniorityLevel, categories, state = 'draft' } = req.body;
|
|
@@ -108,6 +206,19 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
108
206
|
try {
|
|
109
207
|
const companyId = user?.companyId;
|
|
110
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
|
+
}
|
|
111
222
|
const processedCategories = await Promise.all(categories?.map(async (category) => {
|
|
112
223
|
let existingCategory = await TestCategory.findOne({ name: category.name });
|
|
113
224
|
if (!existingCategory) {
|
|
@@ -123,7 +234,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
123
234
|
userId,
|
|
124
235
|
title,
|
|
125
236
|
description,
|
|
126
|
-
targetJob,
|
|
237
|
+
targetJob: targetJobId,
|
|
127
238
|
seniorityLevel,
|
|
128
239
|
state,
|
|
129
240
|
categories: processedCategories
|
|
@@ -149,8 +260,20 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
149
260
|
test.title = title;
|
|
150
261
|
if (description)
|
|
151
262
|
test.description = description;
|
|
152
|
-
if (targetJob)
|
|
153
|
-
|
|
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
|
+
}
|
|
154
277
|
if (seniorityLevel)
|
|
155
278
|
test.seniorityLevel = seniorityLevel;
|
|
156
279
|
if (state)
|
|
@@ -185,7 +308,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
185
308
|
return res.status(404).json({ message: 'Test not found' });
|
|
186
309
|
}
|
|
187
310
|
for (let i = 0; i < test.questions.length; i++) {
|
|
188
|
-
await TestQuestion.findByIdAndDelete(test.questions[i]);
|
|
311
|
+
await TestQuestion.findByIdAndDelete(test.questions[i].questionId);
|
|
189
312
|
}
|
|
190
313
|
await TestResult.deleteMany({ testId: id });
|
|
191
314
|
await Test.findByIdAndDelete(id);
|
|
@@ -204,6 +327,8 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
204
327
|
if (!test) {
|
|
205
328
|
return res.status(404).json({ message: 'no test founded with this id' });
|
|
206
329
|
}
|
|
330
|
+
// Migration automatique si nécessaire
|
|
331
|
+
await migrateTestIfNeeded(test);
|
|
207
332
|
const questions = [];
|
|
208
333
|
for (const questionRef of test.questions) {
|
|
209
334
|
console.log(questionRef);
|
|
@@ -213,7 +338,10 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
213
338
|
questions.push(question);
|
|
214
339
|
}
|
|
215
340
|
}
|
|
216
|
-
|
|
341
|
+
// Récupérer le nom du job pour l'affichage
|
|
342
|
+
const testObj = test.toObject();
|
|
343
|
+
testObj.targetJobName = await getJobName(testObj.targetJob);
|
|
344
|
+
res.status(200).json({ test: testObj, questions });
|
|
217
345
|
}
|
|
218
346
|
catch (err) {
|
|
219
347
|
console.error('error when geting test : ', err);
|
|
@@ -236,7 +364,15 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
236
364
|
const query = {};
|
|
237
365
|
// Filtres
|
|
238
366
|
if (targetJob !== 'all') {
|
|
239
|
-
|
|
367
|
+
// Si on filtre par targetJob, on cherche d'abord le TestJob correspondant
|
|
368
|
+
const jobType = await TestJob.findOne({ name: targetJob });
|
|
369
|
+
if (jobType) {
|
|
370
|
+
query.targetJob = jobType._id;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// Si le job n'existe pas, on ne retourne aucun résultat
|
|
374
|
+
query.targetJob = null;
|
|
375
|
+
}
|
|
240
376
|
}
|
|
241
377
|
if (seniorityLevel !== 'all') {
|
|
242
378
|
query.seniorityLevel = seniorityLevel;
|
|
@@ -246,10 +382,13 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
246
382
|
}
|
|
247
383
|
// Recherche sur testName et targetJob
|
|
248
384
|
if (search) {
|
|
385
|
+
// Pour la recherche sur targetJob, on cherche d'abord les jobs qui correspondent
|
|
386
|
+
const matchingJobs = await TestJob.find({ name: { $regex: search, $options: 'i' } });
|
|
387
|
+
const jobIds = matchingJobs.map(job => job._id);
|
|
249
388
|
query.$or = [
|
|
250
389
|
{ title: { $regex: search, $options: 'i' } },
|
|
251
390
|
{ description: { $regex: search, $options: 'i' } },
|
|
252
|
-
{ targetJob: { $
|
|
391
|
+
{ targetJob: { $in: jobIds } },
|
|
253
392
|
{ seniorityLevel: { $regex: search, $options: 'i' } }
|
|
254
393
|
];
|
|
255
394
|
}
|
|
@@ -267,9 +406,13 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
267
406
|
.exec(),
|
|
268
407
|
Test.countDocuments(query)
|
|
269
408
|
]);
|
|
270
|
-
// Récupérer les noms des catégories pour chaque test
|
|
409
|
+
// Récupérer les noms des catégories et des jobs pour chaque test
|
|
271
410
|
const testsWithCategories = await Promise.all(tests.map(async (test) => {
|
|
411
|
+
// Migration automatique si nécessaire
|
|
412
|
+
await migrateTestIfNeeded(test);
|
|
272
413
|
const testObj = test.toObject();
|
|
414
|
+
// Récupérer le nom du job
|
|
415
|
+
testObj.targetJobName = await getJobName(testObj.targetJob);
|
|
273
416
|
if (testObj.categories && testObj.categories.length > 0) {
|
|
274
417
|
const categoriesWithNames = await Promise.all(testObj.categories.map(async (category) => {
|
|
275
418
|
const categoryDoc = await TestCategory.findById(category.categoryId);
|
|
@@ -478,8 +621,9 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
478
621
|
}
|
|
479
622
|
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
480
623
|
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
624
|
+
const jobName = await getJobName(test.targetJob);
|
|
481
625
|
const generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', {
|
|
482
|
-
job:
|
|
626
|
+
job: jobName,
|
|
483
627
|
seniority: test.seniorityLevel,
|
|
484
628
|
questionType,
|
|
485
629
|
category,
|
|
@@ -2,6 +2,25 @@ import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter, enduranceEv
|
|
|
2
2
|
import CandidateModel from '../models/candidate.model.js';
|
|
3
3
|
import TestResult, { TestState } from '../models/test-result.model.js';
|
|
4
4
|
import Test from '../models/test.model.js';
|
|
5
|
+
import TestJob from '../models/test-job.model.js';
|
|
6
|
+
// Fonction utilitaire pour récupérer le nom du job
|
|
7
|
+
async function getJobName(targetJob) {
|
|
8
|
+
// Si c'est déjà une string (ancien format), on la retourne directement
|
|
9
|
+
if (typeof targetJob === 'string') {
|
|
10
|
+
return targetJob;
|
|
11
|
+
}
|
|
12
|
+
// Si c'est un ObjectId, on récupère le job
|
|
13
|
+
if (targetJob && typeof targetJob === 'object' && targetJob._id) {
|
|
14
|
+
const job = await TestJob.findById(targetJob._id);
|
|
15
|
+
return job ? job.name : 'Job inconnu';
|
|
16
|
+
}
|
|
17
|
+
// Si c'est juste un ObjectId
|
|
18
|
+
if (targetJob && typeof targetJob === 'object' && targetJob.toString) {
|
|
19
|
+
const job = await TestJob.findById(targetJob);
|
|
20
|
+
return job ? job.name : 'Job inconnu';
|
|
21
|
+
}
|
|
22
|
+
return 'Job inconnu';
|
|
23
|
+
}
|
|
5
24
|
class ResultRouter extends EnduranceRouter {
|
|
6
25
|
constructor() {
|
|
7
26
|
super(EnduranceAuthMiddleware.getInstance());
|
|
@@ -81,7 +100,7 @@ class ResultRouter extends EnduranceRouter {
|
|
|
81
100
|
? {
|
|
82
101
|
title: test.title,
|
|
83
102
|
description: test.description,
|
|
84
|
-
targetJob: test.targetJob,
|
|
103
|
+
targetJob: await getJobName(test.targetJob),
|
|
85
104
|
seniorityLevel: test.seniorityLevel,
|
|
86
105
|
categories: categoriesWithNames
|
|
87
106
|
}
|
|
@@ -129,11 +148,14 @@ class ResultRouter extends EnduranceRouter {
|
|
|
129
148
|
const questions = await TestQuestion.find({ _id: { $in: (test.questions || []).map((q) => q.questionId) } }).lean();
|
|
130
149
|
const maxTime = questions.reduce((sum, q) => sum + (q.time || 0), 0);
|
|
131
150
|
const numberOfQuestions = questions.length;
|
|
151
|
+
// Récupérer le nom du job
|
|
152
|
+
const targetJobName = await getJobName(test.targetJob);
|
|
132
153
|
// Construire la réponse sans les questions
|
|
133
154
|
const { questions: _questions, // on retire les questions
|
|
134
155
|
...testWithoutQuestions } = test;
|
|
135
156
|
return res.json({
|
|
136
157
|
...testWithoutQuestions,
|
|
158
|
+
targetJobName,
|
|
137
159
|
categories: categoriesWithNames,
|
|
138
160
|
maxTime,
|
|
139
161
|
numberOfQuestions
|
|
@@ -312,18 +334,25 @@ class ResultRouter extends EnduranceRouter {
|
|
|
312
334
|
score: 0,
|
|
313
335
|
comment: ''
|
|
314
336
|
});
|
|
337
|
+
// Marquer explicitement le champ responses comme modifié
|
|
338
|
+
result.markModified('responses');
|
|
339
|
+
console.log('Avant sauvegarde - Responses:', result.responses);
|
|
315
340
|
// Vérifier si c'était la dernière question
|
|
316
341
|
const totalQuestions = test.questions.length;
|
|
317
342
|
const answeredQuestions = result.responses.length;
|
|
318
343
|
if (answeredQuestions === totalQuestions) {
|
|
319
344
|
result.state = TestState.Finish;
|
|
320
|
-
// Déclencher la correction automatique
|
|
321
|
-
await enduranceEmitter.emit(enduranceEventTypes.CORRECT_TEST, result);
|
|
322
345
|
}
|
|
323
346
|
else {
|
|
324
347
|
result.state = TestState.InProgress;
|
|
325
348
|
}
|
|
326
|
-
|
|
349
|
+
// Sauvegarder d'abord la réponse
|
|
350
|
+
const savedResult = await result.save();
|
|
351
|
+
console.log('Après sauvegarde - Responses:', savedResult.responses);
|
|
352
|
+
// Déclencher la correction automatique seulement après la sauvegarde
|
|
353
|
+
if (answeredQuestions === totalQuestions) {
|
|
354
|
+
await enduranceEmitter.emit(enduranceEventTypes.CORRECT_TEST, savedResult);
|
|
355
|
+
}
|
|
327
356
|
return res.status(200).json({
|
|
328
357
|
message: 'Réponse enregistrée',
|
|
329
358
|
response,
|