Skip to content

Commit 4ad3f04

Browse files
[FEATURE] Ajout du scoring pour les certifications V3 (PIX-9050).
2 parents 62c0d43 + ef61cb5 commit 4ad3f04

File tree

12 files changed

+709
-229
lines changed

12 files changed

+709
-229
lines changed

api/db/database-builder/factory/build-answered-not-completed-certification-assessment.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ const buildAnsweredNotCompletedCertificationAssessment = function ({
88
certifiableUserId,
99
competenceIdSkillIdPairs,
1010
limitDate,
11+
version = 2,
1112
}) {
1213
const certificationCourseId = buildCertificationCourse({
1314
userId: certifiableUserId,
1415
createdAt: limitDate,
15-
version: 2,
16+
version,
1617
}).id;
1718
const certificationAssessment = buildAssessment({
1819
certificationCourseId,

api/lib/domain/events/handle-certification-scoring.js

+50
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import bluebird from 'bluebird';
55
import { CertificationComputeError } from '../errors.js';
66
import { AssessmentCompleted } from './AssessmentCompleted.js';
77
import { checkEventTypes } from './check-event-types.js';
8+
import { CertificationVersion } from '../../../src/shared/domain/models/CertificationVersion.js';
9+
import { CertificationAssessmentScoreV3 } from '../models/CertificationAssessmentScoreV3.js';
810

911
const eventTypes = [AssessmentCompleted];
1012
const EMITTER = 'PIX-ALGO';
@@ -17,11 +19,26 @@ async function handleCertificationScoring({
1719
certificationCourseRepository,
1820
competenceMarkRepository,
1921
scoringCertificationService,
22+
answerRepository,
23+
challengeRepository,
2024
}) {
2125
checkEventTypes(event, eventTypes);
2226

2327
if (event.isCertificationType) {
2428
const certificationAssessment = await certificationAssessmentRepository.get(event.assessmentId);
29+
30+
if (certificationAssessment.version === CertificationVersion.V3) {
31+
return _handleV3CertificationScoring({
32+
challengeRepository,
33+
answerRepository,
34+
assessmentId: event.assessmentId,
35+
certificationAssessment,
36+
assessmentResultRepository,
37+
certificationCourseRepository,
38+
competenceMarkRepository,
39+
});
40+
}
41+
2542
return _calculateCertificationScore({
2643
certificationAssessment,
2744
assessmentResultRepository,
@@ -72,6 +89,39 @@ async function _calculateCertificationScore({
7289
}
7390
}
7491

92+
async function _handleV3CertificationScoring({
93+
challengeRepository,
94+
answerRepository,
95+
assessmentId,
96+
certificationAssessment,
97+
assessmentResultRepository,
98+
certificationCourseRepository,
99+
competenceMarkRepository,
100+
}) {
101+
const allAnswers = await answerRepository.findByAssessment(assessmentId);
102+
const challengeIds = allAnswers.map(({ challengeId }) => challengeId);
103+
const challenges = await challengeRepository.getMany(challengeIds);
104+
105+
const certificationAssessmentScore = CertificationAssessmentScoreV3.fromChallengesAndAnswers({
106+
challenges,
107+
allAnswers,
108+
});
109+
110+
await _saveResult({
111+
certificationAssessment,
112+
certificationAssessmentScore,
113+
assessmentResultRepository,
114+
certificationCourseRepository,
115+
competenceMarkRepository,
116+
});
117+
118+
return new CertificationScoringCompleted({
119+
userId: certificationAssessment.userId,
120+
certificationCourseId: certificationAssessment.certificationCourseId,
121+
reproducibilityRate: certificationAssessmentScore.getPercentageCorrectAnswers(),
122+
});
123+
}
124+
75125
async function _saveResult({
76126
certificationAssessment,
77127
certificationAssessmentScore,

api/lib/domain/events/index.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import _ from 'lodash';
77
import perf_hooks from 'perf_hooks';
88
import * as eventBusBuilder from '../../infrastructure/events/EventBusBuilder.js';
99

10+
import * as answerRepository from '../../infrastructure/repositories/answer-repository.js';
1011
import * as authenticationMethodRepository from '../../infrastructure/repositories/authentication-method-repository.js';
1112
import * as assessmentRepository from '../../infrastructure/repositories/assessment-repository.js';
1213
import * as assessmentResultRepository from '../../infrastructure/repositories/assessment-result-repository.js';
@@ -19,6 +20,7 @@ import * as certificationAssessmentRepository from '../../infrastructure/reposit
1920
import * as certificationCenterRepository from '../../../src/certification/shared/infrastructure/repositories/certification-center-repository.js';
2021
import * as certificationCourseRepository from '../../infrastructure/repositories/certification-course-repository.js';
2122
import * as certificationIssueReportRepository from '../../infrastructure/repositories/certification-issue-report-repository.js';
23+
import * as challengeRepository from '../../infrastructure/repositories/challenge-repository.js';
2224
import * as competenceMarkRepository from '../../infrastructure/repositories/competence-mark-repository.js';
2325
import * as competenceRepository from '../../infrastructure/repositories/competence-repository.js';
2426
import * as complementaryCertificationScoringCriteriaRepository from '../../infrastructure/repositories/complementary-certification-scoring-criteria-repository.js';
@@ -33,7 +35,6 @@ import * as userRepository from '../../../src/shared/infrastructure/repositories
3335
import { participantResultsSharedRepository } from '../../infrastructure/repositories/participant-results-shared-repository.js';
3436
import * as juryCertificationSummaryRepository from '../../infrastructure/repositories/jury-certification-summary-repository.js';
3537
import * as finalizedSessionRepository from '../../infrastructure/repositories/sessions/finalized-session-repository.js';
36-
import * as challengeRepository from '../../infrastructure/repositories/challenge-repository.js';
3738
import { logger } from '../../infrastructure/logger.js';
3839
import * as poleEmploiNotifier from '../../infrastructure/externals/pole-emploi/pole-emploi-notifier.js';
3940
import * as disabledPoleEmploiNotifier from '../../infrastructure/externals/pole-emploi/disabled-pole-emploi-notifier.js';
@@ -57,9 +58,10 @@ function requirePoleEmploiNotifier() {
5758
}
5859

5960
const dependencies = {
60-
authenticationMethodRepository,
61+
answerRepository,
6162
assessmentRepository,
6263
assessmentResultRepository,
64+
authenticationMethodRepository,
6365
badgeAcquisitionRepository,
6466
badgeRepository,
6567
campaignRepository,
@@ -69,23 +71,23 @@ const dependencies = {
6971
certificationCenterRepository,
7072
certificationCourseRepository,
7173
certificationIssueReportRepository,
74+
challengeRepository,
7275
competenceMarkRepository,
7376
competenceRepository,
7477
complementaryCertificationScoringCriteriaRepository,
78+
finalizedSessionRepository,
79+
juryCertificationSummaryRepository,
7580
knowledgeElementRepository,
81+
logger,
7682
organizationRepository,
83+
participantResultsSharedRepository,
84+
poleEmploiNotifier: requirePoleEmploiNotifier(),
7785
poleEmploiSendingRepository,
7886
scoringCertificationService,
7987
skillRepository,
8088
supervisorAccessRepository,
8189
targetProfileRepository,
8290
userRepository,
83-
participantResultsSharedRepository,
84-
poleEmploiNotifier: requirePoleEmploiNotifier(),
85-
juryCertificationSummaryRepository,
86-
finalizedSessionRepository,
87-
challengeRepository,
88-
logger,
8991
};
9092

9193
const partnerCertificationScoringRepository = injectDependencies(dependency, dependencies);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { status } from './AssessmentResult.js';
2+
import { FlashAssessmentAlgorithm } from './FlashAssessmentAlgorithm.js';
3+
4+
/*
5+
Score should not be totally linear. It should be piecewise linear, so we define
6+
here the different intervals. See documentation here :
7+
https://1024pix.atlassian.net/wiki/spaces/DD/pages/3835133953/Vulgarisation+score+2023
8+
*/
9+
const scoreIntervals = [
10+
{
11+
start: -8,
12+
end: -6,
13+
},
14+
{
15+
start: -6,
16+
end: -4,
17+
},
18+
{
19+
start: -4,
20+
end: -2,
21+
},
22+
{
23+
start: -2,
24+
end: 0,
25+
},
26+
{
27+
start: 0,
28+
end: 2,
29+
},
30+
{
31+
start: 2,
32+
end: 4,
33+
},
34+
{
35+
start: 4,
36+
end: 6,
37+
},
38+
{
39+
start: 6,
40+
end: 8,
41+
},
42+
];
43+
44+
const MAX_PIX_SCORE = 1024;
45+
const INTERVAL_HEIGHT = MAX_PIX_SCORE / scoreIntervals.length;
46+
47+
class CertificationAssessmentScoreV3 {
48+
constructor({ nbPix, percentageCorrectAnswers = 100 }) {
49+
this.nbPix = nbPix;
50+
this.percentageCorrectAnswers = percentageCorrectAnswers;
51+
}
52+
53+
static fromChallengesAndAnswers({ challenges, allAnswers }) {
54+
const algorithm = new FlashAssessmentAlgorithm();
55+
const { estimatedLevel } = algorithm.getEstimatedLevelAndErrorRate({
56+
challenges,
57+
allAnswers,
58+
});
59+
60+
const nbPix = _computeScore(estimatedLevel);
61+
62+
return new CertificationAssessmentScoreV3({
63+
nbPix,
64+
});
65+
}
66+
67+
get status() {
68+
return status.VALIDATED;
69+
}
70+
71+
get competenceMarks() {
72+
return [];
73+
}
74+
75+
getPercentageCorrectAnswers() {
76+
return this.percentageCorrectAnswers;
77+
}
78+
}
79+
80+
const _findIntervalIndex = (estimatedLevel) =>
81+
scoreIntervals.findIndex(({ start, end }) => estimatedLevel < end && estimatedLevel >= start);
82+
83+
const _computeScore = (estimatedLevel) => {
84+
const intervalIndex = _findIntervalIndex(estimatedLevel);
85+
86+
const intervalMaxValue = scoreIntervals[intervalIndex].end;
87+
const intervalWidth = scoreIntervals[intervalIndex].end - scoreIntervals[intervalIndex].start;
88+
89+
// Formula is defined here : https://1024pix.atlassian.net/wiki/spaces/DD/pages/3835133953/Vulgarisation+score+2023#Le-score
90+
const score = INTERVAL_HEIGHT * (intervalIndex + 1 + (estimatedLevel - intervalMaxValue) / intervalWidth);
91+
92+
return Math.round(score);
93+
};
94+
95+
export { CertificationAssessmentScoreV3 };

api/lib/infrastructure/datasources/learning-content/challenge-datasource.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const challengeDatasource = datasource.extend({
7777

7878
return challenges.filter(
7979
(challengeData) =>
80-
_.includes(challengeData.locales, locale) &&
80+
(!locale || _.includes(challengeData.locales, locale)) &&
8181
challengeData.alpha != null &&
8282
challengeData.delta != null &&
8383
challengeData.skillId &&

api/lib/infrastructure/repositories/challenge-repository.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ const findOperativeFlashCompatible = async function ({
197197
return _toDomainCollection({ challengeDataObjects, skills, successProbabilityThreshold });
198198
};
199199

200-
const findFlashCompatible = async function ({ locale, useObsoleteChallenges }) {
200+
const findFlashCompatible = async function ({ locale, useObsoleteChallenges } = {}) {
201201
const challengeDataObjects = await challengeDatasource.findFlashCompatible({ locale, useObsoleteChallenges });
202202
const skills = await skillDatasource.list();
203203
return _toDomainCollection({ challengeDataObjects, skills });

0 commit comments

Comments
 (0)