Skip to content

Commit 9f9eb0f

Browse files
[FEATURE] ajoute le fitre lacune dans la route /assessment-results (pix-16350)
#11392
2 parents 1dcd24d + 96318d9 commit 9f9eb0f

File tree

10 files changed

+240
-27
lines changed

10 files changed

+240
-27
lines changed

Diff for: api/db/seeds/data/team-prescription/build-campaigns.js

+4
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ async function _createProGenericCampaigns(databaseBuilder) {
143143
}
144144

145145
async function _createProCampaigns(databaseBuilder) {
146+
await databaseBuilder.factory.buildTargetProfileShare({
147+
targetProfileId: TARGET_PROFILE_BADGES_STAGES_ID,
148+
organizationId: PRO_ORGANIZATION_ID,
149+
});
146150
await createProfilesCollectionCampaign({
147151
campaignId: CAMPAIGN_PROCOLMUL_ID,
148152
databaseBuilder,

Diff for: api/db/seeds/data/team-prescription/build-target-profiles.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ async function _createTargetProfileWithBadgesStages(databaseBuilder) {
7070
cappedTubesDTO,
7171
badgeId: BADGES_TUBES_CAMP_ID,
7272
altMessage: '1 RT double critère Campaign & Tubes',
73-
imageUrl: 'some_image.svg',
73+
imageUrl: 'https://images.pix.fr/badges/abcpix_je_navigue_sur_internet.svg',
7474
message: '1 RT double critère Campaign & Tubes',
7575
title: '1 RT double critère Campaign & Tubes',
7676
key: `SOME_KEY_FOR_RT_${BADGES_TUBES_CAMP_ID}`,
@@ -84,7 +84,7 @@ async function _createTargetProfileWithBadgesStages(databaseBuilder) {
8484
cappedTubesDTO,
8585
badgeId: BADGES_CAMP_ID,
8686
altMessage: '1 RT simple critère Campaign',
87-
imageUrl: 'some_other_image.svg',
87+
imageUrl: 'https://images.pix.fr/badges/Badge_OLYMPIX.svg',
8888
message: '1 RT simple critère Campaign',
8989
title: '1 RT simple critère Campaign',
9090
key: `SOME_KEY_FOR_RT_${BADGES_CAMP_ID}`,

Diff for: api/src/prescription/campaign/application/campaign-results-controller.js

+7-14
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,20 @@ import * as campaignAssessmentResultMinimalSerializer from '../infrastructure/se
55
import * as campaignCollectiveResultSerializer from '../infrastructure/serializers/jsonapi/campaign-collective-result-serializer.js';
66
import * as campaignProfilesCollectionParticipationSummarySerializer from '../infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer.js';
77

8-
const findAssessmentParticipationResults = async function (request) {
8+
const findAssessmentParticipationResults = async function (
9+
request,
10+
h,
11+
dependencies = { campaignAssessmentResultMinimalSerializer },
12+
) {
913
const { campaignId } = request.params;
1014
const { page, filter: filters } = request.query;
11-
if (filters.divisions && !Array.isArray(filters.divisions)) {
12-
filters.divisions = [filters.divisions];
13-
}
14-
if (filters.groups && !Array.isArray(filters.groups)) {
15-
filters.groups = [filters.groups];
16-
}
17-
if (filters.badges && !Array.isArray(filters.badges)) {
18-
filters.badges = [filters.badges];
19-
}
20-
if (filters.stages && !Array.isArray(filters.stages)) {
21-
filters.stages = [filters.stages];
22-
}
15+
2316
const paginatedParticipations = await usecases.findAssessmentParticipationResultList({
2417
campaignId,
2518
page,
2619
filters,
2720
});
28-
return campaignAssessmentResultMinimalSerializer.serialize(paginatedParticipations);
21+
return dependencies.campaignAssessmentResultMinimalSerializer.serialize(paginatedParticipations);
2922
};
3023

3124
const findProfilesCollectionParticipations = async function (request) {

Diff for: api/src/prescription/campaign/application/campaign-results-route.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ const register = async function (server) {
1919
filter: Joi.object({
2020
divisions: Joi.array().items(Joi.string()),
2121
groups: Joi.array().items(Joi.string()),
22-
badges: [Joi.number().integer(), Joi.array().items(Joi.number().integer())],
23-
stages: [Joi.number().integer(), Joi.array().items(Joi.number().integer())],
22+
badges: Joi.array().items(Joi.number().integer()),
23+
unacquiredBadges: Joi.array().items(Joi.number().integer()),
24+
stages: Joi.array().items(Joi.number().integer()),
2425
search: Joi.string().empty(''),
2526
}).default({}),
2627
page: {

Diff for: api/src/prescription/campaign/domain/errors.js

+10
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,18 @@ class CampaignParticipationDoesNotBelongToUser extends DomainError {
5858
}
5959
}
6060

61+
class AssessmentParticipationResultFilterError extends DomainError {
62+
constructor() {
63+
super(
64+
'Filtering on both acquired and unacquired using the same badge id is impossible',
65+
'ASSESSMENT_PARTICIPATION_RESULT_FILTER_ERROR',
66+
);
67+
}
68+
}
69+
6170
export {
6271
ArchivedCampaignError,
72+
AssessmentParticipationResultFilterError,
6373
CampaignCodeFormatError,
6474
CampaignParticipationDoesNotBelongToUser,
6575
CampaignUniqueCodeError,

Diff for: api/src/prescription/campaign/domain/usecases/find-assessment-participation-result-list.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import { AssessmentParticipationResultFilterError } from '../errors.js';
2+
13
async function findAssessmentParticipationResultList({
24
campaignId,
35
filters,
46
page,
57
campaignAssessmentParticipationResultListRepository,
68
}) {
9+
if (filters?.badges?.some((id) => filters?.unacquiredBadges?.includes(id))) {
10+
throw new AssessmentParticipationResultFilterError();
11+
}
712
return campaignAssessmentParticipationResultListRepository.findPaginatedByCampaignId({ campaignId, filters, page });
813
}
914

Diff for: api/src/prescription/campaign/infrastructure/repositories/campaign-assessment-participation-result-list-repository.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ function _getParticipantsResultList(campaignId, stageCollection, filters) {
3232
.with('campaign_participation_summaries', (qb) => _getParticipations(qb, campaignId, stageCollection, filters))
3333
.select('*')
3434
.from('campaign_participation_summaries')
35-
.modify(_filterByBadgeAcquisitionsOut, filters)
35+
.modify(_filterByAcquiredBadges, filters)
36+
.modify(_filterByUnacquiredBadges, filters)
3637
.orderByRaw('LOWER(??) ASC, LOWER(??) ASC', ['lastName', 'firstName']);
3738
}
3839

@@ -116,10 +117,10 @@ function _filterBySearch(queryBuilder, filters) {
116117
}
117118

118119
function _addAcquiredBadgeIds(queryBuilder, filters) {
119-
if (filters.badges) {
120+
if (filters.badges || filters.unacquiredBadges) {
120121
queryBuilder
121122
.select(knex.raw('ARRAY_AGG("badgeId") OVER (PARTITION BY "campaign-participations"."id") as badges_acquired'))
122-
.join('badge-acquisitions', 'badge-acquisitions.campaignParticipationId', 'campaign-participations.id')
123+
.leftJoin('badge-acquisitions', 'badge-acquisitions.campaignParticipationId', 'campaign-participations.id')
123124
.distinctOn('campaign-participations.id', 'campaign-participations.organizationLearnerId');
124125
}
125126
}
@@ -133,18 +134,26 @@ function _orderBy(queryBuilder, filters) {
133134
nulls: 'last',
134135
},
135136
];
136-
if (filters.badges) {
137+
if (filters.badges || filters.unacquiredBadges) {
137138
orderByClauses.unshift({ column: 'campaign-participations.id' });
138139
}
139140
queryBuilder.orderBy(orderByClauses);
140141
}
141142

142-
function _filterByBadgeAcquisitionsOut(queryBuilder, filters) {
143+
function _filterByAcquiredBadges(queryBuilder, filters) {
143144
if (filters.badges) {
144145
queryBuilder.whereRaw(':badgeIds <@ "badges_acquired"', { badgeIds: filters.badges });
145146
}
146147
}
147148

149+
function _filterByUnacquiredBadges(queryBuilder, filters) {
150+
if (filters.unacquiredBadges) {
151+
queryBuilder.whereRaw(':badgeIds && "badges_acquired" is false', {
152+
badgeIds: filters.unacquiredBadges,
153+
});
154+
}
155+
}
156+
148157
function _filterByStage(queryBuilder, stageCollection, filters) {
149158
if (!filters.stages) return;
150159
const allBoundaries = stageCollection.getThresholdBoundaries();

Diff for: api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-assessment-participation-result-list-repository_test.js

+131
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,137 @@ describe('Integration | Repository | Campaign Assessment Participation Result Li
934934
});
935935
});
936936

937+
context('when there is a filter on unacquiredBadges', function () {
938+
let badge1, badge2;
939+
let user1, user2;
940+
let participation1, participation2;
941+
942+
beforeEach(async function () {
943+
campaign = databaseBuilder.factory.buildAssessmentCampaignForSkills({}, [{ id: 'Skill1' }]);
944+
badge1 = databaseBuilder.factory.buildBadge({ key: 'badge1', targetProfileId: campaign.targetProfileId });
945+
badge2 = databaseBuilder.factory.buildBadge({ key: 'badge2', targetProfileId: campaign.targetProfileId });
946+
user1 = databaseBuilder.factory.buildUser();
947+
user2 = databaseBuilder.factory.buildUser();
948+
949+
participation1 = databaseBuilder.factory.buildCampaignParticipation({
950+
campaignId: campaign.id,
951+
userId: user1.id,
952+
});
953+
participation2 = databaseBuilder.factory.buildCampaignParticipation({
954+
campaignId: campaign.id,
955+
userId: user2.id,
956+
});
957+
958+
await databaseBuilder.commit();
959+
});
960+
961+
it('returns participants which does not have badge1', async function () {
962+
databaseBuilder.factory.buildBadgeAcquisition({
963+
badgeId: badge1.id,
964+
userId: participation1.userId,
965+
campaignParticipationId: participation1.id,
966+
});
967+
databaseBuilder.factory.buildBadgeAcquisition({
968+
badgeId: badge2.id,
969+
userId: participation2.userId,
970+
campaignParticipationId: participation2.id,
971+
});
972+
await databaseBuilder.commit();
973+
974+
// when
975+
const { participations } = await campaignAssessmentParticipationResultListRepository.findPaginatedByCampaignId({
976+
campaignId: campaign.id,
977+
filters: { unacquiredBadges: [badge1.id] },
978+
});
979+
980+
const participantExternalIds = participations.map((result) => result.campaignParticipationId);
981+
982+
// then
983+
expect(participantExternalIds).to.exactlyContain([participation2.id]);
984+
});
985+
986+
it('returns participants which does not have badge1 nor badge2', async function () {
987+
databaseBuilder.factory.buildBadgeAcquisition({
988+
badgeId: badge1.id,
989+
userId: participation1.userId,
990+
campaignParticipationId: participation1.id,
991+
});
992+
await databaseBuilder.commit();
993+
994+
// when
995+
const { participations } = await campaignAssessmentParticipationResultListRepository.findPaginatedByCampaignId({
996+
campaignId: campaign.id,
997+
filters: { unacquiredBadges: [badge1.id, badge2.id] },
998+
});
999+
1000+
const participantExternalIds = participations.map((result) => result.campaignParticipationId);
1001+
1002+
// then
1003+
expect(participantExternalIds).to.exactlyContain([participation2.id]);
1004+
});
1005+
1006+
it('returns no participant that have badge1 or badge2', async function () {
1007+
databaseBuilder.factory.buildBadgeAcquisition({
1008+
badgeId: badge1.id,
1009+
userId: participation1.userId,
1010+
campaignParticipationId: participation1.id,
1011+
});
1012+
1013+
databaseBuilder.factory.buildBadgeAcquisition({
1014+
badgeId: badge2.id,
1015+
userId: participation2.userId,
1016+
campaignParticipationId: participation2.id,
1017+
});
1018+
await databaseBuilder.commit();
1019+
1020+
// when
1021+
const { participations } = await campaignAssessmentParticipationResultListRepository.findPaginatedByCampaignId({
1022+
campaignId: campaign.id,
1023+
filters: { unacquiredBadges: [badge1.id, badge2.id] },
1024+
});
1025+
1026+
// then
1027+
expect(participations).to.be.empty;
1028+
});
1029+
});
1030+
1031+
context('when there is a filter on both unacquiredBadges and badges', function () {
1032+
let badge1;
1033+
let user1;
1034+
let participation1;
1035+
1036+
beforeEach(async function () {
1037+
campaign = databaseBuilder.factory.buildAssessmentCampaignForSkills({}, [{ id: 'Skill1' }]);
1038+
badge1 = databaseBuilder.factory.buildBadge({ key: 'badge1', targetProfileId: campaign.targetProfileId });
1039+
user1 = databaseBuilder.factory.buildUser();
1040+
1041+
participation1 = databaseBuilder.factory.buildCampaignParticipation({
1042+
campaignId: campaign.id,
1043+
userId: user1.id,
1044+
});
1045+
await databaseBuilder.commit();
1046+
});
1047+
1048+
it('returns no participant if acquired and unacquired badge have an id in common', async function () {
1049+
databaseBuilder.factory.buildBadgeAcquisition({
1050+
badgeId: badge1.id,
1051+
userId: participation1.userId,
1052+
campaignParticipationId: participation1.id,
1053+
});
1054+
1055+
await databaseBuilder.commit();
1056+
1057+
// when
1058+
const { participations } = await campaignAssessmentParticipationResultListRepository.findPaginatedByCampaignId({
1059+
campaignId: campaign.id,
1060+
filters: { badges: [badge1.id], unacquiredBadges: [badge1.id] },
1061+
});
1062+
1063+
// then
1064+
expect(participations).to.be.empty;
1065+
});
1066+
});
1067+
9371068
context('when there is a filter on stage', function () {
9381069
beforeEach(async function () {
9391070
const learningContent = [

Diff for: api/tests/prescription/campaign/unit/application/campaign-results-controller_test.js

+48
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,52 @@ describe('Unit | Application | Controller | Campaign Results', function () {
6464
expect(errorCatched).to.be.instanceof(UserNotAuthorizedToAccessEntityError);
6565
});
6666
});
67+
68+
describe('#findAssessmentParticipationResults', function () {
69+
const campaignId = 1;
70+
const userId = 1;
71+
const locale = FRENCH_SPOKEN;
72+
let campaignAssessmentResultMinimalSerializer;
73+
let pageSymbol, filterSymbol, resultSymbol, serializerResponseSymbol;
74+
75+
beforeEach(function () {
76+
sinon.stub(usecases, 'findAssessmentParticipationResultList');
77+
campaignAssessmentResultMinimalSerializer = {
78+
serialize: sinon.stub(),
79+
};
80+
pageSymbol = Symbol('pageSymbol');
81+
filterSymbol = Symbol('filter');
82+
resultSymbol = Symbol('result');
83+
serializerResponseSymbol = Symbol('serialize');
84+
});
85+
86+
it('should return serialized results', async function () {
87+
// given
88+
usecases.findAssessmentParticipationResultList
89+
.withArgs({
90+
campaignId,
91+
page: pageSymbol,
92+
filters: filterSymbol,
93+
})
94+
.resolves(resultSymbol);
95+
campaignAssessmentResultMinimalSerializer.serialize.withArgs(resultSymbol).resolves(serializerResponseSymbol);
96+
const request = {
97+
auth: { credentials: { userId } },
98+
params: { campaignId },
99+
query: {
100+
page: pageSymbol,
101+
filter: filterSymbol,
102+
},
103+
headers: { 'accept-language': locale },
104+
};
105+
106+
// when
107+
const response = await campaignResultsController.findAssessmentParticipationResults(request, hFake, {
108+
campaignAssessmentResultMinimalSerializer,
109+
});
110+
111+
// then
112+
expect(response).to.equal(serializerResponseSymbol);
113+
});
114+
});
67115
});
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,38 @@
1+
import { AssessmentParticipationResultFilterError } from '../../../../../../src/prescription/campaign/domain/errors.js';
12
import { findAssessmentParticipationResultList } from '../../../../../../src/prescription/campaign/domain/usecases/find-assessment-participation-result-list.js';
2-
import { expect, sinon } from '../../../../../test-helper.js';
3+
import { catchErr, expect, sinon } from '../../../../../test-helper.js';
34

45
describe('Unit | UseCase | find-assessment-participation-result-list', function () {
56
it('return the assessmentParticipationResultMinimal list', async function () {
67
const findPaginatedByCampaignId = sinon.stub();
7-
const checkIfUserOrganizationHasAccessToCampaign = sinon.stub();
88
const campaignId = 1;
99
const filters = Symbol('filters');
1010
const page = Symbol('page');
1111
const participations = Symbol('participations');
1212
findPaginatedByCampaignId.resolves(participations);
13-
checkIfUserOrganizationHasAccessToCampaign.resolves(true);
1413

1514
const results = await findAssessmentParticipationResultList({
1615
campaignId,
1716
filters,
1817
page,
1918
campaignAssessmentParticipationResultListRepository: { findPaginatedByCampaignId },
20-
campaignRepository: { checkIfUserOrganizationHasAccessToCampaign },
2119
});
2220

2321
expect(findPaginatedByCampaignId).to.have.been.calledWithExactly({ page, campaignId, filters });
2422
expect(results).to.equal(participations);
2523
});
24+
25+
it('throw when filter contain the same id on both badge and unacquiredBadge filters', async function () {
26+
const campaignId = 1;
27+
const filters = { unacquiredBadges: [1, 3], badges: [1, 2] };
28+
const page = Symbol('page');
29+
30+
const error = await catchErr(findAssessmentParticipationResultList)({
31+
campaignId,
32+
filters,
33+
page,
34+
});
35+
36+
expect(error).instanceOf(AssessmentParticipationResultFilterError);
37+
});
2638
});

0 commit comments

Comments
 (0)