@programisto/edrm-exams 0.2.5 → 0.2.7
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 +26 -4
- package/dist/modules/edrm-exams/routes/exams-candidate.router.js +1 -1
- package/dist/modules/edrm-exams/routes/exams.router.js +122 -40
- package/dist/modules/edrm-exams/routes/result.router.js +1 -1
- package/package.json +1 -1
- /package/dist/modules/edrm-exams/models/{candidate.models.d.ts → candidate.model.d.ts} +0 -0
- /package/dist/modules/edrm-exams/models/{candidate.models.js → candidate.model.js} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { enduranceListener, enduranceEventTypes } from '@programisto/endurance-core';
|
|
1
|
+
import { enduranceListener, enduranceEventTypes, enduranceEmitter } from '@programisto/endurance-core';
|
|
2
2
|
import TestQuestion from '../models/test-question.model.js';
|
|
3
3
|
import { generateLiveMessage } from '../lib/openai.js';
|
|
4
4
|
import TestResult, { TestState } from '../models/test-result.model.js';
|
|
5
|
+
import CandidateModel from '../models/candidate.model.js';
|
|
6
|
+
import ContactModel from '../models/contact.model.js';
|
|
7
|
+
import TestModel from '../models/test.model.js';
|
|
5
8
|
async function correctTest(options) {
|
|
6
|
-
console.log('Correcting test', { options });
|
|
7
9
|
if (!options.testId)
|
|
8
10
|
throw new Error('TestId is required');
|
|
9
11
|
if (!options.responses)
|
|
@@ -17,6 +19,7 @@ async function correctTest(options) {
|
|
|
17
19
|
throw new Error('Test result not found');
|
|
18
20
|
}
|
|
19
21
|
let finalscore = 0;
|
|
22
|
+
let maxScore = 0;
|
|
20
23
|
// Pour chaque réponse enregistrée en base, on cherche la correction correspondante
|
|
21
24
|
for (const dbResponse of result.responses) {
|
|
22
25
|
const correction = options.responses.find(r => r.questionId.toString() === dbResponse.questionId.toString());
|
|
@@ -27,6 +30,7 @@ async function correctTest(options) {
|
|
|
27
30
|
console.error('Question not found', { questionId: dbResponse.questionId });
|
|
28
31
|
continue;
|
|
29
32
|
}
|
|
33
|
+
maxScore += question.maxScore;
|
|
30
34
|
const scoreResponse = await generateLiveMessage('correctQuestion', {
|
|
31
35
|
question: {
|
|
32
36
|
_id: question._id.toString(),
|
|
@@ -53,15 +57,33 @@ async function correctTest(options) {
|
|
|
53
57
|
result.state = TestState.Finish;
|
|
54
58
|
// Forcer la sauvegarde des sous-documents responses
|
|
55
59
|
result.markModified('responses');
|
|
60
|
+
const scorePercentage = (finalscore / maxScore) * 100;
|
|
56
61
|
// Sauvegarder les modifications avec findByIdAndUpdate pour éviter les conflits de version
|
|
57
62
|
await TestResult.findByIdAndUpdate(result._id, {
|
|
58
63
|
$set: {
|
|
59
64
|
responses: result.responses,
|
|
60
|
-
score:
|
|
65
|
+
score: scorePercentage,
|
|
61
66
|
state: result.state
|
|
62
67
|
}
|
|
63
68
|
});
|
|
64
|
-
|
|
69
|
+
const test = await TestModel.findById(result.testId);
|
|
70
|
+
const candidate = await CandidateModel.findById(result.candidateId);
|
|
71
|
+
if (candidate) {
|
|
72
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
73
|
+
if (contact) {
|
|
74
|
+
enduranceEmitter.emit(enduranceEventTypes.SEND_EMAIL, {
|
|
75
|
+
template: 'test-result',
|
|
76
|
+
to: contact.email,
|
|
77
|
+
data: {
|
|
78
|
+
firstname: contact.firstname,
|
|
79
|
+
lastname: contact.lastname,
|
|
80
|
+
score: result.score,
|
|
81
|
+
testName: test?.title || '',
|
|
82
|
+
testLink: process.env.TEST_INVITATION_LINK || ''
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
65
87
|
}
|
|
66
88
|
catch (err) {
|
|
67
89
|
if (err instanceof Error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter, enduranceEventTypes } from '@programisto/endurance-core';
|
|
2
|
-
import CandidateModel from '../models/candidate.
|
|
2
|
+
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';
|
|
@@ -3,7 +3,7 @@ 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 Candidate from '../models/candidate.
|
|
6
|
+
import Candidate from '../models/candidate.model.js';
|
|
7
7
|
import ContactModel from '../models/contact.model.js';
|
|
8
8
|
import { generateLiveMessage } from '../lib/openai.js';
|
|
9
9
|
class ExamsRouter extends EnduranceRouter {
|
|
@@ -565,6 +565,8 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
565
565
|
emailUser,
|
|
566
566
|
emailPassword,
|
|
567
567
|
data: {
|
|
568
|
+
firstname: contact.firstname,
|
|
569
|
+
testName: test?.title || '',
|
|
568
570
|
testLink
|
|
569
571
|
}
|
|
570
572
|
});
|
|
@@ -810,15 +812,23 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
810
812
|
if (categoriesToUse.length === 0) {
|
|
811
813
|
return res.status(400).json({ message: 'Aucune catégorie disponible pour générer des questions' });
|
|
812
814
|
}
|
|
813
|
-
const questionsPerCategory = Math.ceil(numberOfQuestions / categoriesToUse.length);
|
|
814
815
|
const generatedQuestions = [];
|
|
815
|
-
|
|
816
|
+
let questionsGenerated = 0;
|
|
817
|
+
// Mélanger les catégories pour une répartition aléatoire
|
|
818
|
+
const shuffledCategories = [...categoriesToUse].sort(() => Math.random() - 0.5);
|
|
819
|
+
for (const categoryInfo of shuffledCategories) {
|
|
820
|
+
// Arrêter si on a déjà généré le nombre de questions demandé
|
|
821
|
+
if (questionsGenerated >= numberOfQuestions)
|
|
822
|
+
break;
|
|
816
823
|
const categoryDoc = await TestCategory.findById(categoryInfo.categoryId);
|
|
817
824
|
if (!categoryDoc)
|
|
818
825
|
continue;
|
|
819
826
|
const otherQuestionsIds = test.questions.map(question => question.questionId);
|
|
820
827
|
const otherQuestions = await TestQuestion.find({ _id: { $in: otherQuestionsIds } });
|
|
821
|
-
|
|
828
|
+
// Calculer combien de questions générer pour cette catégorie
|
|
829
|
+
const remainingQuestions = numberOfQuestions - questionsGenerated;
|
|
830
|
+
const questionsForThisCategory = Math.min(remainingQuestions, 1); // Au maximum 1 question par catégorie
|
|
831
|
+
for (let i = 0; i < questionsForThisCategory; i++) {
|
|
822
832
|
const generatedQuestion = await generateLiveMessage('createQuestion', {
|
|
823
833
|
job: test.targetJob,
|
|
824
834
|
seniority: test.seniorityLevel,
|
|
@@ -837,6 +847,7 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
837
847
|
await question.save();
|
|
838
848
|
generatedQuestions.push(question);
|
|
839
849
|
test.questions.push({ questionId: question._id, order: test.questions.length });
|
|
850
|
+
questionsGenerated++;
|
|
840
851
|
}
|
|
841
852
|
catch (parseError) {
|
|
842
853
|
console.error('Erreur lors du parsing de la question générée:', parseError);
|
|
@@ -871,6 +882,8 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
871
882
|
const skip = (page - 1) * limit;
|
|
872
883
|
const search = req.query.search || '';
|
|
873
884
|
const state = req.query.state || 'all';
|
|
885
|
+
const sortBy = req.query.sortBy || 'invitationDate';
|
|
886
|
+
const sortOrder = req.query.sortOrder || 'desc';
|
|
874
887
|
try {
|
|
875
888
|
const test = await Test.findById(testId);
|
|
876
889
|
if (!test) {
|
|
@@ -881,30 +894,84 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
881
894
|
if (state !== 'all') {
|
|
882
895
|
query.state = state;
|
|
883
896
|
}
|
|
884
|
-
// Recherche sur les candidats
|
|
897
|
+
// Recherche sur les candidats via leurs contacts
|
|
885
898
|
if (search) {
|
|
886
|
-
|
|
899
|
+
// D'abord, rechercher dans les contacts
|
|
900
|
+
const contacts = await ContactModel.find({
|
|
887
901
|
$or: [
|
|
888
|
-
{
|
|
889
|
-
{
|
|
902
|
+
{ firstname: { $regex: search, $options: 'i' } },
|
|
903
|
+
{ lastname: { $regex: search, $options: 'i' } },
|
|
890
904
|
{ email: { $regex: search, $options: 'i' } }
|
|
891
905
|
]
|
|
892
906
|
});
|
|
907
|
+
// Ensuite, récupérer les candidats qui ont ces contacts
|
|
908
|
+
const contactIds = contacts.map(c => c._id);
|
|
909
|
+
const candidates = await Candidate.find({
|
|
910
|
+
contact: { $in: contactIds }
|
|
911
|
+
});
|
|
893
912
|
const candidateIds = candidates.map(c => c._id);
|
|
894
913
|
query.candidateId = { $in: candidateIds };
|
|
895
914
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
915
|
+
// Déterminer l'ordre de tri
|
|
916
|
+
const sortDirection = sortOrder === 'asc' ? 1 : -1;
|
|
917
|
+
// Si on trie par lastName, on récupère tous les résultats puis on trie après
|
|
918
|
+
// Sinon on peut trier directement dans la requête MongoDB
|
|
919
|
+
let results, total;
|
|
920
|
+
if (sortBy === 'lastName') {
|
|
921
|
+
// Récupérer tous les résultats sans pagination pour pouvoir trier par lastName
|
|
922
|
+
const allResults = await TestResult.find(query).exec();
|
|
923
|
+
total = allResults.length;
|
|
924
|
+
// Récupérer les données des candidats pour le tri
|
|
925
|
+
const candidateIds = allResults.map(result => result.candidateId);
|
|
926
|
+
const candidates = await Candidate.find({ _id: { $in: candidateIds } });
|
|
927
|
+
const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
|
|
928
|
+
// Combiner les résultats avec les données des candidats et trier
|
|
929
|
+
const resultsWithCandidates = await Promise.all(allResults.map(async (result) => {
|
|
930
|
+
const candidate = candidatesMap.get(result.candidateId.toString());
|
|
931
|
+
if (!candidate) {
|
|
932
|
+
return {
|
|
933
|
+
...result.toObject(),
|
|
934
|
+
candidate: null,
|
|
935
|
+
lastName: ''
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
939
|
+
return {
|
|
940
|
+
...result.toObject(),
|
|
941
|
+
candidate: contact
|
|
942
|
+
? {
|
|
943
|
+
firstName: contact.firstname,
|
|
944
|
+
lastName: contact.lastname,
|
|
945
|
+
email: contact.email
|
|
946
|
+
}
|
|
947
|
+
: null,
|
|
948
|
+
lastName: contact ? contact.lastname : ''
|
|
949
|
+
};
|
|
950
|
+
}));
|
|
951
|
+
// Trier par lastName
|
|
952
|
+
resultsWithCandidates.sort((a, b) => {
|
|
953
|
+
const lastNameA = (a.lastName || '').toLowerCase();
|
|
954
|
+
const lastNameB = (b.lastName || '').toLowerCase();
|
|
955
|
+
return sortDirection === 1
|
|
956
|
+
? lastNameA.localeCompare(lastNameB)
|
|
957
|
+
: lastNameB.localeCompare(lastNameA);
|
|
958
|
+
});
|
|
959
|
+
// Appliquer la pagination
|
|
960
|
+
results = resultsWithCandidates.slice(skip, skip + limit);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
// Tri direct dans MongoDB pour invitationDate
|
|
964
|
+
const sortObject = {};
|
|
965
|
+
sortObject[sortBy] = sortDirection;
|
|
966
|
+
[results, total] = await Promise.all([
|
|
967
|
+
TestResult.find(query)
|
|
968
|
+
.sort(sortObject)
|
|
969
|
+
.skip(skip)
|
|
970
|
+
.limit(limit)
|
|
971
|
+
.exec(),
|
|
972
|
+
TestResult.countDocuments(query)
|
|
973
|
+
]);
|
|
974
|
+
}
|
|
908
975
|
// Calculer le maxScore du test
|
|
909
976
|
let maxScore = 0;
|
|
910
977
|
if (test.questions && test.questions.length > 0) {
|
|
@@ -912,30 +979,45 @@ class ExamsRouter extends EnduranceRouter {
|
|
|
912
979
|
const questions = await TestQuestion.find({ _id: { $in: questionIds } }).lean();
|
|
913
980
|
maxScore = questions.reduce((sum, q) => sum + (q.maxScore || 0), 0);
|
|
914
981
|
}
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
982
|
+
// Si on a déjà traité les candidats pour le tri par lastName, on utilise directement les résultats
|
|
983
|
+
let resultsWithCandidates;
|
|
984
|
+
if (sortBy === 'lastName') {
|
|
985
|
+
// Les résultats sont déjà traités avec les données des candidats
|
|
986
|
+
resultsWithCandidates = results.map(result => ({
|
|
987
|
+
...result,
|
|
988
|
+
maxScore
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
// Récupérer les données des candidats
|
|
993
|
+
const candidateIds = results.map(result => result.candidateId);
|
|
994
|
+
const candidates = await Candidate.find({ _id: { $in: candidateIds } });
|
|
995
|
+
const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
|
|
996
|
+
// Combiner les résultats avec les données des candidats
|
|
997
|
+
resultsWithCandidates = await Promise.all(results.map(async (result) => {
|
|
998
|
+
const candidate = candidatesMap.get(result.candidateId.toString());
|
|
999
|
+
if (!candidate) {
|
|
1000
|
+
return {
|
|
1001
|
+
...result.toObject(),
|
|
1002
|
+
candidate: null,
|
|
1003
|
+
maxScore
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
// Récupérer le contact pour obtenir les informations personnelles
|
|
1007
|
+
const contact = await ContactModel.findById(candidate.contact);
|
|
919
1008
|
return {
|
|
920
1009
|
...result.toObject(),
|
|
921
|
-
candidate:
|
|
1010
|
+
candidate: contact
|
|
1011
|
+
? {
|
|
1012
|
+
firstName: contact.firstname,
|
|
1013
|
+
lastName: contact.lastname,
|
|
1014
|
+
email: contact.email
|
|
1015
|
+
}
|
|
1016
|
+
: null,
|
|
922
1017
|
maxScore
|
|
923
1018
|
};
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const contact = await ContactModel.findById(candidate.contact);
|
|
927
|
-
return {
|
|
928
|
-
...result.toObject(),
|
|
929
|
-
candidate: contact
|
|
930
|
-
? {
|
|
931
|
-
firstName: contact.firstname,
|
|
932
|
-
lastName: contact.lastname,
|
|
933
|
-
email: contact.email
|
|
934
|
-
}
|
|
935
|
-
: null,
|
|
936
|
-
maxScore
|
|
937
|
-
};
|
|
938
|
-
}));
|
|
1019
|
+
}));
|
|
1020
|
+
}
|
|
939
1021
|
const totalPages = Math.ceil(total / limit);
|
|
940
1022
|
return res.json({
|
|
941
1023
|
data: resultsWithCandidates,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EnduranceRouter, EnduranceAuthMiddleware, enduranceEmitter, enduranceEventTypes } from '@programisto/endurance-core';
|
|
2
|
-
import CandidateModel from '../models/candidate.
|
|
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
5
|
class ResultRouter extends EnduranceRouter {
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|