From dc4966182a25d6e87df53a269ecd060133a2ba41 Mon Sep 17 00:00:00 2001 From: ThomasBazin Date: Wed, 13 Nov 2024 15:44:52 +0100 Subject: [PATCH 1/6] feat(api): add sharedProfileCount to model --- .../CampaignProfilesCollectionParticipationSummary.js | 2 ++ .../CampaignProfilesCollectionParticipationSummary_test.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/api/src/prescription/campaign/domain/read-models/CampaignProfilesCollectionParticipationSummary.js b/api/src/prescription/campaign/domain/read-models/CampaignProfilesCollectionParticipationSummary.js index 74b114cc672..6303e450ff8 100644 --- a/api/src/prescription/campaign/domain/read-models/CampaignProfilesCollectionParticipationSummary.js +++ b/api/src/prescription/campaign/domain/read-models/CampaignProfilesCollectionParticipationSummary.js @@ -6,6 +6,7 @@ class CampaignProfilesCollectionParticipationSummary { participantExternalId, sharedAt, pixScore, + sharedProfileCount, previousPixScore, previousSharedAt, certifiable, @@ -17,6 +18,7 @@ class CampaignProfilesCollectionParticipationSummary { this.participantExternalId = participantExternalId; this.sharedAt = sharedAt; this.pixScore = pixScore; + this.sharedProfileCount = sharedProfileCount; this.previousPixScore = previousPixScore ?? null; this.previousSharedAt = previousSharedAt ?? null; this.evolution = this.#computeEvolution(this.pixScore, this.previousPixScore); diff --git a/api/tests/prescription/campaign/unit/domain/read-models/CampaignProfilesCollectionParticipationSummary_test.js b/api/tests/prescription/campaign/unit/domain/read-models/CampaignProfilesCollectionParticipationSummary_test.js index fd732564da5..65ef4bf9248 100644 --- a/api/tests/prescription/campaign/unit/domain/read-models/CampaignProfilesCollectionParticipationSummary_test.js +++ b/api/tests/prescription/campaign/unit/domain/read-models/CampaignProfilesCollectionParticipationSummary_test.js @@ -11,6 +11,7 @@ describe('Unit | Domain | Read-Models | CampaignResults | CampaignProfilesCollec participantExternalId: 'Sarah2024', sharedAt: '2024-10-28', pixScore: 20, + sharedProfileCount: 2, previousPixScore: null, previousSharedAt: null, certifiable: true, @@ -40,6 +41,7 @@ describe('Unit | Domain | Read-Models | CampaignResults | CampaignProfilesCollec participantExternalId: 'Sarah2024', certifiable: true, certifiableCompetencesCount: 9, + sharedProfileCount: 2, }; describe('when previous participation pixScore and shared date are undefined', function () { From 0506b12a065e981606949d4c5bff79ed5bfd1ff7 Mon Sep 17 00:00:00 2001 From: ThomasBazin Date: Wed, 13 Nov 2024 15:45:36 +0100 Subject: [PATCH 2/6] feat(api): add sharedProfileCount to repository --- ...ection-participation-summary-repository.js | 7 + ...n-participation-summary-repository_test.js | 148 +++++++++++++++++- 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js b/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js index 860ee35a7e8..ee235516906 100644 --- a/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js +++ b/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js @@ -66,6 +66,13 @@ async function _getParticipations(qb, campaignId, filters) { 'campaign-participations.pixScore AS pixScore', 'previousParticipationsInfos.previousPixScore', 'previousParticipationsInfos.previousSharedAt', + knex('campaign-participations') + .as('sharedProfileCount') + .count() + .whereRaw('"campaign-participations"."organizationLearnerId" = "view-active-organization-learners".id') + .whereNotNull('campaign-participations.sharedAt') + .whereNull('campaign-participations.deletedAt') + .where('campaign-participations.campaignId', campaignId), ) .distinctOn('campaign-participations.organizationLearnerId') .from('campaign-participations') diff --git a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository_test.js b/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository_test.js index 2f3990db2b9..2aae844023a 100644 --- a/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository_test.js +++ b/api/tests/prescription/campaign/integration/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository_test.js @@ -177,7 +177,7 @@ describe('Integration | Repository | Campaign Profiles Collection Participation await databaseBuilder.commit(); }); - it('should return the certification profile info and pix score', async function () { + it('should return the certification profile info, pix score and count', async function () { // when const results = await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId); @@ -191,6 +191,7 @@ describe('Integration | Repository | Campaign Profiles Collection Participation participantExternalId: 'JeBu', sharedAt, pixScore: 46, + sharedProfileCount: 1, certifiable: false, certifiableCompetencesCount: 1, }), @@ -260,6 +261,151 @@ describe('Integration | Repository | Campaign Profiles Collection Participation }); }); + context('participations count', function () { + beforeEach(async function () { + databaseBuilder.factory.buildCampaignParticipation({ + campaignId, + sharedAt: new Date('2020-01-02'), + createdAt: new Date('2020-01-02'), + isImproved: true, + userId: organizationLearner.userId, + organizationLearnerId: organizationLearner.id, + }); + + await databaseBuilder.commit(); + }); + + describe('when participant has only one shared participation', function () { + it('should count one participation', async function () { + // when + const results = + await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId); + + // then + expect(results.data[0].sharedProfileCount).to.equal(1); + }); + }); + describe('when participant has multiple participations', function () { + it('should return the count of shared participations only', async function () { + // given + databaseBuilder.factory.buildCampaignParticipation({ + isImproved: true, + sharedAt: new Date('2022-01-02'), + createdAt: new Date('2022-01-02'), + status: SHARED, + campaignId, + userId: organizationLearner.userId, + organizationLearnerId: organizationLearner.id, + }); + + databaseBuilder.factory.buildCampaignParticipation({ + isImproved: false, + sharedAt: null, + createdAt: new Date('2022-01-02'), + status: TO_SHARE, + campaignId, + userId: organizationLearner.userId, + organizationLearnerId: organizationLearner.id, + }); + + await databaseBuilder.commit(); + + // when + const results = + await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId); + + // then + expect(results.data[0].sharedProfileCount).to.equal(2); + }); + + it('should not count a deleted participation', async function () { + // given deleted participation + databaseBuilder.factory.buildCampaignParticipation({ + isImproved: true, + sharedAt: new Date('2022-01-02'), + createdAt: new Date('2022-01-02'), + deletedAt: new Date('2022-01-02'), + status: SHARED, + campaignId, + userId: organizationLearner.userId, + organizationLearnerId: organizationLearner.id, + }); + + // given not shared participation + databaseBuilder.factory.buildCampaignParticipation({ + isImproved: false, + sharedAt: null, + createdAt: new Date('2022-01-02'), + status: TO_SHARE, + campaignId, + userId: organizationLearner.userId, + organizationLearnerId: organizationLearner.id, + }); + + await databaseBuilder.commit(); + + // when + const results = + await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId); + + // then + expect(results.data[0].sharedProfileCount).to.equal(1); + }); + }); + + describe('when there is another participant for same campaign', function () { + it('should only count shared participations for same learner', async function () { + // given second organisation learner and his participation + const secondOrganizationLearner = databaseBuilder.factory.buildOrganizationLearner({ organizationId }); + databaseBuilder.factory.buildCampaignParticipation({ + isImproved: false, + sharedAt: new Date('2022-01-02'), + createdAt: new Date('2022-01-02'), + status: SHARED, + campaignId, + userId: secondOrganizationLearner.userId, + organizationLearnerId: secondOrganizationLearner.id, + }); + + await databaseBuilder.commit(); + + // when + const results = + await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId); + + // then + expect(results.data[0].sharedProfileCount).to.equal(1); + expect(results.data[1].sharedProfileCount).to.equal(1); + }); + }); + + describe('when participant has participations to different campaigns', function () { + it('should only count shared participations for same campaign', async function () { + // given second campaign and participation + const secondCampaignId = databaseBuilder.factory.buildCampaign({ organizationId }).id; + + databaseBuilder.factory.buildCampaignParticipation({ + isImproved: false, + sharedAt: new Date('2022-01-02'), + createdAt: new Date('2022-01-02'), + status: SHARED, + campaignId: secondCampaignId, + userId: organizationLearner.userId, + organizationLearnerId: organizationLearner.id, + }); + + await databaseBuilder.commit(); + + // when + const results = + await campaignProfilesCollectionParticipationSummaryRepository.findPaginatedByCampaignId(campaignId); + + // then + expect(results.data[0].sharedProfileCount).to.equal(1); + }); + }); + }); + context('additionnal informations about previous participation and evolution', function () { let userId, organizationId, From e4ca8d0f21e667eeccb4759b46f3965e1d14f7ad Mon Sep 17 00:00:00 2001 From: ThomasBazin Date: Wed, 13 Nov 2024 15:46:13 +0100 Subject: [PATCH 3/6] feat(api): add sharedProfileCount to serializer --- ...aign-profiles-collection-participation-summary-serializer.js | 1 + ...profiles-collection-participation-summary-serializer_test.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/api/src/prescription/campaign/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer.js b/api/src/prescription/campaign/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer.js index 87de6191076..67de1f3575a 100644 --- a/api/src/prescription/campaign/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer.js +++ b/api/src/prescription/campaign/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer.js @@ -11,6 +11,7 @@ const serialize = function ({ data, pagination }) { 'participantExternalId', 'sharedAt', 'pixScore', + 'sharedProfileCount', 'previousPixScore', 'previousSharedAt', 'evolution', diff --git a/api/tests/prescription/campaign/unit/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer_test.js b/api/tests/prescription/campaign/unit/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer_test.js index 1bf437b5801..6e6fc21caa6 100644 --- a/api/tests/prescription/campaign/unit/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer_test.js +++ b/api/tests/prescription/campaign/unit/infrastructure/serializers/jsonapi/campaign-profiles-collection-participation-summary-serializer_test.js @@ -13,6 +13,7 @@ describe('Unit | Serializer | JSONAPI | campaign-profiles-collection-participati participantExternalId: 'abo', sharedAt: new Date(2020, 2, 2), pixScore: 1024, + sharedProfileCount: 1, previousPixScore: 512, previousSharedAt: new Date(2024, 10, 29), certifiable: true, @@ -29,6 +30,7 @@ describe('Unit | Serializer | JSONAPI | campaign-profiles-collection-participati 'participant-external-id': 'abo', 'shared-at': new Date(2020, 2, 2), 'pix-score': 1024, + 'shared-profile-count': 1, 'previous-pix-score': 512, 'previous-shared-at': new Date(2024, 10, 29), evolution: 'increase', From e0a3a080f4b16046fec87c3b28cdfe9fbae91544 Mon Sep 17 00:00:00 2001 From: ThomasBazin Date: Wed, 13 Nov 2024 16:33:33 +0100 Subject: [PATCH 4/6] feat(orga): add translations for shared profiles count --- orga/translations/en.json | 4 +++- orga/translations/fr.json | 4 +++- orga/translations/nl.json | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/orga/translations/en.json b/orga/translations/en.json index e928a3cf34b..b1090443c83 100644 --- a/orga/translations/en.json +++ b/orga/translations/en.json @@ -1270,6 +1270,7 @@ "title": "List of submitted profiles", "caption": "This table contains the list of participants who have shared their profiles. It indicates for each participant their last name, first name, the sent date, the Pix score, the evolution, the certifiability status and the number of certifiable competences.", "column": { + "ariaSharedProfileCount": "Number of profiles shared", "certifiable": "Eligible for certification", "competences-certifiables": "Competences eligible for certification", "evolution": "Evolution", @@ -1282,7 +1283,8 @@ "sending-date": { "label": "Sent on", "on-hold": "Pending" - } + }, + "sharedProfileCount": "No. profiles shared" }, "empty": "No profiles yet", "evolution": { diff --git a/orga/translations/fr.json b/orga/translations/fr.json index 1595010f54d..b0adad8c269 100644 --- a/orga/translations/fr.json +++ b/orga/translations/fr.json @@ -1276,6 +1276,7 @@ "title": "Liste des profils reçus", "caption": "Ce tableau comporte la liste des participants ayant partagé leurs profils. Il indique pour chaque participant leur nom, prénom, la date d’envoi, le score Pix, l'évolution, le statut de certificabilité et le nombre de compétences certifiables.", "column": { + "ariaSharedProfileCount": "Nombre de partages de profil", "certifiable": "Certifiable", "competences-certifiables": "Comp. certifiables", "evolution": "Évolution", @@ -1288,7 +1289,8 @@ "sending-date": { "label": "Date d'envoi", "on-hold": "En attente d'envoi" - } + }, + "sharedProfileCount": "Nb de partages de profil" }, "empty": "En attente de profils", "evolution": { diff --git a/orga/translations/nl.json b/orga/translations/nl.json index 24db773fc5d..463c364be1b 100644 --- a/orga/translations/nl.json +++ b/orga/translations/nl.json @@ -1268,6 +1268,7 @@ "table": { "caption": "Deze tabel bevat de lijst met deelnemers die hun profiel hebben gedeeld. Het geeft voor elke deelnemer zijn achternaam, voornaam, de verzonden datum, de Pix-score, de evolutie, de certificeerbaarheidsstatus en het aantal certificeerbare competenties.", "column": { + "ariaSharedProfileCount": "Aantal profielaandelen", "certifiable": "Certificeerbaar", "competences-certifiables": "Certificeerbare vaard.", "evolution": "Evolutie", @@ -1277,6 +1278,7 @@ "label": "Pix-score", "value": "{score, number}" }, + "sharedProfileCount": "Aantal profielaandelen", "sending-date": { "label": "Verzenddatum", "on-hold": "In afwachting van verzending" From 2d903612b22e20690919341ca6e55b1f6f141ae1 Mon Sep 17 00:00:00 2001 From: ThomasBazin Date: Wed, 13 Nov 2024 16:34:36 +0100 Subject: [PATCH 5/6] feat(orga): display new table column for shared profiles count --- .../campaign/results/profile-list.gjs | 10 ++++ ...ofiles-collection-participation-summary.js | 1 + .../campaign/results/profile-list-test.js | 46 ++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/orga/app/components/campaign/results/profile-list.gjs b/orga/app/components/campaign/results/profile-list.gjs index 16c5a86779b..1c90e4902d0 100644 --- a/orga/app/components/campaign/results/profile-list.gjs +++ b/orga/app/components/campaign/results/profile-list.gjs @@ -44,6 +44,7 @@ import ParticipationEvolutionIcon from './participation-evolution-icon'; {{/if}} + @@ -67,6 +68,12 @@ import ParticipationEvolutionIcon from './participation-evolution-icon'; {{t "pages.profiles-list.table.column.competences-certifiables" }} + + {{#if @campaign.multipleSendings}} + + {{t "pages.profiles-list.table.column.sharedProfileCount"}} + + {{/if}} @@ -116,6 +123,9 @@ import ParticipationEvolutionIcon from './participation-evolution-icon'; {{profile.certifiableCompetencesCount}} + + {{profile.sharedProfileCount}} + {{/each}} diff --git a/orga/app/models/campaign-profiles-collection-participation-summary.js b/orga/app/models/campaign-profiles-collection-participation-summary.js index 2d90e4d86f8..d23c74d81a2 100644 --- a/orga/app/models/campaign-profiles-collection-participation-summary.js +++ b/orga/app/models/campaign-profiles-collection-participation-summary.js @@ -6,6 +6,7 @@ export default class CampaignProfilesCollectionParticipationSummary extends Mode @attr('string') participantExternalId; @attr('date') sharedAt; @attr('number') pixScore; + @attr('number') sharedProfileCount; @attr('nullable-string') evolution; @attr('boolean') certifiable; @attr('number') certifiableCompetencesCount; diff --git a/orga/tests/integration/components/campaign/results/profile-list-test.js b/orga/tests/integration/components/campaign/results/profile-list-test.js index 0b4f5775cc5..f21bb7364c9 100644 --- a/orga/tests/integration/components/campaign/results/profile-list-test.js +++ b/orga/tests/integration/components/campaign/results/profile-list-test.js @@ -46,7 +46,7 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo }); }); module('table headers for multiple sendings campaign', function () { - test('it should display evolution header and tooltip when campaign is multiple sendings', async function (assert) { + test('it should display evolution header and tooltip and shared profile count when campaign is multiple sendings', async function (assert) { // given this.campaign = store.createRecord('campaign', { id: '1', @@ -74,9 +74,12 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo name: t('pages.profiles-list.table.column.evolution'), }); assert.ok(within(evolutionHeader).getByText(t('pages.profiles-list.table.evolution-tooltip.content'))); + assert.ok( + screen.getByRole('columnheader', { name: t('pages.profiles-list.table.column.ariaSharedProfileCount') }), + ); }); - test('it should not display evolution header if campaign is not multiple sendings', async function (assert) { + test('it should not display evolution header or shared profile count if campaign is not multiple sendings', async function (assert) { // given this.campaign = store.createRecord('campaign', { id: '1', @@ -101,6 +104,9 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo // then assert.notOk(screen.queryByRole('columnheader', { name: t('pages.profiles-list.table.column.evolution') })); + assert.notOk( + screen.queryByRole('columnheader', { name: t('pages.profiles-list.table.column.ariaSharedProfileCount') }), + ); }); }); @@ -209,6 +215,42 @@ module('Integration | Component | Campaign::Results::ProfileList', function (hoo assert.ok(screen.getByRole('cell', { name: t('pages.profiles-list.table.evolution.unavailable') })); }); + test('it should display number of profiles shares', async function (assert) { + // given + this.campaign = store.createRecord('campaign', { + id: '1', + name: 'campagne 1', + participationsCount: 1, + multipleSendings: true, + }); + this.profiles = [ + { + firstName: 'John', + lastName: 'Doe', + participantExternalId: '123', + sharedProfileCount: 3, + evolution: 'decrease', + sharedAt: new Date(2020, 1, 1), + }, + ]; + this.profiles.meta = { rowCount: 1 }; + + // when + const screen = await render( + hbs``, + ); + + // then + assert.ok(screen.getByRole('cell', { name: '3' })); + }); + test('it should display the profile list with external id', async function (assert) { // given this.campaign = store.createRecord('campaign', { From e0bae0bcfd3648d702cfe5fe8482869a8fb936de Mon Sep 17 00:00:00 2001 From: ThomasBazin Date: Wed, 13 Nov 2024 20:09:35 +0100 Subject: [PATCH 6/6] chore?(api): refacto repository to use a with clause --- ...ection-participation-summary-repository.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js b/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js index ee235516906..f8c23bda312 100644 --- a/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js +++ b/api/src/prescription/campaign/infrastructure/repositories/campaign-profiles-collection-participation-summary-repository.js @@ -56,6 +56,15 @@ async function _getParticipations(qb, campaignId, filters) { .whereNull('campaign-participations.deletedAt') .orderBy('sharedAt', 'desc'); }) + .with('participationsCount', (qb) => { + qb.select('organizationLearnerId') + .count('organizationLearnerId AS sharedProfileCount') + .from('campaign-participations') + .groupBy('organizationLearnerId') + .where('campaignId', campaignId) + .whereNotNull('campaign-participations.sharedAt') + .whereNull('campaign-participations.deletedAt'); + }) .select( 'campaign-participations.id AS campaignParticipationId', 'campaign-participations.userId AS userId', @@ -66,13 +75,7 @@ async function _getParticipations(qb, campaignId, filters) { 'campaign-participations.pixScore AS pixScore', 'previousParticipationsInfos.previousPixScore', 'previousParticipationsInfos.previousSharedAt', - knex('campaign-participations') - .as('sharedProfileCount') - .count() - .whereRaw('"campaign-participations"."organizationLearnerId" = "view-active-organization-learners".id') - .whereNotNull('campaign-participations.sharedAt') - .whereNull('campaign-participations.deletedAt') - .where('campaign-participations.campaignId', campaignId), + 'participationsCount.sharedProfileCount', ) .distinctOn('campaign-participations.organizationLearnerId') .from('campaign-participations') @@ -87,6 +90,11 @@ async function _getParticipations(qb, campaignId, filters) { 'campaign-participations.organizationLearnerId', ).andOn('campaign-participations.id', '!=', 'previousParticipationsInfos.id'); }) + .join( + 'participationsCount', + 'participationsCount.organizationLearnerId', + 'campaign-participations.organizationLearnerId', + ) .where('campaign-participations.campaignId', campaignId) .whereNull('campaign-participations.deletedAt') .whereNotNull('campaign-participations.sharedAt')