Skip to content

Commit

Permalink
[FEATURE] Sélection des acquis accessibles pour proposer un test amén…
Browse files Browse the repository at this point in the history
…agé (PIX-14241).

 #10144
  • Loading branch information
pix-service-auto-merge authored Sep 23, 2024
2 parents 43927ad + e84feee commit 667d39b
Show file tree
Hide file tree
Showing 19 changed files with 436 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @typedef {import('./index.js').CertificationChallengeRepository} CertificationChallengeRepository
* @typedef {import('./index.js').ChallengeRepository} ChallengeRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').CertificationChallengeRepository} CertificationChallengeRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').ChallengeRepository} ChallengeRepository
*/

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
/**
* @typedef {import('./index.js').AnswerRepository} AnswerRepository
* @typedef {import('./index.js').CertificationChallengeRepository} CertificationChallengeRepository
* @typedef {import('./index.js').CertificationChallengeLiveAlertRepository} CertificationChallengeLiveAlertRepository
* @typedef {import('./index.js').CertificationCourseRepository} CertificationCourseRepository
* @typedef {import('./index.js').ChallengeRepository} ChallengeRepository
* @typedef {import('./index.js').FlashAlgorithmConfigurationRepository} FlashAlgorithmConfigurationRepository
* @typedef {import('./index.js').PickChallengeService} PickChallengeService
* @typedef {import('./index.js').FlashAlgorithmService} FlashAlgorithmService
* @typedef {import('../../../session-management/domain/usecases/index.js').AnswerRepository} AnswerRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').CertificationChallengeRepository} CertificationChallengeRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').CertificationChallengeLiveAlertRepository} CertificationChallengeLiveAlertRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').CertificationCourseRepository} CertificationCourseRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').ChallengeRepository} ChallengeRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').FlashAlgorithmConfigurationRepository} FlashAlgorithmConfigurationRepository
* @typedef {import('../../../session-management/domain/usecases/index.js').PickChallengeService} PickChallengeService
* @typedef {import('../../../session-management/domain/usecases/index.js').FlashAlgorithmService} FlashAlgorithmService
* @typedef {import('../../../session-management/domain/usecases/index.js').CertificationCandidateRepository} CertificationCandidateRepository
*/

import Debug from 'debug';

import { AssessmentEndedError } from '../../../../shared/domain/errors.js';
import { CertificationChallenge, FlashAssessmentAlgorithm } from '../../../../shared/domain/models/index.js';

const debugGetNextChallengeForV3Certification = Debug('pix:certif:v3:get-next-challenge');

/**
* @param {Object} params
* @param {AnswerRepository} params.answerRepository
Expand All @@ -22,8 +27,9 @@ import { CertificationChallenge, FlashAssessmentAlgorithm } from '../../../../sh
* @param {FlashAlgorithmConfigurationRepository} params.flashAlgorithmConfigurationRepository
* @param {FlashAlgorithmService} params.flashAlgorithmService
* @param {PickChallengeService} params.pickChallengeService
* @param {CertificationCandidateRepository} params.certificationCandidateRepository
*/
const getNextChallengeForV3Certification = async function ({
const getNextChallenge = async function ({
assessment,
answerRepository,
certificationChallengeRepository,
Expand All @@ -34,6 +40,7 @@ const getNextChallengeForV3Certification = async function ({
flashAlgorithmService,
locale,
pickChallengeService,
certificationCandidateRepository,
}) {
const certificationCourse = await certificationCourseRepository.get({ id: assessment.certificationCourseId });

Expand Down Expand Up @@ -77,9 +84,19 @@ const getNextChallengeForV3Certification = async function ({
challenges,
});

const candidate = await certificationCandidateRepository.findByAssessmentId({ assessmentId: assessment.id });
const challengesForCandidate = candidate.accessibilityAdjustmentNeeded
? challengesWithoutSkillsWithAValidatedLiveAlert.filter((challenge) => challenge.isAccessible)
: challengesWithoutSkillsWithAValidatedLiveAlert;
debugGetNextChallengeForV3Certification(
candidate.accessibilityAdjustmentNeeded
? `Candidate needs accessibility adjustment, possible challenges have been filtered (${challengesForCandidate.length} out of ${challengesWithoutSkillsWithAValidatedLiveAlert.length} selected`
: `Candidate does need any adjustment, all ${challengesWithoutSkillsWithAValidatedLiveAlert.length} have been selected`,
);

const possibleChallenges = assessmentAlgorithm.getPossibleNextChallenges({
assessmentAnswers: allAnswers,
challenges: challengesWithoutSkillsWithAValidatedLiveAlert,
challenges: challengesForCandidate,
});

if (_hasAnsweredToAllChallenges({ possibleChallenges })) {
Expand Down Expand Up @@ -134,4 +151,4 @@ const _getValidatedLiveAlertChallengeIds = async ({ assessmentId, certificationC
return certificationChallengeLiveAlertRepository.getLiveAlertValidatedChallengeIdsByAssessmentId({ assessmentId });
};

export { getNextChallengeForV3Certification };
export { getNextChallenge };
54 changes: 54 additions & 0 deletions api/src/certification/evaluation/domain/usecases/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { pickChallengeService } from '../../../../evaluation/domain/services/pick-challenge-service.js';
import { injectDependencies } from '../../../../shared/infrastructure/utils/dependency-injection.js';
import { importNamedExportsFromDirectory } from '../../../../shared/infrastructure/utils/import-named-exports-from-directory.js';
import * as flashAlgorithmService from '../../../flash-certification/domain/services/algorithm-methods/flash.js';
import {
answerRepository,
assessmentRepository,
assessmentResultRepository,
certificationChallengeRepository,
challengeRepository,
competenceMarkRepository,
cpfExportRepository,
flashAlgorithmConfigurationRepository,
sessionRepositories,
sharedCompetenceMarkRepository,
} from '../../../session-management/infrastructure/repositories/index.js';
import * as certificationCandidateRepository from '../../infrastructure/repositories/certification-candidate-repository.js';

const dependencies = {
...sessionRepositories,
certificationCandidateRepository,
assessmentRepository,
assessmentResultRepository,
answerRepository,
sharedCompetenceMarkRepository,
challengeRepository,
competenceMarkRepository,
cpfExportRepository,
certificationChallengeRepository,
flashAlgorithmConfigurationRepository,
flashAlgorithmService,
pickChallengeService,
};

const path = dirname(fileURLToPath(import.meta.url));

/**
* Note : current ignoredFileNames are injected in * {@link file://./../../../shared/domain/usecases/index.js}
* This is in progress, because they should be injected in this file and not by shared sub-domain
* The only remaining file ignored should be index.js
*/
const usecasesWithoutInjectedDependencies = {
...(await importNamedExportsFromDirectory({
path: join(path, './'),
ignoredFileNames: ['index.js'],
})),
};

const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies);

export { usecases };
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { knex } from '../../../../../db/knex-database-connection.js';
import { CertificationCandidateNotFoundError } from '../../../../shared/domain/errors.js';
import { Candidate } from '../../../enrolment/domain/models/Candidate.js';

const findByAssessmentId = async function ({ assessmentId }) {
const result = await knex('certification-candidates')
.select('certification-candidates.*')
.join('certification-courses', 'certification-courses.userId', 'certification-candidates.userId')
.join('assessments', 'assessments.certificationCourseId', 'certification-courses.id')
.where('assessments.id', assessmentId)
.first();

if (!result) {
throw new CertificationCandidateNotFoundError();
}

return new Candidate(result);
};

export { findByAssessmentId };
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class CertificationCandidateForSupervising {
enrolledComplementaryCertification,
stillValidBadgeAcquisitions = [],
isCompanionActive = false,
accessibilityAdjustmentNeeded,
} = {}) {
this.id = id;
this.userId = userId;
Expand All @@ -30,6 +31,7 @@ class CertificationCandidateForSupervising {
this.enrolledComplementaryCertification = enrolledComplementaryCertification;
this.stillValidBadgeAcquisitions = stillValidBadgeAcquisitions;
this.isCompanionActive = isCompanionActive;
this.accessibilityAdjustmentNeeded = accessibilityAdjustmentNeeded;
}

authorizeToStart() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Serializer as JSONAPISerializer } from 'jsonapi-serializer';

import { usecases } from '../../../../lib/domain/usecases/index.js';
import { usecases as certificationEvaluationUsecases } from '../../../certification/evaluation/domain/usecases/index.js';
import * as certificationVersionRepository from '../../../certification/results/infrastructure/repositories/certification-version-repository.js';
import { usecases as certificationUsecases } from '../../../certification/session-management/domain/usecases/index.js';
import { CertificationVersion } from '../../../certification/shared/domain/models/CertificationVersion.js';
Expand Down Expand Up @@ -198,9 +199,9 @@ async function _getChallengeByAssessmentType({ assessment, request, dependencies
});

if (CertificationVersion.isV3(certificationCourseVersion)) {
return certificationUsecases.getNextChallengeForV3Certification({ assessment, locale });
return certificationEvaluationUsecases.getNextChallenge({ assessment, locale });
} else {
return certificationUsecases.getNextChallengeForV2Certification({ assessment, locale });
return certificationEvaluationUsecases.getNextChallengeForV2Certification({ assessment, locale });
}
}

Expand Down
20 changes: 19 additions & 1 deletion api/src/shared/domain/models/Challenge.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const ChallengeType = Object.freeze({
QROCM_DEP: 'QROCM-dep',
});

const Accessibility = Object.freeze({
RAS: 'RAS',
OK: 'OK',
});

/**
* Traduction: Épreuve
*/
Expand Down Expand Up @@ -49,6 +54,8 @@ class Challenge {
* @param successProbabilityThreshold
* @param shuffled
* @param alternativeVersion
* @param blindnessCompatibility
* @param colorBlindnessCompatibility
*/
constructor({
id,
Expand Down Expand Up @@ -80,6 +87,8 @@ class Challenge {
responsive,
shuffled,
alternativeVersion,
blindnessCompatibility,
colorBlindnessCompatibility,
} = {}) {
this.id = id;
this.answer = answer;
Expand Down Expand Up @@ -110,6 +119,8 @@ class Challenge {
this.successProbabilityThreshold = successProbabilityThreshold;
this.shuffled = shuffled;
this.alternativeVersion = alternativeVersion;
this.blindnessCompatibility = blindnessCompatibility;
this.colorBlindnessCompatibility = colorBlindnessCompatibility;
}

isTimed() {
Expand Down Expand Up @@ -148,6 +159,13 @@ class Challenge {
return this._isCompliant('Tablet');
}

get isAccessible() {
return (
(this.blindnessCompatibility === Accessibility.OK || this.blindnessCompatibility === Accessibility.RAS) &&
(this.colorBlindnessCompatibility === Accessibility.OK || this.colorBlindnessCompatibility === Accessibility.RAS)
);
}

set successProbabilityThreshold(successProbabilityThreshold) {
if (this.difficulty == null || this.discriminant == null || successProbabilityThreshold == null) return;
this.minimumCapability = this.difficulty - Math.log(1 / successProbabilityThreshold - 1) / this.discriminant;
Expand Down Expand Up @@ -186,4 +204,4 @@ class Challenge {

Challenge.Type = ChallengeType;

export { Challenge, ChallengeType as Type };
export { Accessibility, Challenge, ChallengeType as Type };
Original file line number Diff line number Diff line change
Expand Up @@ -205,5 +205,7 @@ function _toDomain({ challengeDataObject, skillDataObject, successProbabilityThr
shuffled: challengeDataObject.shuffled,
successProbabilityThreshold,
alternativeVersion: challengeDataObject.alternativeVersion,
blindnessCompatibility: challengeDataObject.accessibility1,
colorBlindnessCompatibility: challengeDataObject.accessibility2,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ describe('Certification | Configuration | Integration | Repository | center-pilo
describe('update', function () {
it('should update the center pilot features', async function () {
// given
const centerData = databaseBuilder.factory.buildCertificationCenter({ isV3Pilot: false, updatedAt: new Date() });
const centerData = databaseBuilder.factory.buildCertificationCenter({
isV3Pilot: false,
updatedAt: new Date('2020-01-01'),
});
await databaseBuilder.commit();

// when
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as certificationCandidateRepository from '../../../../../../src/certification/evaluation/infrastructure/repositories/certification-candidate-repository.js';
import { CertificationCandidateNotFoundError } from '../../../../../../src/shared/domain/errors.js';
import { Assessment } from '../../../../../../src/shared/domain/models/Assessment.js';
import { catchErr, databaseBuilder, domainBuilder, expect } from '../../../../../test-helper.js';

describe('Integration | Repository | certification candidate', function () {
describe('#findByAssessmentId', function () {
describe('when certification candidate is found', function () {
it('should return the certification candidate', async function () {
// given
const session = databaseBuilder.factory.buildSession();
const user = databaseBuilder.factory.buildUser();
const candidate = databaseBuilder.factory.buildCertificationCandidate({
lastName: 'Joplin',
firstName: 'Janis',
sessionId: session.id,
userId: user.id,
authorizedToStart: false,
});
const certificationCourse = databaseBuilder.factory.buildCertificationCourse({
userId: user.id,
sessionId: session.id,
createdAt: new Date('2022-10-01T14:00:00Z'),
});
const assessmentId = databaseBuilder.factory.buildAssessment({
certificationCourseId: certificationCourse.id,
state: Assessment.states.STARTED,
}).id;

await databaseBuilder.commit();

// when
const result = await certificationCandidateRepository.findByAssessmentId({
assessmentId,
});

// then
expect(result).to.deep.equal(
domainBuilder.certification.enrolment.buildCandidate({
...candidate,
subscriptions: [],
}),
);
});
});

describe('when certification candidate is not found', function () {
it('should throw a certification candidate not found error', async function () {
// given
const session = databaseBuilder.factory.buildSession();
const user = databaseBuilder.factory.buildUser();
databaseBuilder.factory.buildCertificationCandidate({
lastName: 'Joplin',
firstName: 'Janis',
sessionId: session.id,
userId: user.id,
authorizedToStart: false,
});
const certificationCourse = databaseBuilder.factory.buildCertificationCourse({
userId: user.id,
sessionId: session.id,
createdAt: new Date('2022-10-01T14:00:00Z'),
});
databaseBuilder.factory.buildAssessment({
certificationCourseId: certificationCourse.id,
state: Assessment.states.STARTED,
});

await databaseBuilder.commit();

// when
const error = await catchErr(certificationCandidateRepository.findByAssessmentId)({
assessmentId: 4659,
});

// then
expect(error).to.be.an.instanceOf(CertificationCandidateNotFoundError);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getNextChallengeForV2Certification } from '../../../../../../src/certification/session-management/domain/usecases/get-next-challenge-for-v2-certification.js';
import { getNextChallengeForV2Certification } from '../../../../../../src/certification/evaluation/domain/usecases/get-next-challenge-for-v2-certification.js';
import { Assessment } from '../../../../../../src/shared/domain/models/Assessment.js';
import { domainBuilder, expect, sinon } from '../../../../../test-helper.js';

Expand Down
Loading

0 comments on commit 667d39b

Please sign in to comment.