Skip to content

Commit 857f126

Browse files
alexandrecoinyaf
authored andcommitted
tech(api): update doubled answers and remove unnecessarry data
1 parent ddbcc6d commit 857f126

File tree

2 files changed

+318
-0
lines changed

2 files changed

+318
-0
lines changed

Diff for: api/scripts/certification/fix-doubled-answers.js

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import 'dotenv/config';
2+
3+
import Joi from 'joi';
4+
5+
import { knex } from '../../db/knex-database-connection.js';
6+
import { csvFileParser } from '../../src/shared/application/scripts/parsers.js';
7+
import { Script } from '../../src/shared/application/scripts/script.js';
8+
import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js';
9+
import { Assessment } from '../../src/shared/domain/models/index.js';
10+
11+
const columnsSchemas = [
12+
{ name: 'certificationChallengeId', schema: Joi.number() },
13+
{ name: 'answerId', schema: Joi.number() },
14+
{ name: 'completionDate', schema: Joi.string() },
15+
];
16+
17+
export class FixDoubledAnswers extends Script {
18+
constructor() {
19+
super({
20+
description: 'Fix doubled answers in certification',
21+
permanent: false,
22+
options: {
23+
file: {
24+
type: 'string',
25+
describe:
26+
'CSV File with three columns with certificationChallengeIds (integer), answerIds (integer) and completion date (string) to process',
27+
demandOption: true,
28+
coerce: csvFileParser(columnsSchemas),
29+
},
30+
assessmentId: {
31+
type: 'integer',
32+
describe: 'Id of the assessment answers should be linked to',
33+
demandOption: true,
34+
},
35+
dryRun: {
36+
type: 'boolean',
37+
describe: 'Commit the UPDATE or not',
38+
demandOption: false,
39+
default: true,
40+
},
41+
},
42+
});
43+
}
44+
45+
async handle({ options, logger }) {
46+
const { file: certifChallengeAndAnswerIds, assessmentId, dryRun } = options;
47+
this.logger = logger;
48+
49+
this.logger.info(`dryRun=${dryRun}`);
50+
51+
for (const line of certifChallengeAndAnswerIds) {
52+
const transaction = await knex.transaction();
53+
54+
const { certificationChallengeId, answerId, completionDate } = line;
55+
try {
56+
// Retrieving the certification-course for upcoming update
57+
const certificationChallenge = await transaction('certification-challenges')
58+
.where({
59+
id: certificationChallengeId,
60+
})
61+
.first();
62+
const certificationCourse = await transaction('certification-courses')
63+
.where({
64+
id: certificationChallenge.courseId,
65+
})
66+
.first();
67+
68+
// Update of the certification-course
69+
await transaction('certification-courses').where({ id: certificationCourse.id }).update({
70+
abortReason: null,
71+
completedAt: completionDate,
72+
endedAt: completionDate,
73+
updatedAt: completionDate,
74+
});
75+
76+
// Retrieving the assessment for upcoming update
77+
const assessment = await transaction('assessments')
78+
.where({ certificationCourseId: certificationCourse.id })
79+
.first();
80+
81+
// Removing the certification-challenge-capacity that is not supposed to exist
82+
await transaction('certification-challenge-capacities').where({ certificationChallengeId, answerId }).delete();
83+
this.logger.info(
84+
`certification-challenge-capacity with certificationChallengeId:${certificationChallengeId} deleted`,
85+
);
86+
87+
// Removing the certification-challenge that is not supposed to exist
88+
await transaction('certification-challenges').where('id', '=', certificationChallengeId).delete();
89+
this.logger.info(`certification-challenge: ${certificationChallengeId} deleted`);
90+
91+
// assessment update
92+
const newLastCertificationChallenge = await transaction('certification-challenges')
93+
.where({
94+
courseId: certificationCourse.id,
95+
})
96+
.orderBy('id', 'desc')
97+
.first();
98+
99+
await transaction('assessments').where({ id: assessment.id }).update({
100+
state: Assessment.states.COMPLETED,
101+
lastChallengeId: newLastCertificationChallenge.challengeId,
102+
updatedAt: completionDate,
103+
lastQuestionDate: completionDate,
104+
});
105+
106+
// Link the answer that is not supposed to exist to an assessment created for this purpose
107+
await transaction('answers').where('id', '=', answerId).update({
108+
assessmentId,
109+
});
110+
this.logger.info(`answer: ${answerId} moved to assessment ${assessmentId}`);
111+
112+
dryRun ? await transaction.rollback() : await transaction.commit();
113+
} catch (error) {
114+
await transaction.rollback();
115+
throw error;
116+
}
117+
}
118+
return 0;
119+
}
120+
}
121+
122+
await ScriptRunner.execute(import.meta.url, FixDoubledAnswers);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { FixDoubledAnswers } from '../../../../scripts/certification/fix-doubled-answers.js';
2+
import { CertificationAssessment } from '../../../../src/certification/session-management/domain/models/CertificationAssessment.js';
3+
import { AlgorithmEngineVersion } from '../../../../src/certification/shared/domain/models/AlgorithmEngineVersion.js';
4+
import { ABORT_REASONS } from '../../../../src/certification/shared/domain/models/CertificationCourse.js';
5+
import { Assessment } from '../../../../src/shared/domain/models/index.js';
6+
import { createTempFile, databaseBuilder, expect, knex, sinon } from '../../../test-helper.js';
7+
8+
describe('Integration | Scripts | Certification | fix-doubled-answers', function () {
9+
it('should parse input file', async function () {
10+
const script = new FixDoubledAnswers();
11+
const { options } = script.metaInfo;
12+
const file = 'doubled-answers.csv';
13+
const data =
14+
'certificationChallengeId,answerId,completionDate\n1,2,2021-01-02 8:20:45.000000+01:00\n3,4,2021-01-02 9:20:45.000000+01:00\n5,6,2021-01-02 10:20:45.000000+01:00\n';
15+
const csvFilePath = await createTempFile(file, data);
16+
17+
const parsedData = await options.file.coerce(csvFilePath);
18+
19+
expect(parsedData).to.deep.equal([
20+
{ certificationChallengeId: 1, answerId: 2, completionDate: '2021-01-02 8:20:45.000000+01:00' },
21+
{ certificationChallengeId: 3, answerId: 4, completionDate: '2021-01-02 9:20:45.000000+01:00' },
22+
{ certificationChallengeId: 5, answerId: 6, completionDate: '2021-01-02 10:20:45.000000+01:00' },
23+
]);
24+
});
25+
26+
it('should link doubled answers to another assessment', async function () {
27+
// given
28+
const certificationCourseId = 123;
29+
const secondCertificationCourseId = 456;
30+
const logger = {
31+
info: sinon.stub(),
32+
debug: sinon.stub(),
33+
};
34+
35+
const assessmentToUseForUpdate = await databaseBuilder.factory.buildAssessment({
36+
id: 123,
37+
type: Assessment.types.COMPETENCE_EVALUATION,
38+
});
39+
const user = databaseBuilder.factory.buildUser();
40+
41+
const firstCertificationCourse = databaseBuilder.factory.buildCertificationCourse({
42+
id: certificationCourseId,
43+
userId: user.id,
44+
version: AlgorithmEngineVersion.V3,
45+
abortReason: ABORT_REASONS.TECHNICAL,
46+
completedAt: null,
47+
finalizedAt: new Date('2021-01-01'),
48+
});
49+
const secondCertificationCourse = databaseBuilder.factory.buildCertificationCourse({
50+
id: secondCertificationCourseId,
51+
userId: user.id,
52+
version: AlgorithmEngineVersion.V3,
53+
abortReason: ABORT_REASONS.TECHNICAL,
54+
completedAt: null,
55+
finalizedAt: new Date('2021-01-01'),
56+
});
57+
58+
const firstAssessmentId = databaseBuilder.factory.buildAssessment({
59+
certificationCourseId: firstCertificationCourse.id,
60+
userId: user.id,
61+
type: Assessment.types.CERTIFICATION,
62+
state: 'endedDueToFinalization',
63+
}).id;
64+
const secondAssessmentId = databaseBuilder.factory.buildAssessment({
65+
certificationCourseId: secondCertificationCourse.id,
66+
userId: user.id,
67+
type: Assessment.types.CERTIFICATION,
68+
state: 'endedDueToFinalization',
69+
}).id;
70+
71+
const firstCertificationChallengeToKeep = databaseBuilder.factory.buildCertificationChallenge({
72+
id: 1,
73+
challengeId: 'recYYYY',
74+
courseId: certificationCourseId,
75+
});
76+
const firstCertificationChallengeToBeRemoved = databaseBuilder.factory.buildCertificationChallenge({
77+
id: 2,
78+
challengeId: 'rec123',
79+
courseId: certificationCourseId,
80+
});
81+
82+
const secondCertificationChallengeToKeep = databaseBuilder.factory.buildCertificationChallenge({
83+
id: 3,
84+
challengeId: 'recXXXX',
85+
courseId: secondCertificationCourseId,
86+
});
87+
const secondCertificationChallengeToBeRemoved = databaseBuilder.factory.buildCertificationChallenge({
88+
id: 4,
89+
challengeId: 'rec456',
90+
courseId: secondCertificationCourseId,
91+
});
92+
93+
const firstAnswerToBeUpdated = databaseBuilder.factory.buildAnswer({
94+
id: 1,
95+
challengeId: 'rec123',
96+
assessmentId: firstAssessmentId,
97+
});
98+
const secondAnswerToBeUpdated = databaseBuilder.factory.buildAnswer({
99+
id: 2,
100+
challengeId: 'rec456',
101+
assessmentId: secondAssessmentId,
102+
});
103+
104+
databaseBuilder.factory.buildCertificationChallengeCapacity({
105+
certificationChallengeId: firstCertificationChallengeToBeRemoved.id,
106+
answerId: firstAnswerToBeUpdated.id,
107+
capacity: 1,
108+
});
109+
databaseBuilder.factory.buildCertificationChallengeCapacity({
110+
certificationChallengeId: secondCertificationChallengeToBeRemoved.id,
111+
answerId: secondAnswerToBeUpdated.id,
112+
capacity: 2,
113+
});
114+
115+
await databaseBuilder.commit();
116+
117+
const file = [
118+
{
119+
certificationChallengeId: firstCertificationChallengeToBeRemoved.id,
120+
answerId: firstAnswerToBeUpdated.id,
121+
completionDate: '2021-01-02 8:20:45.000000+01:00',
122+
},
123+
{
124+
certificationChallengeId: secondCertificationChallengeToBeRemoved.id,
125+
answerId: secondAnswerToBeUpdated.id,
126+
completionDate: '2021-01-02 9:20:45.000000+01:00',
127+
},
128+
];
129+
const script = new FixDoubledAnswers();
130+
131+
// when
132+
await script.handle({ options: { file, assessmentId: assessmentToUseForUpdate.id, dryRun: false }, logger });
133+
134+
// then
135+
const answers = await knex('answers').select('id', 'assessmentId').orderBy('id');
136+
expect(answers).to.deep.equal([
137+
{
138+
id: firstAnswerToBeUpdated.id,
139+
assessmentId: assessmentToUseForUpdate.id,
140+
},
141+
{
142+
id: secondAnswerToBeUpdated.id,
143+
assessmentId: assessmentToUseForUpdate.id,
144+
},
145+
]);
146+
const certificationChallenges = await knex('certification-challenges').select('id');
147+
expect(certificationChallenges).to.deep.equal([{ id: 1 }, { id: 3 }]);
148+
149+
const certificationChallengeCapacities = await knex('certification-challenge-capacities');
150+
expect(certificationChallengeCapacities).to.deep.equal([]);
151+
152+
const assessments = await knex('assessments')
153+
.select('id', 'updatedAt', 'state', 'lastChallengeId', 'lastQuestionDate')
154+
.where({ type: Assessment.types.CERTIFICATION });
155+
expect(assessments).to.deep.equal([
156+
{
157+
id: firstAssessmentId,
158+
state: CertificationAssessment.states.COMPLETED,
159+
updatedAt: new Date('2021-01-02T07:20:45Z'),
160+
lastChallengeId: firstCertificationChallengeToKeep.challengeId,
161+
lastQuestionDate: new Date('2021-01-02T07:20:45Z'),
162+
},
163+
{
164+
id: secondAssessmentId,
165+
state: CertificationAssessment.states.COMPLETED,
166+
updatedAt: new Date('2021-01-02T08:20:45Z'),
167+
lastChallengeId: secondCertificationChallengeToKeep.challengeId,
168+
lastQuestionDate: new Date('2021-01-02T08:20:45Z'),
169+
},
170+
]);
171+
172+
const certificationCourses = await knex('certification-courses').select(
173+
'id',
174+
'completedAt',
175+
'updatedAt',
176+
'endedAt',
177+
'abortReason',
178+
);
179+
expect(certificationCourses).to.deep.equal([
180+
{
181+
id: certificationCourseId,
182+
completedAt: new Date('2021-01-02T07:20:45Z'),
183+
updatedAt: new Date('2021-01-02T07:20:45Z'),
184+
endedAt: new Date('2021-01-02T07:20:45Z'),
185+
abortReason: null,
186+
},
187+
{
188+
id: secondCertificationCourseId,
189+
completedAt: new Date('2021-01-02T08:20:45Z'),
190+
updatedAt: new Date('2021-01-02T08:20:45Z'),
191+
endedAt: new Date('2021-01-02T08:20:45Z'),
192+
abortReason: null,
193+
},
194+
]);
195+
});
196+
});

0 commit comments

Comments
 (0)