Skip to content

Commit e6f4def

Browse files
[TECH] Copier le ValidatorQroc dans le bounded context de devcomp (PIX-10025)
#7536
2 parents 77ba6ae + f83a612 commit e6f4def

15 files changed

+1998
-1
lines changed
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Traduction: Correction
3+
* Context: Objet existant dans le cadre de la correction d'une réponse pendant le fonctionnement
4+
* interne de l'algorithme.
5+
*/
6+
class Solution {
7+
/**
8+
*
9+
* @param id: id de la ligne Épreuve du référentiel dont est extraite l'information de la Solution
10+
* @param isT1Enabled: T1 - Espaces, casse & accents
11+
* @param isT2Enabled: T2 - Ponctuation
12+
* @param isT3Enabled: T3 - Distance d'édition
13+
* @param type: type de l'épreuve
14+
* @param value: Bonne réponse attendue.
15+
*
16+
* Les traitements T1, T2 et T3 sont les traitements qu'il est possible d'utiliser pour valider une réponse.
17+
* Pour plus d'informations, ne pas hésiter à se reporter aux explications présentes dans pix-editor.
18+
*/
19+
constructor({
20+
id,
21+
isT1Enabled = false,
22+
isT2Enabled = false,
23+
isT3Enabled = false,
24+
type,
25+
value,
26+
qrocBlocksTypes,
27+
} = {}) {
28+
this.id = id;
29+
this.isT1Enabled = isT1Enabled;
30+
this.isT2Enabled = isT2Enabled;
31+
this.isT3Enabled = isT3Enabled;
32+
this.type = type;
33+
this.value = value;
34+
this.qrocBlocksTypes = qrocBlocksTypes;
35+
}
36+
37+
get enabledTreatments() {
38+
const enabledTreatments = [];
39+
if (this.isT1Enabled) {
40+
enabledTreatments.push('t1');
41+
}
42+
if (this.isT2Enabled) {
43+
enabledTreatments.push('t2');
44+
}
45+
if (this.isT3Enabled) {
46+
enabledTreatments.push('t3');
47+
}
48+
return enabledTreatments;
49+
}
50+
51+
// TODO: delete when deactivation object is correctly deleted everywhere
52+
/**
53+
* @deprecated use the enabledTreatments property
54+
*/
55+
get deactivations() {
56+
return {
57+
t1: !this.enabledTreatments.includes('t1'),
58+
t2: !this.enabledTreatments.includes('t2'),
59+
t3: !this.enabledTreatments.includes('t3'),
60+
};
61+
}
62+
}
63+
64+
export { Solution };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as solutionServiceQROC from '../../services/solution-service-qroc.js';
2+
import { Validation } from './Validation.js';
3+
import { Validator } from './Validator.js';
4+
5+
/**
6+
* Traduction: Vérificateur de réponse pour un QROC
7+
*/
8+
class ValidatorQROC extends Validator {
9+
injectedSolutionServiceQROC;
10+
constructor({ solutions } = {}, injectedSolutionServiceQROC = solutionServiceQROC) {
11+
super({ solution: solutions });
12+
this.injectedSolutionServiceQROC = injectedSolutionServiceQROC;
13+
}
14+
15+
assess({ answer, challengeFormat }) {
16+
const result = this.injectedSolutionServiceQROC.match({
17+
answer: answer.value,
18+
solutions: this.solution,
19+
challengeFormat,
20+
});
21+
22+
return new Validation({
23+
result,
24+
resultDetails: null,
25+
});
26+
}
27+
}
28+
29+
export { ValidatorQROC };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { _ } from '../../../shared/infrastructure/utils/lodash-utils.js';
2+
3+
const ALL_TREATMENTS = ['t1', 't2', 't3'];
4+
5+
function getEnabledTreatments(shouldApplyTreatments, deactivations) {
6+
return shouldApplyTreatments ? ALL_TREATMENTS.filter((treatment) => !deactivations[treatment]) : [];
7+
}
8+
9+
function useLevenshteinRatio(enabledTreatments) {
10+
return _.includes(enabledTreatments, 't3');
11+
}
12+
13+
export { getEnabledTreatments, useLevenshteinRatio };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { cleanStringAndParseFloat, isNumeric } from '../../../shared/infrastructure/utils/string-utils.js';
2+
import lodash from 'lodash';
3+
import { applyPreTreatments, applyTreatments } from './validation-treatments.js';
4+
import { validateAnswer } from './string-comparison-service.js';
5+
import { AnswerStatus } from '../models/validator/AnswerStatus.js';
6+
import { getEnabledTreatments, useLevenshteinRatio } from './services-utils.js';
7+
8+
const CHALLENGE_NUMBER_FORMAT = 'nombre';
9+
const { every, isEmpty, isString, map } = lodash;
10+
11+
const match = function ({ answer, challengeFormat, solutions }) {
12+
const deactivations = solutions.deactivations;
13+
const qrocBlocksTypes = solutions.qrocBlocksTypes || {};
14+
const shouldApplyTreatments = qrocBlocksTypes[Object.keys(qrocBlocksTypes)[0]] === 'select' ? false : true;
15+
16+
const isIncorrectAnswerFormat = !isString(answer);
17+
const isIncorrectSolutionFormat = solutions.value?.some((s) => !isString(s) || isEmpty(s));
18+
19+
if (isIncorrectAnswerFormat || isIncorrectSolutionFormat) {
20+
return AnswerStatus.KO;
21+
}
22+
23+
const solutionsValue = solutions.value;
24+
const areAllNumericSolutions = every(solutionsValue, (solution) => {
25+
return isNumeric(solution);
26+
});
27+
28+
if (isNumeric(answer) && areAllNumericSolutions && challengeFormat === CHALLENGE_NUMBER_FORMAT) {
29+
return _getAnswerStatusFromNumberMatching(answer, solutionsValue);
30+
}
31+
32+
return _getAnswerStatusFromStringMatching(answer, solutionsValue, deactivations, shouldApplyTreatments);
33+
};
34+
35+
export { match };
36+
37+
function _getAnswerStatusFromNumberMatching(answer, solutions) {
38+
const treatedSolutions = solutions.map((solution) => cleanStringAndParseFloat(solution));
39+
const treatedAnswer = cleanStringAndParseFloat(answer);
40+
const indexOfSolution = treatedSolutions.indexOf(treatedAnswer);
41+
const isAnswerMatchingSolution = indexOfSolution !== -1;
42+
if (isAnswerMatchingSolution) {
43+
return AnswerStatus.OK;
44+
}
45+
return AnswerStatus.KO;
46+
}
47+
48+
function _getAnswerStatusFromStringMatching(answer, solutions, deactivations, shouldApplyTreatments) {
49+
const enabledTreatments = getEnabledTreatments(shouldApplyTreatments, deactivations);
50+
const treatedAnswer = applyTreatments(applyPreTreatments(answer), enabledTreatments);
51+
const treatedSolutions = map(solutions, (solution) => applyTreatments(solution, enabledTreatments));
52+
53+
return validateAnswer(treatedAnswer, treatedSolutions, useLevenshteinRatio(enabledTreatments))
54+
? AnswerStatus.OK
55+
: AnswerStatus.KO;
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import _ from 'lodash';
2+
import levenshtein from 'fast-levenshtein';
3+
import { LEVENSHTEIN_DISTANCE_MAX_RATE } from '../../../shared/domain/constants.js';
4+
5+
function isOneStringCloseEnoughFromMultipleStrings(inputString, references, MAX_ACCEPTABLE_RATIO) {
6+
return getSmallestLevenshteinRatio(inputString, references) <= MAX_ACCEPTABLE_RATIO;
7+
}
8+
9+
function getSmallestLevenshteinRatio(inputString, references) {
10+
return getSmallestLevenshteinDistance(inputString, references) / inputString.length;
11+
}
12+
13+
function getSmallestLevenshteinDistance(comparative, alternatives) {
14+
const getLevenshteinDistance = (alternative) => levenshtein.get(comparative, alternative);
15+
return _(alternatives).map(getLevenshteinDistance).min();
16+
}
17+
18+
function validateAnswer(answer, solutions, useLevenshteinRatio) {
19+
if (useLevenshteinRatio) {
20+
return getSmallestLevenshteinRatio(answer, solutions) <= LEVENSHTEIN_DISTANCE_MAX_RATE;
21+
}
22+
return _.includes(solutions, answer);
23+
}
24+
25+
export {
26+
isOneStringCloseEnoughFromMultipleStrings,
27+
getSmallestLevenshteinDistance,
28+
getSmallestLevenshteinRatio,
29+
validateAnswer,
30+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import _ from 'lodash';
2+
3+
function normalizeAndRemoveAccents(value) {
4+
// Remove uppercase/spaces/accents/diacritics, see http://stackoverflow.com/a/37511463/827989
5+
return value
6+
.toString()
7+
.trim()
8+
.toLowerCase()
9+
.normalize('NFD')
10+
.replace(/[\u0300-\u036f]/g, '')
11+
.replace(/\s/g, '');
12+
}
13+
14+
function removeSpecialCharacters(value) {
15+
return value
16+
.toString()
17+
.replace(/[^a-zA-Z0-9 ]+/g, '')
18+
.replace('/ {2,}/', ' ')
19+
.replace(/\s\s+/g, ' ');
20+
}
21+
22+
function applyPreTreatments(value) {
23+
return value
24+
.toString()
25+
.normalize('NFC')
26+
.replace(/\u00A0/g, ' ');
27+
}
28+
29+
const treatments = {
30+
t1: normalizeAndRemoveAccents,
31+
t2: removeSpecialCharacters,
32+
};
33+
34+
function applyTreatments(string, enabledTreatments) {
35+
let result = string.toString();
36+
if (_.isEmpty(enabledTreatments)) {
37+
return result;
38+
}
39+
_(enabledTreatments)
40+
.sort()
41+
.each((treatment) => {
42+
const treatmentFunction = _.get(treatments, treatment);
43+
result = treatmentFunction ? treatmentFunction(result) : result;
44+
});
45+
return result;
46+
}
47+
48+
export { normalizeAndRemoveAccents, removeSpecialCharacters, applyPreTreatments, applyTreatments, treatments };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Solution } from '../../../../../src/devcomp/domain/models/Solution.js';
2+
3+
const buildSolution = function ({
4+
id = 'recCHAL123',
5+
type = 'QCM',
6+
value = ['1'],
7+
isT1Enabled = false,
8+
isT2Enabled = false,
9+
isT3Enabled = false,
10+
} = {}) {
11+
return new Solution({
12+
id,
13+
type,
14+
value,
15+
isT1Enabled,
16+
isT2Enabled,
17+
isT3Enabled,
18+
});
19+
};
20+
21+
export { buildSolution };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { buildSolution } from './build-solution.js';
2+
3+
export { buildSolution };

0 commit comments

Comments
 (0)