@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.
@@ -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: result.score,
65
+ score: scorePercentage,
61
66
  state: result.state
62
67
  }
63
68
  });
64
- console.log('Test correction completed and saved', { finalScore: finalscore });
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.models.js';
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.models.js';
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
- for (const categoryInfo of categoriesToUse) {
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
- for (let i = 0; i < questionsPerCategory; i++) {
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
- const candidates = await Candidate.find({
899
+ // D'abord, rechercher dans les contacts
900
+ const contacts = await ContactModel.find({
887
901
  $or: [
888
- { firstName: { $regex: search, $options: 'i' } },
889
- { lastName: { $regex: search, $options: 'i' } },
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
- const [results, total] = await Promise.all([
897
- TestResult.find(query)
898
- .sort({ invitationDate: -1 })
899
- .skip(skip)
900
- .limit(limit)
901
- .exec(),
902
- TestResult.countDocuments(query)
903
- ]);
904
- // Récupérer les données des candidats
905
- const candidateIds = results.map(result => result.candidateId);
906
- const candidates = await Candidate.find({ _id: { $in: candidateIds } });
907
- const candidatesMap = new Map(candidates.map(c => [c._id.toString(), c]));
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
- // Combiner les résultats avec les données des candidats
916
- const resultsWithCandidates = await Promise.all(results.map(async (result) => {
917
- const candidate = candidatesMap.get(result.candidateId.toString());
918
- if (!candidate) {
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: null,
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
- // Récupérer le contact pour obtenir les informations personnelles
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.models.js';
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.2.5",
4
+ "version": "0.2.7",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },