Skip to content

Commit 2c0148c

Browse files
[TECH] Récupérer les paliers acquis dans la page "Mes parcours" (PIX-8912)
2 parents 971920e + 91aab4b commit 2c0148c

14 files changed

+473
-215
lines changed

api/lib/domain/read-models/CampaignParticipationOverview.js

+8-14
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,34 @@ class CampaignParticipationOverview {
1010
sharedAt,
1111
organizationName,
1212
status,
13+
campaignId,
14+
targetProfileId,
1315
campaignCode,
1416
campaignTitle,
1517
campaignArchivedAt,
1618
deletedAt,
1719
masteryRate,
20+
totalStagesCount,
21+
validatedStagesCount,
1822
validatedSkillsCount,
19-
stageCollection,
2023
} = {}) {
2124
this.id = id;
2225
this.createdAt = createdAt;
26+
this.targetProfileId = targetProfileId;
2327
this.isShared = status === SHARED;
2428
this.sharedAt = sharedAt;
2529
this.organizationName = organizationName;
2630
this.status = status;
31+
this.campaignId = campaignId;
2732
this.campaignCode = campaignCode;
2833
this.campaignTitle = campaignTitle;
29-
this.stageCollection = stageCollection;
3034
this.masteryRate = !_.isNil(masteryRate) ? Number(masteryRate) : null;
3135
this.validatedSkillsCount = validatedSkillsCount;
32-
3336
const dates = [deletedAt, campaignArchivedAt].filter((a) => a != null);
34-
37+
this.totalStagesCount = totalStagesCount;
38+
this.validatedStagesCount = validatedStagesCount;
3539
this.disabledAt = _.min(dates) || null;
3640
}
37-
38-
get validatedStagesCount() {
39-
if (this.stageCollection.totalStages === 0 || !this.isShared) return null;
40-
41-
return this.stageCollection.getReachedStage(this.validatedSkillsCount, this.masteryRate * 100).reachedStage;
42-
}
43-
44-
get totalStagesCount() {
45-
return this.stageCollection.totalStages;
46-
}
4741
}
4842

4943
export { CampaignParticipationOverview };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* This service compares available and acquired stages for a campaign participation.
3+
*/
4+
class StagesAndAcquiredStagesComparison {
5+
/**
6+
* @type {Stage[]}
7+
*/
8+
#acquiredStages = [];
9+
10+
/**
11+
* @type {number}
12+
*/
13+
#totalNumberOfStages;
14+
15+
/**
16+
* @param {Stage[]} availableStages
17+
* @param {StageAcquisition[]} stageAcquisitions
18+
*/
19+
constructor(availableStages, stageAcquisitions) {
20+
this.#totalNumberOfStages = availableStages.length;
21+
this.#acquiredStages = availableStages
22+
.sort(this.#sortByLevelOrThreshold)
23+
.filter((availableStage) => stageAcquisitions.find(({ stageId }) => stageId === availableStage.id));
24+
}
25+
26+
/**
27+
* @param {Stage} previousStage
28+
* @param {Stage} currentStage
29+
*
30+
* @returns {-1, 0, 1}
31+
*/
32+
#sortByLevelOrThreshold(previousStage, currentStage) {
33+
if (currentStage.isFirstSkill) {
34+
return previousStage.isZeroStage ? -1 : 1;
35+
}
36+
return currentStage.level
37+
? previousStage.level - currentStage.level
38+
: previousStage.threshold - currentStage.threshold;
39+
}
40+
41+
/**
42+
* @returns {number}
43+
*/
44+
get reachedStageNumber() {
45+
return this.#acquiredStages.length;
46+
}
47+
48+
/**
49+
* @returns {number}
50+
*/
51+
get totalNumberOfStages() {
52+
return this.#totalNumberOfStages;
53+
}
54+
55+
/**
56+
* @returns {Stage}
57+
*/
58+
get reachedStage() {
59+
return this.#acquiredStages[this.#acquiredStages.length - 1];
60+
}
61+
}
62+
63+
/**
64+
* Compare stages and stages acquisitions to
65+
* build stages information for a campaign.
66+
*
67+
* @param {Stage[]} availableStages
68+
* @param {StageAcquisition[]} stageAcquisitions
69+
* @returns {{reachedStage: Stage, reachedStageNumber: number, totalNumberOfStages: number}}
70+
*/
71+
export const compare = (availableStages, stageAcquisitions) => {
72+
const stageComparison = new StagesAndAcquiredStagesComparison(availableStages, stageAcquisitions);
73+
74+
return {
75+
reachedStageNumber: stageComparison.reachedStageNumber,
76+
totalNumberOfStages: stageComparison.totalNumberOfStages,
77+
reachedStage: stageComparison.reachedStage,
78+
};
79+
};

api/lib/domain/usecases/find-user-campaign-participation-overviews.js

+39-6
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,50 @@ const findUserCampaignParticipationOverviews = async function ({
22
userId,
33
states,
44
page,
5+
stageRepository,
6+
stageAcquisitionRepository,
57
campaignParticipationOverviewRepository,
8+
stageAndStageAcquisitionComparisonService,
69
}) {
710
const concatenatedStates = states ? [].concat(states) : undefined;
811

9-
const campaignParticipationOverviews = await campaignParticipationOverviewRepository.findByUserIdWithFilters({
10-
userId,
11-
states: concatenatedStates,
12-
page,
13-
});
12+
const { campaignParticipationOverviews, pagination } =
13+
await campaignParticipationOverviewRepository.findByUserIdWithFilters({
14+
userId,
15+
states: concatenatedStates,
16+
page,
17+
});
1418

15-
return campaignParticipationOverviews;
19+
// We deduplicate targetProfileIds in the case where several campaigns belong to the same target profile
20+
const targetProfileIds = [...new Set(campaignParticipationOverviews.map(({ targetProfileId }) => targetProfileId))];
21+
const campaignParticipationIds = campaignParticipationOverviews.map(({ id }) => id);
22+
23+
const [stages, acquiredStages] = await Promise.all([
24+
stageRepository.getByTargetProfileIds(targetProfileIds),
25+
stageAcquisitionRepository.getByCampaignParticipations(campaignParticipationIds),
26+
]);
27+
28+
const campaignParticipationOverviewsWithStages = campaignParticipationOverviews.map(
29+
(campaignParticipationOverview) => {
30+
const stagesForThisCampaign = stages.filter(
31+
({ targetProfileId }) => targetProfileId === campaignParticipationOverview.targetProfileId,
32+
);
33+
const acquiredStagesForThisCampaign = acquiredStages.filter(
34+
({ campaignParticipationId }) => campaignParticipationId === campaignParticipationOverview.id,
35+
);
36+
const stagesComparison = stageAndStageAcquisitionComparisonService.compare(
37+
stagesForThisCampaign,
38+
acquiredStagesForThisCampaign,
39+
);
40+
41+
campaignParticipationOverview.totalStagesCount = stagesComparison.totalNumberOfStages;
42+
campaignParticipationOverview.validatedStagesCount = stagesComparison.reachedStageNumber;
43+
44+
return campaignParticipationOverview;
45+
},
46+
);
47+
48+
return { campaignParticipationOverviews: campaignParticipationOverviewsWithStages, pagination };
1649
};
1750

1851
export { findUserCampaignParticipationOverviews };

api/lib/domain/usecases/index.js

+15-7
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ import * as skillRepository from '../../infrastructure/repositories/skill-reposi
160160
import * as smartRandom from '../../domain/services/algorithm-methods/smart-random.js';
161161
import * as stageCollectionForTargetProfileRepository from '../../infrastructure/repositories/target-profile-management/stage-collection-repository.js';
162162
import * as stageCollectionRepository from '../../infrastructure/repositories/user-campaign-results/stage-collection-repository.js';
163+
import * as stageRepository from '../../infrastructure/repositories/stage-repository.js';
164+
import * as stageAcquisitionRepository from '../../infrastructure/repositories/stage-acquisition-repository.js';
163165
import * as studentRepository from '../../infrastructure/repositories/student-repository.js';
164166
import * as supervisorAccessRepository from '../../infrastructure/repositories/supervisor-access-repository.js';
165167
import * as supOrganizationLearnerRepository from '../../../src/prescription/learner-management/infrastructure/repositories/sup-organization-learner-repository.js';
@@ -187,6 +189,8 @@ import * as userToCreateRepository from '../../infrastructure/repositories/user-
187189
import * as userValidator from '../validators/user-validator.js';
188190
import * as verifyCertificateCodeService from '../../domain/services/verify-certificate-code-service.js';
189191
import * as writeCsvUtils from '../../infrastructure/utils/csv/write-csv-utils.js';
192+
import * as writeOdsUtils from '../../infrastructure/utils/ods/write-ods-utils.js';
193+
import * as stageAndStageAcquisitionComparisonService from '../../domain/services/stages/stage-and-stage-acquisition-comparison-service.js';
190194
import { CampaignParticipationsStatsRepository as campaignParticipationsStatsRepository } from '../../infrastructure/repositories/campaign-participations-stats-repository.js';
191195
import { campaignParticipantActivityRepository } from '../../infrastructure/repositories/campaign-participant-activity-repository.js';
192196
import { campaignParticipationResultRepository } from '../../infrastructure/repositories/campaign-participation-result-repository.js';
@@ -212,7 +216,6 @@ function requirePoleEmploiNotifier() {
212216
}
213217

214218
const dependencies = {
215-
TargetProfileForSpecifierRepository,
216219
accountRecoveryDemandRepository,
217220
activityAnswerRepository,
218221
activityRepository,
@@ -222,6 +225,7 @@ const dependencies = {
222225
areaRepository,
223226
assessmentRepository,
224227
assessmentResultRepository,
228+
attachableTargetProfileRepository,
225229
authenticationMethodRepository,
226230
authenticationServiceRegistry,
227231
authenticationSessionService,
@@ -234,7 +238,6 @@ const dependencies = {
234238
campaignAssessmentParticipationRepository,
235239
campaignAssessmentParticipationResultListRepository,
236240
campaignAssessmentParticipationResultRepository,
237-
codeGenerator,
238241
campaignCollectiveResultRepository,
239242
campaignCreatorRepository,
240243
campaignCsvExportService,
@@ -282,6 +285,7 @@ const dependencies = {
282285
certificationResultRepository,
283286
challengeRepository,
284287
cleaCertifiedCandidateRepository,
288+
codeGenerator,
285289
codeUtils,
286290
competenceEvaluationRepository,
287291
competenceMarkRepository,
@@ -334,9 +338,9 @@ const dependencies = {
334338
organizationPlacesCapacityRepository,
335339
organizationPlacesLotRepository,
336340
organizationRepository,
341+
organizationsToAttachToTargetProfileRepository,
337342
organizationTagRepository,
338343
organizationValidator,
339-
organizationsToAttachToTargetProfileRepository,
340344
participantResultRepository,
341345
participantResultsSharedRepository,
342346
participationsForCampaignManagementRepository,
@@ -371,15 +375,18 @@ const dependencies = {
371375
sessionsImportValidationService,
372376
skillRepository,
373377
smartRandom,
374-
stageCollectionRepository,
378+
stageAndStageAcquisitionComparisonService,
375379
stageCollectionForTargetProfileRepository,
380+
stageCollectionRepository,
381+
stageRepository,
382+
stageAcquisitionRepository,
376383
studentRepository,
384+
supervisorAccessRepository,
377385
supOrganizationLearnerRepository,
378386
supOrganizationParticipantRepository,
379-
supervisorAccessRepository,
380387
tagRepository,
381-
attachableTargetProfileRepository,
382388
targetProfileForAdminRepository,
389+
TargetProfileForSpecifierRepository,
383390
targetProfileForUpdateRepository,
384391
targetProfileRepository,
385392
targetProfileShareRepository,
@@ -395,8 +402,8 @@ const dependencies = {
395402
tutorialRepository: repositories.tutorialRepository,
396403
userEmailRepository,
397404
userLoginRepository,
398-
userOrgaSettingsRepository,
399405
userOrganizationsForAdminRepository,
406+
userOrgaSettingsRepository,
400407
userRecommendedTrainingRepository: repositories.userRecommendedTrainingRepository,
401408
userReconciliationService,
402409
userRepository,
@@ -406,6 +413,7 @@ const dependencies = {
406413
userValidator,
407414
verifyCertificateCodeService,
408415
writeCsvUtils,
416+
writeOdsUtils,
409417
attendanceSheetPdfUtils,
410418
};
411419

api/lib/infrastructure/repositories/campaign-participation-overview-repository.js

+4-17
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { knex } from '../../../db/knex-database-connection.js';
22
import { CampaignTypes } from '../../domain/models/CampaignTypes.js';
3-
import { CampaignParticipationOverview } from '../../domain/read-models/CampaignParticipationOverview.js';
43
import { fetchPage } from '../utils/knex-utils.js';
5-
import bluebird from 'bluebird';
64
import { CampaignParticipationStatuses } from '../../domain/models/CampaignParticipationStatuses.js';
7-
import * as stageCollectionRepository from './user-campaign-results/stage-collection-repository.js';
5+
import { CampaignParticipationOverview } from '../../domain/read-models/CampaignParticipationOverview.js';
86

97
const findByUserIdWithFilters = async function ({ userId, states, page }) {
108
const queryBuilder = _findByUserId({ userId });
@@ -15,10 +13,10 @@ const findByUserIdWithFilters = async function ({ userId, states, page }) {
1513

1614
const { results, pagination } = await fetchPage(queryBuilder, page);
1715

18-
const campaignParticipationOverviews = await _toReadModel(results);
19-
2016
return {
21-
campaignParticipationOverviews,
17+
campaignParticipationOverviews: results.map(
18+
(campaignParticipationOverview) => new CampaignParticipationOverview(campaignParticipationOverview),
19+
),
2220
pagination,
2321
};
2422
};
@@ -93,14 +91,3 @@ function _sortEndedBySharedAt() {
9391
function _filterByStates(queryBuilder, states) {
9492
queryBuilder.whereIn('participationState', states);
9593
}
96-
97-
function _toReadModel(campaignParticipationOverviews) {
98-
return bluebird.mapSeries(campaignParticipationOverviews, async (data) => {
99-
const stageCollection = await stageCollectionRepository.findStageCollection({ campaignId: data.campaignId });
100-
101-
return new CampaignParticipationOverview({
102-
...data,
103-
stageCollection,
104-
});
105-
});
106-
}

api/lib/infrastructure/repositories/stage-repository.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,16 @@ const getByCampaignParticipationId = async (campaignParticipationId, knexConnect
7171
.where('campaign-participations.id', campaignParticipationId),
7272
);
7373

74-
export { getByCampaignIds, getByCampaignId, getByCampaignParticipationId };
74+
/**
75+
* Return campaign stages for several target profile ids,
76+
* this is convenient for campaign overviews
77+
*
78+
* @param {[number]} targetProfileIds
79+
* @param knexConnection
80+
*
81+
* @returns Promise<Stage[]>
82+
*/
83+
const getByTargetProfileIds = async (targetProfileIds, knexConnection = knex) =>
84+
toDomain(await knexConnection('stages').select('stages.*').whereIn('stages.targetProfileId', targetProfileIds));
85+
86+
export { getByCampaignIds, getByCampaignId, getByCampaignParticipationId, getByTargetProfileIds };

0 commit comments

Comments
 (0)