@programisto/edrm-exams 0.2.12 → 0.3.0
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/lib/openai/correctQuestion.txt +6 -10
- package/dist/modules/edrm-exams/lib/openai/createQuestion.txt +5 -67
- package/dist/modules/edrm-exams/lib/openai.d.ts +1 -0
- package/dist/modules/edrm-exams/lib/openai.js +53 -0
- package/dist/modules/edrm-exams/listeners/correct.listener.js +39 -6
- 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.d.ts +1 -0
- package/dist/modules/edrm-exams/routes/exams.router.js +219 -48
- package/dist/modules/edrm-exams/routes/result.router.js +33 -4
- package/package.json +1 -1
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
{
|
|
8
|
-
"score": score,
|
|
9
|
-
"comment": commentaire
|
|
10
|
-
}
|
|
1
|
+
Question : ${instruction}
|
|
2
|
+
Type de question : ${questionType}
|
|
3
|
+
- si MCQ les réponses possibles étaient : ${possibleResponses}
|
|
4
|
+
|
|
5
|
+
Réponse du candidat : ${response}
|
|
6
|
+
Score à donner : De 0 à ${maxScore} avec un commentaire de correction
|
|
@@ -1,68 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- voici le model pour une question :
|
|
6
|
-
const questionModel = new mongoose.Schema(
|
|
7
|
-
{
|
|
8
|
-
instruction:
|
|
9
|
-
{
|
|
10
|
-
type: String,
|
|
11
|
-
required: true
|
|
12
|
-
},
|
|
13
|
-
questionType:
|
|
14
|
-
{
|
|
15
|
-
type: String,
|
|
16
|
-
enum:
|
|
17
|
-
["MCQ", "free question", "exercice"],
|
|
18
|
-
required: true,
|
|
19
|
-
},
|
|
20
|
-
maxScore:
|
|
21
|
-
{
|
|
22
|
-
type: Number,
|
|
23
|
-
required: true,
|
|
24
|
-
},
|
|
25
|
-
possibleResponses: // only on MCQ (multiple choice responses)
|
|
26
|
-
{
|
|
27
|
-
type:
|
|
28
|
-
[{
|
|
29
|
-
possibleResponse:
|
|
30
|
-
{
|
|
31
|
-
type: String,
|
|
32
|
-
required: true,
|
|
33
|
-
},
|
|
34
|
-
valid:
|
|
35
|
-
{
|
|
36
|
-
type: Boolean,
|
|
37
|
-
required: true
|
|
38
|
-
}
|
|
39
|
-
}],
|
|
40
|
-
},
|
|
41
|
-
time: // in secondes
|
|
42
|
-
{
|
|
43
|
-
type: Number,
|
|
44
|
-
required: true,
|
|
45
|
-
},
|
|
46
|
-
textType: //type d'input demandé à l'utilisateur
|
|
47
|
-
{
|
|
48
|
-
type: String,
|
|
49
|
-
enum:
|
|
50
|
-
["text", "code"],
|
|
51
|
-
required: false
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
- toutes les instruction et les réponses doivent toujours être compatible avec LaTeX pour être afficher dans des balises <MathJax>
|
|
55
|
-
voici un exemple : Voici une formule mathématique : \\( \\frac{a}{b} = c \\)
|
|
56
|
-
- voici des containtes pour chaque type de question :
|
|
57
|
-
- MCQ
|
|
58
|
-
(il ne peut y avoir qu'une seule réponse juste sur quatre au total) le temps pour ces questions vont de 30 secondes à 1 minute
|
|
59
|
-
- free question ( ne rien mettre dans possibleResponses)
|
|
60
|
-
le temps pour ces questions vont de 30 secondes à 5 minutes en fonction de la complexité de la réponse attendue (à toi de juger)
|
|
61
|
-
- exercice ( ne rien mettre dans possibleResponses )
|
|
62
|
-
le temps pour ces questions vont de 3 à 20 minutes en fonction de la complexité de la réponse attendue (à toi de juger)
|
|
63
|
-
- les instruction et la difficulté doivent être en rapport avec targetJob et seniorityLevel de ${test}
|
|
64
|
-
- le temps pour répondre à la question dans time doit laisser le temps aux utilisateur d'y répondre, sans non plus lui laisser trop de temps pour qu'il triche (surtout sur les QCM).
|
|
65
|
-
- le format de la réponses doit être en json avec seulement les elements présent dans le model ci dessus et pas besoin d'ajouter d"_id".
|
|
66
|
-
- si la réponse attendue à une question de type free question ou exercice est un bout de code, alors remplit textType en "code", sinon laisse "text"
|
|
1
|
+
Métier ciblé : ${job}
|
|
2
|
+
Catégorie de la question : ${category} niveau ${expertiseLevel}
|
|
3
|
+
Type de question : ${questionType}
|
|
4
|
+
Format : json
|
|
67
5
|
|
|
68
|
-
|
|
6
|
+
Voici la liste des questions déjà présentes dans le test (pour éviter les doublons) : ${otherQuestions}
|
|
@@ -33,4 +33,5 @@ interface ContextBuilder {
|
|
|
33
33
|
}>;
|
|
34
34
|
}
|
|
35
35
|
export declare function generateLiveMessage(messageType: keyof ContextBuilder, params: CreateQuestionParams | CorrectQuestionParams, json?: boolean): Promise<string>;
|
|
36
|
+
export declare function generateLiveMessageAssistant(assistantId: string, messageType: keyof ContextBuilder, params: CreateQuestionParams | CorrectQuestionParams, json?: boolean): Promise<string>;
|
|
36
37
|
export {};
|
|
@@ -74,6 +74,59 @@ export async function generateLiveMessage(messageType, params, json) {
|
|
|
74
74
|
}
|
|
75
75
|
return 'Brain freezed, I cannot generate a live message right now.';
|
|
76
76
|
}
|
|
77
|
+
export async function generateLiveMessageAssistant(assistantId, messageType, params, json) {
|
|
78
|
+
const MAX_RETRY = 2;
|
|
79
|
+
let retryCount = 0;
|
|
80
|
+
// Construire le contexte pour le message
|
|
81
|
+
const context = await contextBuilder[messageType](params);
|
|
82
|
+
const text = fs.readFileSync(path.join(__dirname, 'openai', `${messageType}.txt`), 'utf8');
|
|
83
|
+
const message = text.replace(/\${(.*?)}/g, (_, v) => context[v]);
|
|
84
|
+
while (retryCount <= MAX_RETRY) {
|
|
85
|
+
try {
|
|
86
|
+
// Créer un thread avec l'assistant
|
|
87
|
+
const thread = await openai.beta.threads.create();
|
|
88
|
+
// Ajouter le message avec le contexte
|
|
89
|
+
await openai.beta.threads.messages.create(thread.id, {
|
|
90
|
+
role: 'user',
|
|
91
|
+
content: message
|
|
92
|
+
});
|
|
93
|
+
// Exécuter l'assistant
|
|
94
|
+
const run = await openai.beta.threads.runs.create(thread.id, {
|
|
95
|
+
assistant_id: assistantId
|
|
96
|
+
});
|
|
97
|
+
// Attendre que l'exécution soit terminée
|
|
98
|
+
let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
|
|
99
|
+
while (runStatus.status === 'in_progress' || runStatus.status === 'queued') {
|
|
100
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
101
|
+
runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
|
|
102
|
+
}
|
|
103
|
+
if (runStatus.status === 'failed') {
|
|
104
|
+
throw new Error('Assistant execution failed');
|
|
105
|
+
}
|
|
106
|
+
// Récupérer les messages de réponse
|
|
107
|
+
const messages = await openai.beta.threads.messages.list(thread.id);
|
|
108
|
+
const lastMessage = messages.data[0]; // Le premier message est le plus récent
|
|
109
|
+
if (!lastMessage || !lastMessage.content || lastMessage.content.length === 0) {
|
|
110
|
+
throw new Error('No content in response');
|
|
111
|
+
}
|
|
112
|
+
const content = lastMessage.content[0];
|
|
113
|
+
if (content.type === 'text') {
|
|
114
|
+
return removeQuotes(content.text.value);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
throw new Error('Unexpected content type');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
retryCount++;
|
|
122
|
+
console.log(error);
|
|
123
|
+
if (retryCount > MAX_RETRY) {
|
|
124
|
+
return 'Brain freezed, I cannot generate a live message right now.';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return 'Brain freezed, I cannot generate a live message right now.';
|
|
129
|
+
}
|
|
77
130
|
function removeQuotes(str) {
|
|
78
131
|
if (str.startsWith('"') && str.endsWith('"')) {
|
|
79
132
|
return str.substring(1, str.length - 1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { enduranceListener, enduranceEventTypes, enduranceEmitter } from '@programisto/endurance-core';
|
|
2
2
|
import TestQuestion from '../models/test-question.model.js';
|
|
3
|
-
import {
|
|
3
|
+
import { generateLiveMessageAssistant } from '../lib/openai.js';
|
|
4
4
|
import TestResult, { TestState } from '../models/test-result.model.js';
|
|
5
5
|
import CandidateModel from '../models/candidate.model.js';
|
|
6
6
|
import ContactModel from '../models/contact.model.js';
|
|
@@ -31,7 +31,7 @@ async function correctTest(options) {
|
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
33
|
maxScore += question.maxScore;
|
|
34
|
-
const scoreResponse = await
|
|
34
|
+
const scoreResponse = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CORRECT_QUESTION || '', 'correctQuestion', {
|
|
35
35
|
question: {
|
|
36
36
|
_id: question._id.toString(),
|
|
37
37
|
instruction: question.instruction,
|
|
@@ -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: {
|
|
@@ -80,7 +113,7 @@ async function correctTest(options) {
|
|
|
80
113
|
lastname: contact.lastname,
|
|
81
114
|
score: result.score,
|
|
82
115
|
testName: test?.title || '',
|
|
83
|
-
testLink
|
|
116
|
+
testLink
|
|
84
117
|
}
|
|
85
118
|
});
|
|
86
119
|
}
|
|
@@ -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,13 +3,80 @@ 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
|
-
import { generateLiveMessage } from '../lib/openai.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
|
+
}
|
|
9
34
|
class ExamsRouter extends EnduranceRouter {
|
|
10
35
|
constructor() {
|
|
11
36
|
super(EnduranceAuthMiddleware.getInstance());
|
|
12
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
|
+
}
|
|
13
80
|
setupRoutes() {
|
|
14
81
|
const authenticatedOptions = {
|
|
15
82
|
requireAuth: false,
|
|
@@ -57,6 +124,78 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
57
124
|
res.status(500).json({ message: 'Internal server error' });
|
|
58
125
|
}
|
|
59
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
|
+
});
|
|
60
199
|
// Créer un test
|
|
61
200
|
this.post('/test', authenticatedOptions, async (req, res) => {
|
|
62
201
|
const { title, description, targetJob, seniorityLevel, categories, state = 'draft' } = req.body;
|
|
@@ -67,6 +206,19 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
67
206
|
try {
|
|
68
207
|
const companyId = user?.companyId;
|
|
69
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
|
+
}
|
|
70
222
|
const processedCategories = await Promise.all(categories?.map(async (category) => {
|
|
71
223
|
let existingCategory = await TestCategory.findOne({ name: category.name });
|
|
72
224
|
if (!existingCategory) {
|
|
@@ -82,7 +234,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
82
234
|
userId,
|
|
83
235
|
title,
|
|
84
236
|
description,
|
|
85
|
-
targetJob,
|
|
237
|
+
targetJob: targetJobId,
|
|
86
238
|
seniorityLevel,
|
|
87
239
|
state,
|
|
88
240
|
categories: processedCategories
|
|
@@ -108,8 +260,20 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
108
260
|
test.title = title;
|
|
109
261
|
if (description)
|
|
110
262
|
test.description = description;
|
|
111
|
-
if (targetJob)
|
|
112
|
-
|
|
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
|
+
}
|
|
113
277
|
if (seniorityLevel)
|
|
114
278
|
test.seniorityLevel = seniorityLevel;
|
|
115
279
|
if (state)
|
|
@@ -163,6 +327,8 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
163
327
|
if (!test) {
|
|
164
328
|
return res.status(404).json({ message: 'no test founded with this id' });
|
|
165
329
|
}
|
|
330
|
+
// Migration automatique si nécessaire
|
|
331
|
+
await migrateTestIfNeeded(test);
|
|
166
332
|
const questions = [];
|
|
167
333
|
for (const questionRef of test.questions) {
|
|
168
334
|
console.log(questionRef);
|
|
@@ -172,7 +338,10 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
172
338
|
questions.push(question);
|
|
173
339
|
}
|
|
174
340
|
}
|
|
175
|
-
|
|
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 });
|
|
176
345
|
}
|
|
177
346
|
catch (err) {
|
|
178
347
|
console.error('error when geting test : ', err);
|
|
@@ -195,7 +364,15 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
195
364
|
const query = {};
|
|
196
365
|
// Filtres
|
|
197
366
|
if (targetJob !== 'all') {
|
|
198
|
-
|
|
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
|
+
}
|
|
199
376
|
}
|
|
200
377
|
if (seniorityLevel !== 'all') {
|
|
201
378
|
query.seniorityLevel = seniorityLevel;
|
|
@@ -205,10 +382,13 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
205
382
|
}
|
|
206
383
|
// Recherche sur testName et targetJob
|
|
207
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);
|
|
208
388
|
query.$or = [
|
|
209
389
|
{ title: { $regex: search, $options: 'i' } },
|
|
210
390
|
{ description: { $regex: search, $options: 'i' } },
|
|
211
|
-
{ targetJob: { $
|
|
391
|
+
{ targetJob: { $in: jobIds } },
|
|
212
392
|
{ seniorityLevel: { $regex: search, $options: 'i' } }
|
|
213
393
|
];
|
|
214
394
|
}
|
|
@@ -226,9 +406,13 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
226
406
|
.exec(),
|
|
227
407
|
Test.countDocuments(query)
|
|
228
408
|
]);
|
|
229
|
-
// 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
|
|
230
410
|
const testsWithCategories = await Promise.all(tests.map(async (test) => {
|
|
411
|
+
// Migration automatique si nécessaire
|
|
412
|
+
await migrateTestIfNeeded(test);
|
|
231
413
|
const testObj = test.toObject();
|
|
414
|
+
// Récupérer le nom du job
|
|
415
|
+
testObj.targetJobName = await getJobName(testObj.targetJob);
|
|
232
416
|
if (testObj.categories && testObj.categories.length > 0) {
|
|
233
417
|
const categoriesWithNames = await Promise.all(testObj.categories.map(async (category) => {
|
|
234
418
|
const categoryDoc = await TestCategory.findById(category.categoryId);
|
|
@@ -437,8 +621,9 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
437
621
|
}
|
|
438
622
|
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
439
623
|
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
440
|
-
const
|
|
441
|
-
|
|
624
|
+
const jobName = await getJobName(test.targetJob);
|
|
625
|
+
const generatedQuestion = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CREATE_QUESTION || '', 'createQuestion', {
|
|
626
|
+
job: jobName,
|
|
442
627
|
seniority: test.seniorityLevel,
|
|
443
628
|
questionType,
|
|
444
629
|
category,
|
|
@@ -715,7 +900,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
715
900
|
const question = await TestQuestion.findById(response.questionId);
|
|
716
901
|
if (!question)
|
|
717
902
|
continue;
|
|
718
|
-
const score = await
|
|
903
|
+
const score = await generateLiveMessageAssistant(process.env.OPENAI_ASSISTANT_ID_CORRECT_QUESTION || '', 'correctQuestion', {
|
|
719
904
|
question: {
|
|
720
905
|
_id: question._id.toString(),
|
|
721
906
|
instruction: question.instruction,
|
|
@@ -819,45 +1004,32 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
819
1004
|
}
|
|
820
1005
|
const generatedQuestions = [];
|
|
821
1006
|
let questionsGenerated = 0;
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
832
|
-
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
833
|
-
// Calculer combien de questions générer pour cette catégorie
|
|
834
|
-
const remainingQuestions = numberOfQuestions - questionsGenerated;
|
|
835
|
-
const questionsForThisCategory = Math.min(remainingQuestions, Math.ceil(numberOfQuestions / categoriesToUse.length));
|
|
836
|
-
for (let i = 0; i < questionsForThisCategory; i++) {
|
|
837
|
-
const generatedQuestion = await generateLiveMessage('createQuestion', {
|
|
838
|
-
job: test.targetJob,
|
|
839
|
-
seniority: test.seniorityLevel,
|
|
840
|
-
category: categoryDoc.name,
|
|
841
|
-
questionType: ['MCQ', 'free question', 'exercice'][Math.floor(Math.random() * 3)],
|
|
842
|
-
expertiseLevel: categoryInfo.expertiseLevel,
|
|
843
|
-
otherQuestions: otherQuestions.map(question => question.instruction).join('\n')
|
|
844
|
-
}, true);
|
|
845
|
-
// Vérifier si la réponse est un JSON valide
|
|
846
|
-
if (generatedQuestion === 'Brain freezed, I cannot generate a live message right now.') {
|
|
847
|
-
console.error('Échec de génération de question pour la catégorie:', categoryDoc.name);
|
|
848
|
-
continue; // Passer à la question suivante
|
|
849
|
-
}
|
|
850
|
-
try {
|
|
851
|
-
const question = new TestQuestion(JSON.parse(generatedQuestion));
|
|
852
|
-
await question.save();
|
|
1007
|
+
let attempts = 0;
|
|
1008
|
+
const maxAttempts = numberOfQuestions * 3; // Limite pour éviter les boucles infinies
|
|
1009
|
+
// Si on spécifie une catégorie, on génère toutes les questions pour cette catégorie
|
|
1010
|
+
if (category && category !== 'ALL') {
|
|
1011
|
+
const categoryInfo = categoriesToUse[0];
|
|
1012
|
+
while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
|
|
1013
|
+
attempts++;
|
|
1014
|
+
const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
|
|
1015
|
+
if (question) {
|
|
853
1016
|
generatedQuestions.push(question);
|
|
854
|
-
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
855
1017
|
questionsGenerated++;
|
|
856
1018
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
// Pour ALL, répartition aléatoire sur toutes les catégories
|
|
1023
|
+
const shuffledCategories = [...categoriesToUse].sort(() => Math.random() - 0.5);
|
|
1024
|
+
while (questionsGenerated < numberOfQuestions && attempts < maxAttempts) {
|
|
1025
|
+
attempts++;
|
|
1026
|
+
// Sélectionner une catégorie aléatoire
|
|
1027
|
+
const randomCategoryIndex = Math.floor(Math.random() * shuffledCategories.length);
|
|
1028
|
+
const categoryInfo = shuffledCategories[randomCategoryIndex];
|
|
1029
|
+
const question = await this.generateAndSaveQuestion(test, categoryInfo, true);
|
|
1030
|
+
if (question) {
|
|
1031
|
+
generatedQuestions.push(question);
|
|
1032
|
+
questionsGenerated++;
|
|
861
1033
|
}
|
|
862
1034
|
}
|
|
863
1035
|
}
|
|
@@ -867,7 +1039,6 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
867
1039
|
message: 'Aucune question n\'a pu être générée. Veuillez réessayer plus tard.'
|
|
868
1040
|
});
|
|
869
1041
|
}
|
|
870
|
-
await test.save();
|
|
871
1042
|
res.status(200).json({
|
|
872
1043
|
message: `${generatedQuestions.length} question(s) générée(s) avec succès`,
|
|
873
1044
|
questions: generatedQuestions,
|
|
@@ -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,
|