From 9e6ad2310f3669b1fb29b50d41766498706affb5 Mon Sep 17 00:00:00 2001 From: Aurelie Crouillebois Date: Wed, 6 Nov 2024 18:04:18 +0100 Subject: [PATCH] feat(api): get features of organization learner --- .../build-organization-learner-feature.js | 22 +++++++++++++++- ...-index-on-organization-learner-features.js | 25 +++++++++++++++++++ .../api/models/OrganizationLearner.js | 5 ++-- .../domain/read-models/OrganizationLearner.js | 2 ++ .../organization-learner-repository.js | 7 ++++++ .../domain/models/OrganizationLearner.js | 12 ++++++++- .../read-models/OrganizationLearnerDTO.js | 12 ++++++++- .../serializers/organization-learner.js | 10 +++++++- .../organization-learner-repository_test.js | 17 +++++++++++++ ...learner-with-completed-mission-ids_test.js | 24 ++++++++++++++++++ .../organization-learner-controller_test.js | 2 ++ 11 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 api/db/migrations/20241106164209_add-index-on-organization-learner-features.js diff --git a/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js b/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js index 5cd3ec3e54f..7d7b83eede1 100644 --- a/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js +++ b/api/db/database-builder/factory/prescription/organization-learners/build-organization-learner-feature.js @@ -22,4 +22,24 @@ const buildOrganizationLearnerFeature = function ({ }); }; -export { buildOrganizationLearnerFeature }; +const buildOrganizationLearnerFeatureWithFeatureKey = function ({ + id = databaseBuffer.getNextId(), + organizationLearnerId, + featureKey, +} = {}) { + organizationLearnerId = organizationLearnerId === undefined ? buildOrganizationLearner().id : organizationLearnerId; + const featureId = buildFeature({ key: featureKey }).id; + + const values = { + id, + organizationLearnerId, + featureId, + }; + + return databaseBuffer.pushInsertable({ + tableName: 'organization-learner-features', + values, + }); +}; + +export { buildOrganizationLearnerFeature, buildOrganizationLearnerFeatureWithFeatureKey }; diff --git a/api/db/migrations/20241106164209_add-index-on-organization-learner-features.js b/api/db/migrations/20241106164209_add-index-on-organization-learner-features.js new file mode 100644 index 00000000000..3dc64d0966e --- /dev/null +++ b/api/db/migrations/20241106164209_add-index-on-organization-learner-features.js @@ -0,0 +1,25 @@ +// Make sure you properly test your migration, especially DDL (Data Definition Language) +// ! If the target table is large, and the migration take more than 20 minutes, the deployment will fail ! + +// You can design and test your migration to avoid this by following this guide +// https://1024pix.atlassian.net/wiki/spaces/EDTDT/pages/3849323922/Cr+er+une+migration + +// If your migrations target `answers` or `knowledge-elements` +// contact @team-captains, because automatic migrations are not active on `pix-datawarehouse-production` +// this may prevent data replication to succeed the day after your migration is deployed on `pix-api-production` +const TABLE_NAME = 'organization-learner-features'; +const COLUMN_NAME = 'organizationLearnerId'; + +const up = async function (knex) { + await knex.schema.table(TABLE_NAME, function (table) { + table.index(COLUMN_NAME); + }); +}; + +const down = async function (knex) { + await knex.schema.table(TABLE_NAME, function (table) { + table.dropIndex(COLUMN_NAME); + }); +}; + +export { down, up }; diff --git a/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js b/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js index 86d4232f731..fdaec58e645 100644 --- a/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js +++ b/api/src/prescription/organization-learner/application/api/models/OrganizationLearner.js @@ -1,9 +1,10 @@ export class OrganizationLearner { - constructor({ id, firstName, lastName, organizationId, ...attributes }) { + constructor({ id, firstName, lastName, features, organizationId, ...attributes }) { this.id = id; this.firstName = firstName; this.lastName = lastName; - this.division = attributes['Libellé classe']; + this.features = features; this.organizationId = organizationId; + this.division = attributes['Libellé classe']; } } diff --git a/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js b/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js index f8fa5754731..d4a2a497d1b 100644 --- a/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js +++ b/api/src/prescription/organization-learner/domain/read-models/OrganizationLearner.js @@ -16,6 +16,7 @@ class OrganizationLearner { organizationId, certifiableAtFromLearner, userId, + features, } = {}) { this.id = id; this.firstName = firstName; @@ -27,6 +28,7 @@ class OrganizationLearner { this.authenticationMethods = authenticationMethods; this.organizationId = organizationId; this.userId = userId; + this.features = features; this._buildCertificability({ isCertifiableFromCampaign, diff --git a/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js b/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js index 7a841d15da2..6e3ceeaa0b9 100644 --- a/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js +++ b/api/src/prescription/organization-learner/infrastructure/repositories/organization-learner-repository.js @@ -47,6 +47,7 @@ async function get({ organizationLearnerId }) { 'subquery.isCertifiableFromCampaign', 'subquery.certifiableAtFromCampaign', knex.raw('array_remove(ARRAY_AGG("identityProvider"), NULL) AS "authenticationMethods"'), + knex.raw('array_remove(ARRAY_AGG(features.key), NULL) as features'), 'users.email', 'users.username', ) @@ -55,6 +56,12 @@ async function get({ organizationLearnerId }) { .leftJoin('subquery', 'subquery.organizationLearnerId', 'view-active-organization-learners.id') .leftJoin('authentication-methods', 'authentication-methods.userId', 'view-active-organization-learners.userId') .leftJoin('users', 'view-active-organization-learners.userId', 'users.id') + .leftJoin( + 'organization-learner-features', + 'view-active-organization-learners.id', + 'organization-learner-features.organizationLearnerId', + ) + .leftJoin('features', 'organization-learner-features.featureId', 'features.id') .groupBy( 'view-active-organization-learners.id', 'view-active-organization-learners.firstName', diff --git a/api/src/school/domain/models/OrganizationLearner.js b/api/src/school/domain/models/OrganizationLearner.js index 50192bd0009..87d9b536c3f 100644 --- a/api/src/school/domain/models/OrganizationLearner.js +++ b/api/src/school/domain/models/OrganizationLearner.js @@ -1,9 +1,19 @@ class OrganizationLearner { - constructor({ id, lastName, firstName, division, organizationId, completedMissionIds, startedMissionIds } = {}) { + constructor({ + id, + lastName, + firstName, + division, + features, + organizationId, + completedMissionIds, + startedMissionIds, + } = {}) { this.id = id; this.lastName = lastName; this.firstName = firstName; this.division = division; + this.features = features || []; this.organizationId = organizationId; this.completedMissionIds = completedMissionIds; this.startedMissionIds = startedMissionIds; diff --git a/api/src/school/domain/read-models/OrganizationLearnerDTO.js b/api/src/school/domain/read-models/OrganizationLearnerDTO.js index 9c49748574c..23a608d5c99 100644 --- a/api/src/school/domain/read-models/OrganizationLearnerDTO.js +++ b/api/src/school/domain/read-models/OrganizationLearnerDTO.js @@ -1,5 +1,14 @@ class OrganizationLearnerDTO { - constructor({ id, displayName, firstName, division, organizationId, startedMissionIds, completedMissionIds } = {}) { + constructor({ + id, + displayName, + firstName, + division, + organizationId, + startedMissionIds, + completedMissionIds, + features, + } = {}) { this.id = id; this.displayName = displayName; this.firstName = firstName; @@ -7,6 +16,7 @@ class OrganizationLearnerDTO { this.organizationId = organizationId; this.startedMissionIds = startedMissionIds; this.completedMissionIds = completedMissionIds; + this.features = features; } } diff --git a/api/src/school/infrastructure/serializers/organization-learner.js b/api/src/school/infrastructure/serializers/organization-learner.js index 81323cc93a6..f6696d11f94 100644 --- a/api/src/school/infrastructure/serializers/organization-learner.js +++ b/api/src/school/infrastructure/serializers/organization-learner.js @@ -2,7 +2,15 @@ import { Serializer } from 'jsonapi-serializer'; const serialize = function (organizationLearner) { return new Serializer('organizationLearner', { - attributes: ['firstName', 'displayName', 'division', 'organizationId', 'completedMissionIds', 'startedMissionIds'], + attributes: [ + 'firstName', + 'displayName', + 'division', + 'organizationId', + 'completedMissionIds', + 'startedMissionIds', + 'features', + ], transform: function (organizationLearner) { return { ...organizationLearner, diff --git a/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js b/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js index a3f5b31b72a..90c3c3de153 100644 --- a/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js +++ b/api/tests/prescription/organization-learner/integration/infrastructure/repositories/organization-learner-repository_test.js @@ -47,6 +47,23 @@ describe('Integration | Infrastructure | Repository | Organization Learner', fun expect(organizationLearner.email).to.equal('k.s@example.net'); expect(organizationLearner.username).to.equal('sassouk'); expect(organizationLearner.organizationId).to.equal(organizationId); + expect(organizationLearner.features).to.be.empty; + }); + + it("Should return organization learner's features", async function () { + const organizationLearnerId = databaseBuilder.factory.buildOrganizationLearner().id; + databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearnerFeatureWithFeatureKey({ + organizationLearnerId, + featureKey: 'ORALIZATION', + }); + databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearnerFeatureWithFeatureKey({ + organizationLearnerId, + featureKey: 'BLA', + }); + await databaseBuilder.commit(); + + const organizationLearner = await organizationLearnerRepository.get({ organizationLearnerId }); + expect(organizationLearner.features).to.deep.equal(['ORALIZATION', 'BLA']); }); it('Should return the organization learner with a given ID', async function () { diff --git a/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js b/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js index 53749359ac7..965f61ff922 100644 --- a/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js +++ b/api/tests/school/integration/domain/usecases/get-organization-learner-with-completed-mission-ids_test.js @@ -42,6 +42,30 @@ describe('Integration | Usecase | get-organization-learner-with-completed-missio ); }); + it('should return organization learner with features', async function () { + const organizationLearner = + databaseBuilder.factory.prescription.organizationLearners.buildOndeOrganizationLearner(); + databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearnerFeatureWithFeatureKey({ + organizationLearnerId: organizationLearner.id, + featureKey: 'ORALIZATION', + }); + await databaseBuilder.commit(); + + const result = await usecases.getOrganizationLearnerWithMissionIdsByState({ + organizationLearnerId: organizationLearner.id, + }); + + expect(result).to.deep.equal( + new OrganizationLearner({ + ...organizationLearner, + division: organizationLearner.attributes['Libellé classe'], + completedMissionIds: [], + startedMissionIds: [], + features: ['ORALIZATION'], + }), + ); + }); + it('should return only the good organization learner', async function () { const organizationLearner = databaseBuilder.factory.prescription.organizationLearners.buildOndeOrganizationLearner(); diff --git a/api/tests/school/unit/application/organization-learner-controller_test.js b/api/tests/school/unit/application/organization-learner-controller_test.js index f9f2f4e0075..ae033d77eed 100644 --- a/api/tests/school/unit/application/organization-learner-controller_test.js +++ b/api/tests/school/unit/application/organization-learner-controller_test.js @@ -15,6 +15,7 @@ describe('Unit | Controller | organization-learner-controller', function () { organizationId: '345', completedMissionIds: ['rec12344', 'rec435'], startedMissionIds: undefined, + features: ['ORALIZATION'], }), ); const id = 4356; @@ -33,6 +34,7 @@ describe('Unit | Controller | organization-learner-controller', function () { 'display-name': undefined, division: 'CM2', 'organization-id': '345', + features: ['ORALIZATION'], }, id: '4356', type: 'organizationLearners',