Skip to content

Commit

Permalink
[FEATURE] Ajout du scoring pour les certifications V3 (PIX-9050).
Browse files Browse the repository at this point in the history
  • Loading branch information
pix-service-auto-merge authored Sep 11, 2023
2 parents 62c0d43 + ef61cb5 commit 4ad3f04
Show file tree
Hide file tree
Showing 12 changed files with 709 additions and 229 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ const buildAnsweredNotCompletedCertificationAssessment = function ({
certifiableUserId,
competenceIdSkillIdPairs,
limitDate,
version = 2,
}) {
const certificationCourseId = buildCertificationCourse({
userId: certifiableUserId,
createdAt: limitDate,
version: 2,
version,
}).id;
const certificationAssessment = buildAssessment({
certificationCourseId,
Expand Down
50 changes: 50 additions & 0 deletions api/lib/domain/events/handle-certification-scoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import bluebird from 'bluebird';
import { CertificationComputeError } from '../errors.js';
import { AssessmentCompleted } from './AssessmentCompleted.js';
import { checkEventTypes } from './check-event-types.js';
import { CertificationVersion } from '../../../src/shared/domain/models/CertificationVersion.js';
import { CertificationAssessmentScoreV3 } from '../models/CertificationAssessmentScoreV3.js';

const eventTypes = [AssessmentCompleted];
const EMITTER = 'PIX-ALGO';
Expand All @@ -17,11 +19,26 @@ async function handleCertificationScoring({
certificationCourseRepository,
competenceMarkRepository,
scoringCertificationService,
answerRepository,
challengeRepository,
}) {
checkEventTypes(event, eventTypes);

if (event.isCertificationType) {
const certificationAssessment = await certificationAssessmentRepository.get(event.assessmentId);

if (certificationAssessment.version === CertificationVersion.V3) {
return _handleV3CertificationScoring({
challengeRepository,
answerRepository,
assessmentId: event.assessmentId,
certificationAssessment,
assessmentResultRepository,
certificationCourseRepository,
competenceMarkRepository,
});
}

return _calculateCertificationScore({
certificationAssessment,
assessmentResultRepository,
Expand Down Expand Up @@ -72,6 +89,39 @@ async function _calculateCertificationScore({
}
}

async function _handleV3CertificationScoring({
challengeRepository,
answerRepository,
assessmentId,
certificationAssessment,
assessmentResultRepository,
certificationCourseRepository,
competenceMarkRepository,
}) {
const allAnswers = await answerRepository.findByAssessment(assessmentId);
const challengeIds = allAnswers.map(({ challengeId }) => challengeId);
const challenges = await challengeRepository.getMany(challengeIds);

const certificationAssessmentScore = CertificationAssessmentScoreV3.fromChallengesAndAnswers({
challenges,
allAnswers,
});

await _saveResult({
certificationAssessment,
certificationAssessmentScore,
assessmentResultRepository,
certificationCourseRepository,
competenceMarkRepository,
});

return new CertificationScoringCompleted({
userId: certificationAssessment.userId,
certificationCourseId: certificationAssessment.certificationCourseId,
reproducibilityRate: certificationAssessmentScore.getPercentageCorrectAnswers(),
});
}

async function _saveResult({
certificationAssessment,
certificationAssessmentScore,
Expand Down
18 changes: 10 additions & 8 deletions api/lib/domain/events/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import _ from 'lodash';
import perf_hooks from 'perf_hooks';
import * as eventBusBuilder from '../../infrastructure/events/EventBusBuilder.js';

import * as answerRepository from '../../infrastructure/repositories/answer-repository.js';
import * as authenticationMethodRepository from '../../infrastructure/repositories/authentication-method-repository.js';
import * as assessmentRepository from '../../infrastructure/repositories/assessment-repository.js';
import * as assessmentResultRepository from '../../infrastructure/repositories/assessment-result-repository.js';
Expand All @@ -19,6 +20,7 @@ import * as certificationAssessmentRepository from '../../infrastructure/reposit
import * as certificationCenterRepository from '../../../src/certification/shared/infrastructure/repositories/certification-center-repository.js';
import * as certificationCourseRepository from '../../infrastructure/repositories/certification-course-repository.js';
import * as certificationIssueReportRepository from '../../infrastructure/repositories/certification-issue-report-repository.js';
import * as challengeRepository from '../../infrastructure/repositories/challenge-repository.js';
import * as competenceMarkRepository from '../../infrastructure/repositories/competence-mark-repository.js';
import * as competenceRepository from '../../infrastructure/repositories/competence-repository.js';
import * as complementaryCertificationScoringCriteriaRepository from '../../infrastructure/repositories/complementary-certification-scoring-criteria-repository.js';
Expand All @@ -33,7 +35,6 @@ import * as userRepository from '../../../src/shared/infrastructure/repositories
import { participantResultsSharedRepository } from '../../infrastructure/repositories/participant-results-shared-repository.js';
import * as juryCertificationSummaryRepository from '../../infrastructure/repositories/jury-certification-summary-repository.js';
import * as finalizedSessionRepository from '../../infrastructure/repositories/sessions/finalized-session-repository.js';
import * as challengeRepository from '../../infrastructure/repositories/challenge-repository.js';
import { logger } from '../../infrastructure/logger.js';
import * as poleEmploiNotifier from '../../infrastructure/externals/pole-emploi/pole-emploi-notifier.js';
import * as disabledPoleEmploiNotifier from '../../infrastructure/externals/pole-emploi/disabled-pole-emploi-notifier.js';
Expand All @@ -57,9 +58,10 @@ function requirePoleEmploiNotifier() {
}

const dependencies = {
authenticationMethodRepository,
answerRepository,
assessmentRepository,
assessmentResultRepository,
authenticationMethodRepository,
badgeAcquisitionRepository,
badgeRepository,
campaignRepository,
Expand All @@ -69,23 +71,23 @@ const dependencies = {
certificationCenterRepository,
certificationCourseRepository,
certificationIssueReportRepository,
challengeRepository,
competenceMarkRepository,
competenceRepository,
complementaryCertificationScoringCriteriaRepository,
finalizedSessionRepository,
juryCertificationSummaryRepository,
knowledgeElementRepository,
logger,
organizationRepository,
participantResultsSharedRepository,
poleEmploiNotifier: requirePoleEmploiNotifier(),
poleEmploiSendingRepository,
scoringCertificationService,
skillRepository,
supervisorAccessRepository,
targetProfileRepository,
userRepository,
participantResultsSharedRepository,
poleEmploiNotifier: requirePoleEmploiNotifier(),
juryCertificationSummaryRepository,
finalizedSessionRepository,
challengeRepository,
logger,
};

const partnerCertificationScoringRepository = injectDependencies(dependency, dependencies);
Expand Down
95 changes: 95 additions & 0 deletions api/lib/domain/models/CertificationAssessmentScoreV3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { status } from './AssessmentResult.js';
import { FlashAssessmentAlgorithm } from './FlashAssessmentAlgorithm.js';

/*
Score should not be totally linear. It should be piecewise linear, so we define
here the different intervals. See documentation here :
https://1024pix.atlassian.net/wiki/spaces/DD/pages/3835133953/Vulgarisation+score+2023
*/
const scoreIntervals = [
{
start: -8,
end: -6,
},
{
start: -6,
end: -4,
},
{
start: -4,
end: -2,
},
{
start: -2,
end: 0,
},
{
start: 0,
end: 2,
},
{
start: 2,
end: 4,
},
{
start: 4,
end: 6,
},
{
start: 6,
end: 8,
},
];

const MAX_PIX_SCORE = 1024;
const INTERVAL_HEIGHT = MAX_PIX_SCORE / scoreIntervals.length;

class CertificationAssessmentScoreV3 {
constructor({ nbPix, percentageCorrectAnswers = 100 }) {
this.nbPix = nbPix;
this.percentageCorrectAnswers = percentageCorrectAnswers;
}

static fromChallengesAndAnswers({ challenges, allAnswers }) {
const algorithm = new FlashAssessmentAlgorithm();
const { estimatedLevel } = algorithm.getEstimatedLevelAndErrorRate({
challenges,
allAnswers,
});

const nbPix = _computeScore(estimatedLevel);

return new CertificationAssessmentScoreV3({
nbPix,
});
}

get status() {
return status.VALIDATED;
}

get competenceMarks() {
return [];
}

getPercentageCorrectAnswers() {
return this.percentageCorrectAnswers;
}
}

const _findIntervalIndex = (estimatedLevel) =>
scoreIntervals.findIndex(({ start, end }) => estimatedLevel < end && estimatedLevel >= start);

const _computeScore = (estimatedLevel) => {
const intervalIndex = _findIntervalIndex(estimatedLevel);

const intervalMaxValue = scoreIntervals[intervalIndex].end;
const intervalWidth = scoreIntervals[intervalIndex].end - scoreIntervals[intervalIndex].start;

// Formula is defined here : https://1024pix.atlassian.net/wiki/spaces/DD/pages/3835133953/Vulgarisation+score+2023#Le-score
const score = INTERVAL_HEIGHT * (intervalIndex + 1 + (estimatedLevel - intervalMaxValue) / intervalWidth);

return Math.round(score);
};

export { CertificationAssessmentScoreV3 };
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const challengeDatasource = datasource.extend({

return challenges.filter(
(challengeData) =>
_.includes(challengeData.locales, locale) &&
(!locale || _.includes(challengeData.locales, locale)) &&
challengeData.alpha != null &&
challengeData.delta != null &&
challengeData.skillId &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ const findOperativeFlashCompatible = async function ({
return _toDomainCollection({ challengeDataObjects, skills, successProbabilityThreshold });
};

const findFlashCompatible = async function ({ locale, useObsoleteChallenges }) {
const findFlashCompatible = async function ({ locale, useObsoleteChallenges } = {}) {
const challengeDataObjects = await challengeDatasource.findFlashCompatible({ locale, useObsoleteChallenges });
const skills = await skillDatasource.list();
return _toDomainCollection({ challengeDataObjects, skills });
Expand Down
Loading

0 comments on commit 4ad3f04

Please sign in to comment.