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..650a6d171c0 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',
diff --git a/api/tests/school/unit/application/school-controller_test.js b/api/tests/school/unit/application/school-controller_test.js
index a0c534c6905..214fa00e62a 100644
--- a/api/tests/school/unit/application/school-controller_test.js
+++ b/api/tests/school/unit/application/school-controller_test.js
@@ -53,6 +53,7 @@ describe('Unit | Controller | school-controller', function () {
organizationId: 1,
completedMissionIds: [],
startedMissionIds: [],
+ features: [],
},
],
},
diff --git a/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js b/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js
index 3593c163257..28d4d0d99e8 100644
--- a/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js
+++ b/api/tests/school/unit/infrastructure/serializers/organization-learner_test.js
@@ -25,6 +25,7 @@ describe('Unit | Serializer | JSONAPI | organization-learner', function () {
'organization-id': 'orga-1',
'completed-mission-ids': ['1'],
'started-mission-ids': ['2'],
+ features: [],
},
},
};
diff --git a/junior/app/components/bubble.gjs b/junior/app/components/bubble.gjs
index 73de8990b4d..69c72f805d7 100644
--- a/junior/app/components/bubble.gjs
+++ b/junior/app/components/bubble.gjs
@@ -1,5 +1,8 @@
import Component from '@glimmer/component';
import MarkdownToHtml from 'junior/components/markdown-to-html';
+import * as markdownConverter from 'junior/utils/markdown-converter';
+
+import OralizationButton from './oralization-button';
export default class Bubble extends Component {
get getClasses() {
@@ -9,5 +12,19 @@ export default class Bubble extends Component {
}
return className;
}
-
{{t "pages.missions.introduction-page.start-mission"}}