Skip to content

Commit 2e3955e

Browse files
[TECH] Script pour fix les certification-challenge-capacities liés à des live-alerts (PIX-16701).
#11524
2 parents cbd1196 + c51c630 commit 2e3955e

File tree

2 files changed

+363
-0
lines changed

2 files changed

+363
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import 'dotenv/config';
2+
3+
import { knex } from '../../db/knex-database-connection.js';
4+
import { AlgorithmEngineVersion } from '../../src/certification/shared/domain/models/AlgorithmEngineVersion.js';
5+
import { Script } from '../../src/shared/application/scripts/script.js';
6+
import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js';
7+
8+
export class FixValidatedLiveAlertCertificationChallengeIds extends Script {
9+
constructor() {
10+
super({
11+
description: 'Fix certification-challenge-capacities from live alert misalignment',
12+
permanent: false,
13+
options: {
14+
dryRun: {
15+
type: 'boolean',
16+
describe: 'Commit the UPDATE or not',
17+
demandOption: true,
18+
},
19+
batchSize: {
20+
type: 'number',
21+
describe: 'Number of rows to update at once',
22+
demandOption: false,
23+
default: 1000,
24+
},
25+
delayBetweenBatch: {
26+
type: 'number',
27+
describe: 'In ms, force a pause between COMMIT',
28+
demandOption: false,
29+
default: 100,
30+
},
31+
},
32+
});
33+
34+
this.totalNumberOfUpdatedRows = 0;
35+
}
36+
37+
async handle({ options, logger }) {
38+
this.logger = logger;
39+
const dryRun = options.dryRun;
40+
const batchSize = options.batchSize;
41+
const delayInMs = options.delayBetweenBatch;
42+
this.logger.info(`dryRun=${dryRun}`);
43+
44+
let hasNext = true;
45+
let cursorId = 0;
46+
47+
do {
48+
const transaction = await knex.transaction();
49+
try {
50+
const courseIds = await this.#getCourseIds({
51+
cursorId,
52+
batchSize,
53+
transaction,
54+
});
55+
56+
for (const currentCourse of courseIds) {
57+
const results = await this.fixCertificationCapacities({ courseId: currentCourse.id, transaction });
58+
59+
this.logger.debug({ results });
60+
61+
this.totalNumberOfUpdatedRows += results.numberOfUpdates;
62+
}
63+
64+
dryRun ? await transaction.rollback() : await transaction.commit();
65+
66+
// Prepare for next batch
67+
hasNext = courseIds.length > 0;
68+
cursorId = courseIds.at(-1)?.id;
69+
await this.delay(delayInMs);
70+
} catch (error) {
71+
await transaction.rollback();
72+
throw error;
73+
}
74+
} while (hasNext);
75+
76+
this.logger.info({
77+
totalNumberOfUpdatedRows: this.totalNumberOfUpdatedRows,
78+
});
79+
80+
return 0;
81+
}
82+
83+
async fixCertificationCapacities({ courseId, transaction }) {
84+
const allCertificationChallenges = await transaction
85+
.select({
86+
id: 'certification-challenges.id',
87+
challengeId: 'certification-challenges.challengeId',
88+
})
89+
.from('certification-challenges')
90+
.where('certification-challenges.courseId', '=', courseId)
91+
.orderBy('certification-challenges.createdAt', 'asc');
92+
93+
// Find all capacities
94+
const certificationCapacities = await transaction
95+
.select({
96+
// table certif-challenges
97+
certifCourse_Id: 'certification-challenges.courseId',
98+
certifChallenge_Id: 'certification-challenges.id',
99+
certifChallenge_ChallengeId: 'certification-challenges.challengeId',
100+
// table answers
101+
answer_ChallengeId: 'answers.challengeId',
102+
// table capacities
103+
capacity_CertificationChallengeId: 'certification-challenge-capacities.certificationChallengeId',
104+
capacity_AnswerId: 'certification-challenge-capacities.answerId',
105+
})
106+
.from('certification-challenge-capacities')
107+
.innerJoin(
108+
'certification-challenges',
109+
'certification-challenge-capacities.certificationChallengeId',
110+
'certification-challenges.id',
111+
)
112+
.innerJoin('certification-courses', 'certification-challenges.courseId', 'certification-courses.id')
113+
.leftJoin('answers', 'certification-challenge-capacities.answerId', 'answers.id')
114+
.where('certification-challenges.courseId', '=', courseId)
115+
.orderBy('certification-challenges.createdAt', 'asc');
116+
117+
this.logger.debug({ certificationCapacities });
118+
119+
// Now fix capacities
120+
const capacitiesToUpdate = [];
121+
for (const currentCapacity of certificationCapacities) {
122+
this.logger.debug({ currentCapacity });
123+
if (currentCapacity.certifChallenge_ChallengeId !== currentCapacity.answer_ChallengeId) {
124+
// Copy capacity in error
125+
const capacityToModify = { ...currentCapacity };
126+
capacityToModify.capacity_CertificationChallengeId = 'REPLACE_ME';
127+
// Update also challengeId just to show we gound the right match in logs
128+
capacityToModify.certifChallenge_ChallengeId = 'REPLACE_ME';
129+
130+
// Get the right challenge id for this capacity answer
131+
const indexOfCorrectChallenge = allCertificationChallenges.findIndex(
132+
(certifChallenge) => certifChallenge.challengeId === currentCapacity.answer_ChallengeId,
133+
);
134+
135+
if (indexOfCorrectChallenge !== -1) {
136+
// Update capacity with right answer id
137+
capacityToModify.capacity_CertificationChallengeId =
138+
allCertificationChallenges.at(indexOfCorrectChallenge).id;
139+
// Update also certifChallengeChallengeId just to show we found the right match in logs
140+
capacityToModify.certifChallenge_ChallengeId =
141+
allCertificationChallenges.at(indexOfCorrectChallenge).challengeId;
142+
capacitiesToUpdate.push(capacityToModify);
143+
} else {
144+
// It is not possible that a capacity has a answer but no challenge
145+
throw new Error(`Capacity ${capacityToModify.capacity_AnswerId} do not have a corresponding challenge`);
146+
}
147+
148+
// That answer cannot be used anymore
149+
allCertificationChallenges.splice(indexOfCorrectChallenge, 1);
150+
} else {
151+
// We are on a correct certif challenge, forget about her
152+
const correctChallenge = allCertificationChallenges.findIndex(
153+
(certifChallenge) => certifChallenge.id === currentCapacity.certifChallenge_Id,
154+
);
155+
// That certif challenge cannot be used anymore
156+
allCertificationChallenges.splice(correctChallenge, 1);
157+
}
158+
}
159+
160+
// We have to do the update in reverse because certificationChallengeId is the PRIMARY KEY
161+
for (const capacityToUpdate of capacitiesToUpdate.reverse()) {
162+
await transaction('certification-challenge-capacities')
163+
.where('answerId', '=', capacityToUpdate.capacity_AnswerId)
164+
.update({
165+
certificationChallengeId: capacityToUpdate.capacity_CertificationChallengeId,
166+
});
167+
}
168+
169+
this.logger.debug({ capacitiesToUpdate });
170+
171+
return {
172+
numberOfUpdates: capacitiesToUpdate.length,
173+
};
174+
}
175+
176+
#getCourseIds({ cursorId, batchSize, transaction }) {
177+
return transaction
178+
.select('id')
179+
.from('certification-courses')
180+
.where('certification-courses.id', '>', cursorId)
181+
.andWhere('certification-courses.version', '=', AlgorithmEngineVersion.V3)
182+
.orderBy('certification-courses.id')
183+
.limit(batchSize);
184+
}
185+
186+
async delay(ms) {
187+
return new Promise((resolve) => setTimeout(resolve, ms));
188+
}
189+
}
190+
191+
await ScriptRunner.execute(import.meta.url, FixValidatedLiveAlertCertificationChallengeIds);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { FixValidatedLiveAlertCertificationChallengeIds } from '../../../../scripts/certification/fix-validated-live-alert-certification-challenge-ids.js';
2+
import { AlgorithmEngineVersion } from '../../../../src/certification/shared/domain/models/AlgorithmEngineVersion.js';
3+
import { CertificationChallengeLiveAlertStatus } from '../../../../src/certification/shared/domain/models/CertificationChallengeLiveAlert.js';
4+
import { databaseBuilder, expect, knex, sinon } from '../../../test-helper.js';
5+
6+
describe('Integration | Scripts | Certification | fix-validated-live-alert-certification-challenge-ids', function () {
7+
describe('when candidate has completed the test', function () {
8+
describe('when there is a live alert', function () {
9+
it('should fix the capacities challenges ids', async function () {
10+
// given
11+
const certificationCourseId = 321;
12+
const options = { dryRun: false, batchSize: 10 };
13+
const logger = {
14+
info: sinon.stub(),
15+
debug: sinon.stub(),
16+
};
17+
18+
const user = databaseBuilder.factory.buildUser();
19+
const certificationCourse = databaseBuilder.factory.buildCertificationCourse({
20+
id: certificationCourseId,
21+
userId: user.id,
22+
version: AlgorithmEngineVersion.V3,
23+
});
24+
const assessment = databaseBuilder.factory.buildAssessment({
25+
certificationCourseId: certificationCourse.id,
26+
userId: user.id,
27+
});
28+
const firstChallenge = databaseBuilder.factory.buildCertificationChallenge({
29+
id: 1,
30+
challengeId: 'rec123',
31+
courseId: certificationCourseId,
32+
});
33+
const secondChallenge = databaseBuilder.factory.buildCertificationChallenge({
34+
id: 2,
35+
challengeId: 'rec456',
36+
courseId: certificationCourseId,
37+
});
38+
const thirdChallengeWithLiveAlert = databaseBuilder.factory.buildCertificationChallenge({
39+
id: 3,
40+
challengeId: 'recWithLiveAlert',
41+
courseId: certificationCourseId,
42+
});
43+
const fourthChallenge = databaseBuilder.factory.buildCertificationChallenge({
44+
id: 4,
45+
challengeId: 'rec789',
46+
courseId: certificationCourseId,
47+
});
48+
const fifthChallenge = databaseBuilder.factory.buildCertificationChallenge({
49+
id: 5,
50+
challengeId: 'rec002',
51+
courseId: certificationCourseId,
52+
});
53+
54+
const firstAnswer = databaseBuilder.factory.buildAnswer({
55+
id: 1,
56+
challengeId: 'rec123',
57+
assessment: assessment.id,
58+
});
59+
const secondAnswer = databaseBuilder.factory.buildAnswer({
60+
id: 2,
61+
challengeId: 'rec456',
62+
assessment: assessment.id,
63+
});
64+
const thirdAnswer = databaseBuilder.factory.buildAnswer({
65+
id: 3,
66+
challengeId: 'rec789',
67+
assessment: assessment.id,
68+
});
69+
const fourthAnswer = databaseBuilder.factory.buildAnswer({
70+
id: 4,
71+
challengeId: 'rec002',
72+
assessment: assessment.id,
73+
});
74+
75+
databaseBuilder.factory.buildCertificationChallengeLiveAlert({
76+
assessmentId: assessment.id,
77+
challengeId: 'recWithLiveAlert',
78+
status: CertificationChallengeLiveAlertStatus.VALIDATED,
79+
questionNumber: 3,
80+
});
81+
82+
databaseBuilder.factory.buildCertificationChallengeCapacity({
83+
certificationChallengeId: firstChallenge.id,
84+
answerId: firstAnswer.id,
85+
capacity: 1,
86+
createdAt: new Date('2020-01-01'),
87+
});
88+
databaseBuilder.factory.buildCertificationChallengeCapacity({
89+
certificationChallengeId: secondChallenge.id,
90+
answerId: secondAnswer.id,
91+
capacity: 2,
92+
createdAt: new Date('2020-01-01'),
93+
});
94+
databaseBuilder.factory.buildCertificationChallengeCapacity({
95+
certificationChallengeId: thirdChallengeWithLiveAlert.id,
96+
answerId: thirdAnswer.id,
97+
capacity: 3,
98+
createdAt: new Date('2020-01-01'),
99+
});
100+
101+
databaseBuilder.factory.buildCertificationChallengeCapacity({
102+
certificationChallengeId: fourthChallenge.id,
103+
answerId: fourthAnswer.id,
104+
capacity: 4,
105+
createdAt: new Date('2020-01-01'),
106+
});
107+
108+
await databaseBuilder.commit();
109+
110+
const script = new FixValidatedLiveAlertCertificationChallengeIds();
111+
112+
// when
113+
await script.handle({ options, logger });
114+
115+
// then
116+
const certificationChallengeCapacities = await knex('certification-challenge-capacities').orderBy('answerId');
117+
expect(certificationChallengeCapacities).to.deep.equal([
118+
{
119+
certificationChallengeId: firstChallenge.id,
120+
answerId: firstAnswer.id,
121+
capacity: 1,
122+
createdAt: new Date('2020-01-01'),
123+
},
124+
{
125+
certificationChallengeId: secondChallenge.id,
126+
answerId: secondChallenge.id,
127+
capacity: 2,
128+
createdAt: new Date('2020-01-01'),
129+
},
130+
{
131+
certificationChallengeId: fourthChallenge.id,
132+
answerId: thirdAnswer.id,
133+
capacity: 3,
134+
createdAt: new Date('2020-01-01'),
135+
},
136+
{
137+
certificationChallengeId: fifthChallenge.id,
138+
answerId: fourthAnswer.id,
139+
capacity: 4,
140+
createdAt: new Date('2020-01-01'),
141+
},
142+
]);
143+
144+
const certificationChallenges = await knex('certification-challenges')
145+
.select('id', 'challengeId')
146+
.orderBy('id');
147+
expect(certificationChallenges).to.deep.equal([
148+
{
149+
challengeId: firstChallenge.challengeId,
150+
id: firstChallenge.id,
151+
},
152+
{
153+
challengeId: secondChallenge.challengeId,
154+
id: secondChallenge.id,
155+
},
156+
{
157+
challengeId: thirdChallengeWithLiveAlert.challengeId,
158+
id: thirdChallengeWithLiveAlert.id,
159+
},
160+
{
161+
challengeId: fourthChallenge.challengeId,
162+
id: fourthChallenge.id,
163+
},
164+
{
165+
challengeId: fifthChallenge.challengeId,
166+
id: fifthChallenge.id,
167+
},
168+
]);
169+
});
170+
});
171+
});
172+
});

0 commit comments

Comments
 (0)