@programisto/edrm-exams 0.3.11 → 0.3.12

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.
@@ -0,0 +1,18 @@
1
+ import type { CategoryScore } from '../models/test-result.model.js';
2
+ export interface ComputeScoresResult {
3
+ score: number;
4
+ scoresByCategory: CategoryScore[];
5
+ }
6
+ /** Objet ayant au minimum testId et responses (document TestResult ou équivalent). */
7
+ export interface ResultWithResponses {
8
+ testId: unknown;
9
+ responses?: Array<{
10
+ questionId: unknown;
11
+ score?: number;
12
+ }>;
13
+ }
14
+ /**
15
+ * Calcule le score global et les sous-scores par catégorie à partir des réponses d'un TestResult.
16
+ * Les questions sans categoryId contribuent au score global uniquement (pas d'entrée dans scoresByCategory).
17
+ */
18
+ export declare function computeScoresByCategory(result: ResultWithResponses): Promise<ComputeScoresResult>;
@@ -0,0 +1,48 @@
1
+ import Test from '../models/test.model.js';
2
+ import TestQuestion from '../models/test-question.model.js';
3
+ /**
4
+ * Calcule le score global et les sous-scores par catégorie à partir des réponses d'un TestResult.
5
+ * Les questions sans categoryId contribuent au score global uniquement (pas d'entrée dans scoresByCategory).
6
+ */
7
+ export async function computeScoresByCategory(result) {
8
+ const responses = result.responses || [];
9
+ let score = 0;
10
+ const categoryMap = new Map();
11
+ const test = await Test.findById(result.testId).lean();
12
+ if (!test) {
13
+ return { score: 0, scoresByCategory: [] };
14
+ }
15
+ for (const res of responses) {
16
+ const questionId = res.questionId?.toString?.() ?? res.questionId;
17
+ if (!questionId)
18
+ continue;
19
+ const question = await TestQuestion.findById(questionId).lean();
20
+ if (!question)
21
+ continue;
22
+ const responseScore = typeof res.score === 'number' && !isNaN(res.score) ? res.score : 0;
23
+ const questionMaxScore = typeof question.maxScore === 'number' && !isNaN(question.maxScore) ? question.maxScore : 0;
24
+ score += responseScore;
25
+ const catId = question.categoryId;
26
+ if (catId) {
27
+ const key = catId.toString?.() ?? String(catId);
28
+ const existing = categoryMap.get(key);
29
+ if (existing) {
30
+ existing.score += responseScore;
31
+ existing.maxScore += questionMaxScore;
32
+ }
33
+ else {
34
+ categoryMap.set(key, {
35
+ categoryId: catId,
36
+ score: responseScore,
37
+ maxScore: questionMaxScore
38
+ });
39
+ }
40
+ }
41
+ }
42
+ const scoresByCategory = Array.from(categoryMap.values()).map(({ categoryId, score: catScore, maxScore: catMax }) => ({
43
+ categoryId: categoryId,
44
+ score: catScore,
45
+ maxScore: catMax
46
+ }));
47
+ return { score, scoresByCategory };
48
+ }
@@ -1,6 +1,7 @@
1
1
  import { enduranceListener, enduranceEventTypes, enduranceEmitter } from '@programisto/endurance';
2
2
  import TestQuestion from '../models/test-question.model.js';
3
3
  import { generateLiveMessageAssistant } from '../lib/openai.js';
4
+ import { computeScoresByCategory } from '../lib/score-utils.js';
4
5
  import TestResult, { TestState } from '../models/test-result.model.js';
5
6
  import CandidateModel from '../models/candidate.model.js';
6
7
  import ContactModel from '../models/contact.model.js';
@@ -97,6 +98,9 @@ async function correctTest(options) {
97
98
  result.state = TestState.Finish;
98
99
  // Forcer la sauvegarde des sous-documents responses
99
100
  result.markModified('responses');
101
+ // Calculer les sous-scores par catégorie
102
+ const { scoresByCategory } = await computeScoresByCategory(result);
103
+ result.scoresByCategory = scoresByCategory;
100
104
  // Calculer le pourcentage de score en évitant la division par zéro
101
105
  let scorePercentage = 0;
102
106
  if (maxScore > 0) {
@@ -115,6 +119,7 @@ async function correctTest(options) {
115
119
  $set: {
116
120
  responses: result.responses,
117
121
  score: finalscore,
122
+ scoresByCategory: result.scoresByCategory,
118
123
  state: result.state
119
124
  }
120
125
  });
@@ -1,4 +1,5 @@
1
1
  import { EnduranceSchema } from '@programisto/endurance';
2
+ import TestCategory from './test-category.models.js';
2
3
  declare enum QuestionType {
3
4
  MCQ = "MCQ",
4
5
  FreeQuestion = "free question",
@@ -19,7 +20,8 @@ declare class TestQuestion extends EnduranceSchema {
19
20
  possibleResponses: PossibleResponse[];
20
21
  time: number;
21
22
  textType: TextType;
22
- static getModel(): import("@typegoose/typegoose").ReturnModelType<typeof TestQuestion, import("@typegoose/typegoose/lib/types").BeAnObject>;
23
+ categoryId?: typeof TestCategory;
24
+ static getModel(): import("@typegoose/typegoose").ReturnModelType<typeof TestQuestion, import("@typegoose/typegoose/lib/types.js").BeAnObject>;
23
25
  }
24
- declare const TestQuestionModel: import("@typegoose/typegoose").ReturnModelType<typeof TestQuestion, import("@typegoose/typegoose/lib/types").BeAnObject>;
26
+ declare const TestQuestionModel: import("@typegoose/typegoose").ReturnModelType<typeof TestQuestion, import("@typegoose/typegoose/lib/types.js").BeAnObject>;
25
27
  export default TestQuestionModel;
@@ -8,6 +8,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
10
  import { EnduranceSchema, EnduranceModelType } from '@programisto/endurance';
11
+ import TestCategory from './test-category.models.js';
11
12
  var QuestionType;
12
13
  (function (QuestionType) {
13
14
  // eslint-disable-next-line no-unused-vars
@@ -31,6 +32,7 @@ let TestQuestion = class TestQuestion extends EnduranceSchema {
31
32
  possibleResponses;
32
33
  time;
33
34
  textType;
35
+ categoryId;
34
36
  static getModel() {
35
37
  return TestQuestionModel;
36
38
  }
@@ -59,6 +61,10 @@ __decorate([
59
61
  EnduranceModelType.prop({ required: false, enum: TextType, default: TextType.Text }),
60
62
  __metadata("design:type", String)
61
63
  ], TestQuestion.prototype, "textType", void 0);
64
+ __decorate([
65
+ EnduranceModelType.prop({ ref: () => TestCategory, required: false }),
66
+ __metadata("design:type", Object)
67
+ ], TestQuestion.prototype, "categoryId", void 0);
62
68
  TestQuestion = __decorate([
63
69
  EnduranceModelType.modelOptions({
64
70
  options: {
@@ -12,12 +12,18 @@ interface Response {
12
12
  score?: number;
13
13
  comment?: string;
14
14
  }
15
+ export interface CategoryScore {
16
+ categoryId: ObjectId;
17
+ score: number;
18
+ maxScore: number;
19
+ }
15
20
  declare class TestResult extends EnduranceSchema {
16
21
  testId: typeof Test;
17
22
  candidateId: typeof User;
18
23
  state: TestState;
19
24
  responses: Response[];
20
25
  score?: number;
26
+ scoresByCategory?: CategoryScore[];
21
27
  startTime?: Date;
22
28
  endTime?: Date;
23
29
  static getModel(): import("@typegoose/typegoose").ReturnModelType<typeof TestResult, import("@typegoose/typegoose/lib/types.js").BeAnObject>;
@@ -25,6 +25,7 @@ let TestResult = class TestResult extends EnduranceSchema {
25
25
  state;
26
26
  responses;
27
27
  score;
28
+ scoresByCategory;
28
29
  startTime;
29
30
  endTime;
30
31
  static getModel() {
@@ -51,6 +52,10 @@ __decorate([
51
52
  EnduranceModelType.prop(),
52
53
  __metadata("design:type", Number)
53
54
  ], TestResult.prototype, "score", void 0);
55
+ __decorate([
56
+ EnduranceModelType.prop({ type: [Object], required: false }),
57
+ __metadata("design:type", Array)
58
+ ], TestResult.prototype, "scoresByCategory", void 0);
54
59
  __decorate([
55
60
  EnduranceModelType.prop(),
56
61
  __metadata("design:type", Date)
@@ -7,6 +7,7 @@ import TestJob from '../models/test-job.model.js';
7
7
  import Candidate from '../models/candidate.model.js';
8
8
  import ContactModel from '../models/contact.model.js';
9
9
  import { generateLiveMessage, generateLiveMessageAssistant } from '../lib/openai.js';
10
+ import { computeScoresByCategory } from '../lib/score-utils.js';
10
11
  // Fonction utilitaire pour récupérer le nom du job
11
12
  async function getJobName(targetJob) {
12
13
  // Si c'est déjà une string (ancien format), on la retourne directement
@@ -1092,7 +1093,7 @@ class ExamsRouter extends EnduranceRouter {
1092
1093
  */
1093
1094
  this.put('/test/modifyQuestion/:id', authenticatedOptions, async (req, res) => {
1094
1095
  const { id } = req.params;
1095
- const { instruction, maxScore, time, possibleResponses, textType } = req.body;
1096
+ const { instruction, maxScore, time, possibleResponses, textType, categoryId } = req.body;
1096
1097
  try {
1097
1098
  const question = await TestQuestion.findById(id);
1098
1099
  if (!question) {
@@ -1113,6 +1114,18 @@ class ExamsRouter extends EnduranceRouter {
1113
1114
  if (possibleResponses) {
1114
1115
  question.possibleResponses = possibleResponses;
1115
1116
  }
1117
+ if (categoryId !== undefined) {
1118
+ if (categoryId === null || categoryId === '') {
1119
+ question.categoryId = undefined;
1120
+ }
1121
+ else {
1122
+ const category = await TestCategory.findById(categoryId);
1123
+ if (!category) {
1124
+ return res.status(400).json({ message: 'Catégorie non trouvée' });
1125
+ }
1126
+ question.categoryId = categoryId;
1127
+ }
1128
+ }
1116
1129
  await question.save();
1117
1130
  res.status(200).json({ message: 'question modified with sucess' });
1118
1131
  }
@@ -1159,7 +1172,7 @@ class ExamsRouter extends EnduranceRouter {
1159
1172
  */
1160
1173
  this.put('/test/addCustomQuestion/:id', authenticatedOptions, async (req, res) => {
1161
1174
  const { id } = req.params;
1162
- const { questionType, instruction, maxScore, time } = req.body;
1175
+ const { questionType, instruction, maxScore, time, categoryId } = req.body;
1163
1176
  try {
1164
1177
  const test = await Test.findById(id);
1165
1178
  if (!test) {
@@ -1168,11 +1181,18 @@ class ExamsRouter extends EnduranceRouter {
1168
1181
  if (!testBelongsToEntity(test, req)) {
1169
1182
  return res.status(404).json({ message: 'no test founded with this id' });
1170
1183
  }
1184
+ if (categoryId != null && categoryId !== '') {
1185
+ const category = await TestCategory.findById(categoryId);
1186
+ if (!category) {
1187
+ return res.status(400).json({ message: 'Catégorie non trouvée' });
1188
+ }
1189
+ }
1171
1190
  const question = new TestQuestion({
1172
1191
  questionType,
1173
1192
  instruction,
1174
1193
  maxScore,
1175
- time
1194
+ time,
1195
+ ...(categoryId != null && categoryId !== '' ? { categoryId } : {})
1176
1196
  });
1177
1197
  await question.save();
1178
1198
  test.questions.push({ questionId: question._id, order: test.questions.length });
@@ -1792,6 +1812,8 @@ class ExamsRouter extends EnduranceRouter {
1792
1812
  response.comment = parsedResult.comment;
1793
1813
  }
1794
1814
  result.score = finalscore;
1815
+ const { scoresByCategory } = await computeScoresByCategory(result);
1816
+ result.scoresByCategory = scoresByCategory;
1795
1817
  await result.save();
1796
1818
  res.status(200).json({ data: finalscore });
1797
1819
  }
@@ -2350,8 +2372,10 @@ class ExamsRouter extends EnduranceRouter {
2350
2372
  response.score = score;
2351
2373
  if (typeof comment === 'string')
2352
2374
  response.comment = comment;
2353
- // Recalculer le score global
2354
- result.score = (result.responses || []).reduce((sum, r) => sum + (r.score || 0), 0);
2375
+ // Recalculer le score global et les sous-scores par catégorie
2376
+ const { score: globalScore, scoresByCategory } = await computeScoresByCategory(result);
2377
+ result.score = globalScore;
2378
+ result.scoresByCategory = scoresByCategory;
2355
2379
  await result.save();
2356
2380
  return res.status(200).json({
2357
2381
  message: 'Correction manuelle enregistrée',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@programisto/edrm-exams",
4
- "version": "0.3.11",
4
+ "version": "0.3.12",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },