From d619fa30bfe14f96036f991f66fdbaf79a9d7a8e Mon Sep 17 00:00:00 2001 From: LionelB Date: Mon, 24 Feb 2025 18:52:03 +0100 Subject: [PATCH 1/7] feat(api): add DetachAssessmentModel --- .../domain/read-models/DetachedAssessment.js | 9 +++++++++ .../read-models/DetachedAssessment_test.js | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 api/src/prescription/campaign-participation/domain/read-models/DetachedAssessment.js create mode 100644 api/tests/prescription/campaign-participation/unit/domain/read-models/DetachedAssessment_test.js diff --git a/api/src/prescription/campaign-participation/domain/read-models/DetachedAssessment.js b/api/src/prescription/campaign-participation/domain/read-models/DetachedAssessment.js new file mode 100644 index 00000000000..8d4e5f5b87f --- /dev/null +++ b/api/src/prescription/campaign-participation/domain/read-models/DetachedAssessment.js @@ -0,0 +1,9 @@ +class DetachedAssessment { + constructor({ id, createdAt, state }) { + this.id = id; + this.createdAt = createdAt; + this.state = state; + } +} + +export { DetachedAssessment }; diff --git a/api/tests/prescription/campaign-participation/unit/domain/read-models/DetachedAssessment_test.js b/api/tests/prescription/campaign-participation/unit/domain/read-models/DetachedAssessment_test.js new file mode 100644 index 00000000000..17efa0653f8 --- /dev/null +++ b/api/tests/prescription/campaign-participation/unit/domain/read-models/DetachedAssessment_test.js @@ -0,0 +1,20 @@ +import { DetachedAssessment } from '../../../../../../src/prescription/campaign-participation/domain/read-models/DetachedAssessment.js'; +import { Assessment } from '../../../../../../src/shared/domain/models/Assessment.js'; +import { expect } from '../../../../../test-helper.js'; + +describe('Unit | Domain | Read-Models | DetachAssessment', function () { + describe('constructor', function () { + it('should correctly initialize the information about Detach assessment', function () { + const createdAt = new Date('2012-01-01'); + const assessment = new DetachedAssessment({ + id: 1, + createdAt, + state: Assessment.states.COMPLETED, + }); + + expect(assessment.id).equal(1); + expect(assessment.createdAt).equal(createdAt); + expect(assessment.state).equal(Assessment.states.COMPLETED); + }); + }); +}); From d2de797182cc8a888ee93c4ef5255c28787a1052 Mon Sep 17 00:00:00 2001 From: LionelB Date: Mon, 24 Feb 2025 19:08:35 +0100 Subject: [PATCH 2/7] feat(api): add AnonymisedCampaignAssessment serializer --- ...onymised-campaign-assessment-serializer.js | 12 +++++++ ...sed-campaign-assessment-serializer_test.js | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 api/src/prescription/campaign-participation/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer.js create mode 100644 api/tests/prescription/campaign-participation/unit/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer_test.js diff --git a/api/src/prescription/campaign-participation/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer.js b/api/src/prescription/campaign-participation/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer.js new file mode 100644 index 00000000000..ccbbd0a68d0 --- /dev/null +++ b/api/src/prescription/campaign-participation/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer.js @@ -0,0 +1,12 @@ +import jsonapiSerializer from 'jsonapi-serializer'; + +const { Serializer } = jsonapiSerializer; + +const serialize = function (anonymisedCampaignAssessment, meta) { + return new Serializer('anonymised-campaign-assessment', { + attributes: ['state', 'createdAt'], + meta, + }).serialize(anonymisedCampaignAssessment); +}; + +export { serialize }; diff --git a/api/tests/prescription/campaign-participation/unit/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer_test.js b/api/tests/prescription/campaign-participation/unit/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer_test.js new file mode 100644 index 00000000000..8ee1b4a58b1 --- /dev/null +++ b/api/tests/prescription/campaign-participation/unit/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer_test.js @@ -0,0 +1,34 @@ +import * as serializer from '../../../../../../../src/prescription/campaign-participation/infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer.js'; +import { Assessment } from '../../../../../../../src/shared/domain/models/Assessment.js'; +import { domainBuilder, expect } from '../../../../../../test-helper.js'; + +describe('Unit | Serializer | JSONAPI | anonymised-campaign-assessment-serializer', function () { + describe('#serialize()', function () { + it('should convert a DetachAssessment model object into JSON API data', function () { + // given + const assessment = domainBuilder.buildAssessment({ + id: 123, + createdAt: new Date('2020-10-10'), + state: Assessment.states.STARTED, + type: Assessment.types.CAMPAIGN, + }); + + // when + const json = serializer.serialize([assessment]); + + // then + expect(json).to.deep.equal({ + data: [ + { + type: 'anonymised-campaign-assessment', + id: assessment.id.toString(), + attributes: { + 'created-at': assessment.createdAt, + state: assessment.state, + }, + }, + ], + }); + }); + }); +}); From e9fc183e114704c4d0fcd9712111d64aa518cc98 Mon Sep 17 00:00:00 2001 From: LionelB Date: Mon, 24 Feb 2025 19:09:30 +0100 Subject: [PATCH 3/7] feat(api): add findUserAnonymisedCampaignAssessment usecase --- ...nd-user-anonymised-campaign-assessments.js | 4 ++ ...er-anonymised-campaign-assessments_test.js | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 api/src/prescription/campaign-participation/domain/usecases/find-user-anonymised-campaign-assessments.js create mode 100644 api/tests/prescription/campaign-participation/integration/domain/usecases/find-user-anonymised-campaign-assessments_test.js diff --git a/api/src/prescription/campaign-participation/domain/usecases/find-user-anonymised-campaign-assessments.js b/api/src/prescription/campaign-participation/domain/usecases/find-user-anonymised-campaign-assessments.js new file mode 100644 index 00000000000..39d0387d9db --- /dev/null +++ b/api/src/prescription/campaign-participation/domain/usecases/find-user-anonymised-campaign-assessments.js @@ -0,0 +1,4 @@ +const findUserAnonymisedCampaignAssessments = async function ({ userId, campaignAssessmentParticipationRepository }) { + return await campaignAssessmentParticipationRepository.getDetachedByUserId({ userId }); +}; +export { findUserAnonymisedCampaignAssessments }; diff --git a/api/tests/prescription/campaign-participation/integration/domain/usecases/find-user-anonymised-campaign-assessments_test.js b/api/tests/prescription/campaign-participation/integration/domain/usecases/find-user-anonymised-campaign-assessments_test.js new file mode 100644 index 00000000000..3d550916bb6 --- /dev/null +++ b/api/tests/prescription/campaign-participation/integration/domain/usecases/find-user-anonymised-campaign-assessments_test.js @@ -0,0 +1,69 @@ +import { DetachedAssessment } from '../../../../../../src/prescription/campaign-participation/domain/read-models/DetachedAssessment.js'; +import { usecases } from '../../../../../../src/prescription/campaign-participation/domain/usecases/index.js'; +import { Assessment } from '../../../../../../src/shared/domain/models/Assessment.js'; +import { databaseBuilder, expect } from '../../../../../test-helper.js'; + +describe('Integration | UseCase | find-user-anonymised-campaign-assessments', function () { + let user, organization, campaign, learner; + + beforeEach(async function () { + user = databaseBuilder.factory.buildUser(); + organization = databaseBuilder.factory.buildOrganization(); + campaign = databaseBuilder.factory.buildCampaign({ organizationId: organization.id }); + learner = databaseBuilder.factory.buildOrganizationLearner({ userId: user.id, organization: organization.id }); + await databaseBuilder.commit(); + }); + + context('when there is no anonymised participation', function () { + it('should return an no result', async function () { + // given + const participation = databaseBuilder.factory.buildCampaignParticipation({ + userId: user.id, + campaign: campaign.id, + organizationLearnerId: learner.id, + }); + databaseBuilder.factory.buildAssessment({ + type: Assessment.types.CAMPAIGN, + userId: user.id, + campaignParticipationId: participation.id, + }); + await databaseBuilder.commit(); + + // when + const result = await usecases.findUserAnonymisedCampaignAssessments({ userId: user.id }); + + // then + expect(result).to.be.empty; + }); + }); + context('when there is anonymised participation', function () { + it('should return results', async function () { + ///given + const assessment1 = databaseBuilder.factory.buildAssessment({ + type: Assessment.types.CAMPAIGN, + userId: user.id, + campaignParticipationId: null, + createdAt: new Date('2024-03-15'), + }); + const assessment2 = databaseBuilder.factory.buildAssessment({ + type: Assessment.types.CAMPAIGN, + userId: user.id, + state: Assessment.states.STARTED, + campaignParticipationId: null, + createdAt: new Date('2024-03-26'), + }); + await databaseBuilder.commit(); + + // when + const result = await usecases.findUserAnonymisedCampaignAssessments({ userId: user.id }); + + // then + expect(result).lengthOf(2); + expect(result[0]).instanceOf(DetachedAssessment); + expect(result).deep.members([ + { id: assessment1.id, createdAt: assessment1.createdAt, state: assessment1.state }, + { id: assessment2.id, createdAt: assessment2.createdAt, state: assessment2.state }, + ]); + }); + }); +}); From 47d0d679e8f7b6da54dd8c34ff2526f0ac8dc639 Mon Sep 17 00:00:00 2001 From: LionelB Date: Mon, 24 Feb 2025 19:10:21 +0100 Subject: [PATCH 4/7] feat(api): add getDetachedByUserId repository method --- ...ign-assessment-participation-repository.js | 12 +++++- ...ssessment-participation-repository_test.js | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-assessment-participation-repository.js b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-assessment-participation-repository.js index f3732242d3b..78169fdfe61 100644 --- a/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-assessment-participation-repository.js +++ b/api/src/prescription/campaign-participation/infrastructure/repositories/campaign-assessment-participation-repository.js @@ -6,6 +6,7 @@ import { Assessment } from '../../../../shared/domain/models/Assessment.js'; import * as knowledgeElementRepository from '../../../../shared/infrastructure/repositories/knowledge-element-repository.js'; import * as campaignRepository from '../../../campaign/infrastructure/repositories/campaign-repository.js'; import { CampaignAssessmentParticipation } from '../../domain/models/CampaignAssessmentParticipation.js'; +import { DetachedAssessment } from '../../domain/read-models/DetachedAssessment.js'; const getByCampaignIdAndCampaignParticipationId = async function ({ campaignId, campaignParticipationId }) { const result = await _fetchCampaignAssessmentAttributesFromCampaignParticipation(campaignId, campaignParticipationId); @@ -13,7 +14,16 @@ const getByCampaignIdAndCampaignParticipationId = async function ({ campaignId, return _buildCampaignAssessmentParticipation(result); }; -export { getByCampaignIdAndCampaignParticipationId }; +const getDetachedByUserId = async ({ userId }) => { + const result = await knex('assessments') + .select(['id', 'state', 'createdAt']) + .whereNull('campaignParticipationId') + .where({ userId, type: Assessment.types.CAMPAIGN }); + + return result.map((row) => new DetachedAssessment(row)); +}; + +export { getByCampaignIdAndCampaignParticipationId, getDetachedByUserId }; async function _fetchCampaignAssessmentAttributesFromCampaignParticipation(campaignId, campaignParticipationId) { const [campaignAssessmentParticipation] = await knex diff --git a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-assessment-participation-repository_test.js b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-assessment-participation-repository_test.js index 40e8ecf548c..f4e0d667350 100644 --- a/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-assessment-participation-repository_test.js +++ b/api/tests/prescription/campaign-participation/integration/infrastructure/repositories/campaign-assessment-participation-repository_test.js @@ -1,4 +1,5 @@ import { CampaignAssessmentParticipation } from '../../../../../../src/prescription/campaign-participation/domain/models/CampaignAssessmentParticipation.js'; +import { DetachedAssessment } from '../../../../../../src/prescription/campaign-participation/domain/read-models/DetachedAssessment.js'; import * as campaignAssessmentParticipationRepository from '../../../../../../src/prescription/campaign-participation/infrastructure/repositories/campaign-assessment-participation-repository.js'; import { CampaignParticipationStatuses } from '../../../../../../src/prescription/shared/domain/constants.js'; import { NotFoundError } from '../../../../../../src/shared/domain/errors.js'; @@ -345,4 +346,45 @@ describe('Integration | Repository | Campaign Assessment Participation', functio }); }); }); + + describe('#getDetachedByUserId', function () { + let userId, assessment; + + beforeEach(async function () { + const organizationId = databaseBuilder.factory.buildOrganization().id; + const campaignId = databaseBuilder.factory.buildCampaign({ organizationId }).id; + userId = databaseBuilder.factory.buildUser().id; + const organizationLearnerId = databaseBuilder.factory.buildOrganizationLearner({ userId, organizationId }).id; + databaseBuilder.factory.buildCampaignParticipation({ + campaignId, + userId, + organizationLearnerId, + }); + assessment = databaseBuilder.factory.buildAssessment({ + type: Assessment.types.CAMPAIGN, + userId, + campaignParticipationId: null, + }); + await databaseBuilder.commit(); + }); + context('when userId is unknown', function () { + it('should return an empty array', async function () { + const user = databaseBuilder.factory.buildUser(); + await databaseBuilder.commit(); + + const result = await campaignAssessmentParticipationRepository.getDetachedByUserId({ userId: user.id }); + expect(result).to.be.empty; + }); + }); + context('when user has anonymised participation', function () { + it('should return an array of DetachAssessment', async function () { + const result = await campaignAssessmentParticipationRepository.getDetachedByUserId({ userId }); + expect(result).lengthOf(1); + expect(result[0]).instanceOf(DetachedAssessment); + expect(result[0].id).equal(assessment.id); + expect(result[0].createdAt).deep.equal(assessment.createdAt); + expect(result[0].state).equal(assessment.state); + }); + }); + }); }); From 97ddfb71ddd7af626d9cfcd453009eb738decb02 Mon Sep 17 00:00:00 2001 From: LionelB Date: Mon, 24 Feb 2025 19:11:08 +0100 Subject: [PATCH 5/7] feat(api): add getAnonymisedCampaignAssessments controller --- .../campaign-participation-controller.js | 19 +++++++- .../campaign-participation-controller_test.js | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/api/src/prescription/campaign-participation/application/campaign-participation-controller.js b/api/src/prescription/campaign-participation/application/campaign-participation-controller.js index 891caf1b1ca..49f98da5471 100644 --- a/api/src/prescription/campaign-participation/application/campaign-participation-controller.js +++ b/api/src/prescription/campaign-participation/application/campaign-participation-controller.js @@ -1,6 +1,7 @@ import { DomainTransaction } from '../../../shared/domain/DomainTransaction.js'; import { extractLocaleFromRequest } from '../../../shared/infrastructure/utils/request-response-utils.js'; import { usecases } from '../domain/usecases/index.js'; +import * as anonymisedCampaignAssessmentSerializer from '../infrastructure/serializers/jsonapi/anonymised-campaign-assessment-serializer.js'; import * as availableCampaignParticipationsSerializer from '../infrastructure/serializers/jsonapi/available-campaign-participation-serializer.js'; import * as campaignAnalysisSerializer from '../infrastructure/serializers/jsonapi/campaign-analysis-serializer.js'; import * as campaignAssessmentParticipationResultSerializer from '../infrastructure/serializers/jsonapi/campaign-assessment-participation-result-serializer.js'; @@ -10,7 +11,6 @@ import * as campaignParticipationSerializer from '../infrastructure/serializers/ import * as campaignProfileSerializer from '../infrastructure/serializers/jsonapi/campaign-profile-serializer.js'; import * as participantResultSerializer from '../infrastructure/serializers/jsonapi/participant-result-serializer.js'; import * as participationForCampaignManagementSerializer from '../infrastructure/serializers/jsonapi/participation-for-campaign-management-serializer.js'; - const getUserCampaignParticipationToCampaign = function ( request, h, @@ -153,6 +153,22 @@ const getCampaignParticipationOverviews = async function ( ); }; +const getAnonymisedCampaignAssessments = async function ( + request, + h, + dependencies = { + anonymisedCampaignAssessmentSerializer, + }, +) { + const authenticatedUserId = request.auth.credentials.userId; + + const assessments = await usecases.findUserAnonymisedCampaignAssessments({ + userId: authenticatedUserId, + }); + + return dependencies.anonymisedCampaignAssessmentSerializer.serialize(assessments); +}; + const getUserCampaignAssessmentResult = async function ( request, _, @@ -179,6 +195,7 @@ const campaignParticipationController = { deleteParticipation, findPaginatedParticipationsForCampaignManagement, getAnalysis, + getAnonymisedCampaignAssessments, getCampaignAssessmentParticipation, getCampaignAssessmentParticipationResult, getCampaignParticipationOverviews, diff --git a/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js b/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js index 49a73aa262c..be99ad243a4 100644 --- a/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js +++ b/api/tests/prescription/campaign-participation/unit/application/campaign-participation-controller_test.js @@ -6,6 +6,49 @@ import { expect, hFake, sinon } from '../../../../test-helper.js'; const { FRENCH_SPOKEN } = LOCALE; describe('Unit | Application | Controller | Campaign-Participation', function () { + describe('#getAnonymisedCampaignAssessments', function () { + const userId = '1'; + let dependencies; + + beforeEach(function () { + const anonymisedCampaignAssessmentSerializer = { + serialize: sinon.stub(), + }; + sinon.stub(usecases, 'findUserAnonymisedCampaignAssessments'); + dependencies = { + anonymisedCampaignAssessmentSerializer, + }; + }); + + it('should return serialized anonymised campaign assessments', async function () { + // given + const request = { + auth: { + credentials: { + userId: userId, + }, + }, + params: { + id: userId, + }, + }; + const serializeSymbol = Symbol('serialize'); + usecases.findUserAnonymisedCampaignAssessments.withArgs({ userId }).resolves([]); + dependencies.anonymisedCampaignAssessmentSerializer.serialize.withArgs([]).returns(serializeSymbol); + + // when + const response = await campaignParticipationController.getAnonymisedCampaignAssessments( + request, + hFake, + dependencies, + ); + + // then + expect(response).to.equal(serializeSymbol); + expect(dependencies.anonymisedCampaignAssessmentSerializer.serialize).to.have.been.calledOnce; + }); + }); + describe('#getCampaignParticipationOverviews', function () { const userId = '1'; let dependencies; From bb1c066f6d6d530ba347cc07cd235455b67cf977 Mon Sep 17 00:00:00 2001 From: LionelB Date: Mon, 24 Feb 2025 19:11:30 +0100 Subject: [PATCH 6/7] feat(api): add route --- ...campaign-participation-overview-factory.js | 37 +++++++++++++++- .../campaign-participation-route.js | 26 +++++++++++ .../campaign-participation-route_test.js | 43 +++++++++++++++++++ .../campaign-participation-route_test.js | 33 ++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) diff --git a/api/db/database-builder/factory/campaign-participation-overview-factory.js b/api/db/database-builder/factory/campaign-participation-overview-factory.js index 362394d7cfa..b22cfba0fda 100644 --- a/api/db/database-builder/factory/campaign-participation-overview-factory.js +++ b/api/db/database-builder/factory/campaign-participation-overview-factory.js @@ -4,6 +4,7 @@ import { buildAssessment } from './build-assessment.js'; import { buildCampaign } from './build-campaign.js'; import { buildCampaignParticipation } from './build-campaign-participation.js'; import { buildCampaignSkill } from './build-campaign-skill.js'; +import { buildOrganizationLearner } from './build-organization-learner.js'; import { buildUser } from './build-user.js'; const { STARTED, SHARED, TO_SHARE } = CampaignParticipationStatuses; @@ -166,4 +167,38 @@ const buildDeleted = function ({ return campaignParticipation; }; -export { build, buildArchived, buildDeleted, buildEnded, buildOnGoing, buildToShare }; +const buildDeletedAndAnonymised = function ({ + userId, + createdAt, + sharedAt, + assessmentCreatedAt, + deletedAt = new Date('1998-07-01'), + deletedBy = buildUser().id, + campaignSkills, +} = {}) { + const campaign = buildCampaign(); + campaignSkills.forEach((skill) => buildCampaignSkill({ campaignId: campaign.id, skillId: skill })); + const learner = buildOrganizationLearner({ userId, campaignId: campaign.id }); + + const campaignParticipation = buildCampaignParticipation({ + organizationLearnerId: learner.id, + campaignId: campaign.id, + createdAt: createdAt, + sharedAt: sharedAt || createdAt, + deletedAt, + deletedBy, + status: SHARED, + }); + + const assessment = buildAssessment({ + userId, + campaignParticipationId: null, + createdAt: assessmentCreatedAt, + type: Assessment.types.CAMPAIGN, + state: Assessment.states.COMPLETED, + }); + + return { assessment, campaignParticipation }; +}; + +export { build, buildArchived, buildDeleted, buildDeletedAndAnonymised, buildEnded, buildOnGoing, buildToShare }; diff --git a/api/src/prescription/campaign-participation/application/campaign-participation-route.js b/api/src/prescription/campaign-participation/application/campaign-participation-route.js index c4797cb25e8..4f335cf2581 100644 --- a/api/src/prescription/campaign-participation/application/campaign-participation-route.js +++ b/api/src/prescription/campaign-participation/application/campaign-participation-route.js @@ -258,6 +258,32 @@ const register = async function (server) { tags: ['api'], }, }, + { + method: 'GET', + path: '/api/users/{userId}/anonymised-campaign-assessments', + config: { + pre: [ + { + method: securityPreHandlers.checkRequestedUserIsAuthenticatedUser, + assign: 'requestedUserIsAuthenticatedUser', + }, + ], + validate: { + params: Joi.object({ + userId: identifiersType.userId, + }), + }, + handler: campaignParticipationController.getAnonymisedCampaignAssessments, + notes: [ + '- **Cette route est restreinte aux utilisateurs authentifiés**\n' + + '- Récupération des assessment du type campagnes qui ne sont plus liés à des participations\n' + + '- L’id demandé doit correspondre à celui de l’utilisateur authentifié' + + '- Les assessments sont triés par ordre inverse de création' + + ' (les plus récentes en premier)', + ], + tags: ['api'], + }, + }, { method: 'GET', path: '/api/users/{userId}/campaigns/{campaignId}/campaign-participations', diff --git a/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-route_test.js b/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-route_test.js index 26ce114e8f4..42c501a2bdd 100644 --- a/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-route_test.js +++ b/api/tests/prescription/campaign-participation/acceptance/application/campaign-participation-route_test.js @@ -858,6 +858,49 @@ describe('Acceptance | API | Campaign Participations', function () { }); }); + describe('GET /users/{userId}/anonymised-campaign-assessments', function () { + let userId; + let options; + const skillId = 'recSkillId'; + + beforeEach(function () { + const user = databaseBuilder.factory.buildUser(); + userId = user.id; + + databaseBuilder.factory.learningContent.buildSkill({ id: skillId }); + + return databaseBuilder.commit(); + }); + + it('should return anonymised campaign assessment', async function () { + // given + const deletedCampaignParticipation = + databaseBuilder.factory.campaignParticipationOverviewFactory.buildDeletedAndAnonymised({ + userId, + createdAt: new Date('2021-01-12'), + sharedAt: new Date('2021-01-14'), + deleted: new Date('2023-12-24'), + assessmentCreatedAt: new Date('2021-01-12'), + campaignSkills: [skillId], + }); + + await databaseBuilder.commit(); + options = { + method: 'GET', + url: `/api/users/${userId}/anonymised-campaign-assessments`, + headers: generateAuthenticatedUserRequestHeaders({ userId }), + }; + + // when + const response = await server.inject(options); + + // then + expect(response.statusCode).to.equal(200); + const assessmentIds = response.result.data.map(({ id }) => Number(id)); + expect(assessmentIds).to.deep.equals([deletedCampaignParticipation.assessment.id]); + }); + }); + describe('GET /users/{userId}/campaigns/{campaignId}/campaign-participations', function () { let options; diff --git a/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js b/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js index 0b9df53fad7..5326f0b0dd9 100644 --- a/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js +++ b/api/tests/prescription/campaign-participation/integration/application/campaign-participation-route_test.js @@ -21,6 +21,10 @@ describe('Integration | Application | Route | campaignParticipationRouter', func .stub(campaignParticipationController, 'getUserCampaignAssessmentResult') .callsFake((request, h) => h.response('ok').code(200)); + sinon + .stub(campaignParticipationController, 'getAnonymisedCampaignAssessments') + .callsFake((request, h) => h.response('ok').code(200)); + httpTestServer = new HttpTestServer(); await httpTestServer.register(moduleUnderTest); }); @@ -204,4 +208,33 @@ describe('Integration | Application | Route | campaignParticipationRouter', func }); }); }); + + describe('GET /users/{userId}/anonymised-campaign-assessments', function () { + context('When authenticated user mismatch requested user or user is not authenticated ', function () { + beforeEach(function () { + securityPreHandlers.checkRequestedUserIsAuthenticatedUser.callsFake((request, h) => { + return Promise.resolve(h.response().code(403).takeover()); + }); + }); + + it('should return a 403 HTTP response', async function () { + // when + const response = await httpTestServer.request('GET', '/api/users/1234/anonymised-campaign-assessments'); + + // then + expect(response.statusCode).to.equal(403); + expect(campaignParticipationController.getAnonymisedCampaignAssessments).not.called; + }); + }); + context('When userId is not an integer', function () { + it('should return 400 - Bad request when userId is not an integer', async function () { + // when + const response = await httpTestServer.request('GET', '/api/users/NOTANID/anonymised-campaign-assessments'); + + // then + expect(response.statusCode).to.equal(400); + expect(campaignParticipationController.getAnonymisedCampaignAssessments).not.called; + }); + }); + }); }); From 756d7b65482046853980c154b27b666741f461cf Mon Sep 17 00:00:00 2001 From: LionelB Date: Wed, 26 Feb 2025 18:19:15 +0100 Subject: [PATCH 7/7] feat(mon-pix): display deleted participation in user-tests page --- .../anonymised-campaign-assessment.js | 7 ++++ .../campaign-participation-overview/card.hbs | 2 ++ .../card/deleted.gjs | 35 +++++++++++++++++++ .../models/anonymised-campaign-assessment.js | 10 ++++++ .../app/routes/authenticated/user-tests.js | 11 ++++-- mon-pix/translations/fr.json | 9 +++++ 6 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 mon-pix/app/adapters/anonymised-campaign-assessment.js create mode 100644 mon-pix/app/components/campaign-participation-overview/card/deleted.gjs create mode 100644 mon-pix/app/models/anonymised-campaign-assessment.js diff --git a/mon-pix/app/adapters/anonymised-campaign-assessment.js b/mon-pix/app/adapters/anonymised-campaign-assessment.js new file mode 100644 index 00000000000..1906518b744 --- /dev/null +++ b/mon-pix/app/adapters/anonymised-campaign-assessment.js @@ -0,0 +1,7 @@ +import ApplicationAdapter from './application'; + +export default class AnonymisedCampaignAssessment extends ApplicationAdapter { + urlForFindAll(modelName, { adapterOptions }) { + return `${this.host}/${this.namespace}/users/${adapterOptions.userId}/anonymised-campaign-assessments`; + } +} diff --git a/mon-pix/app/components/campaign-participation-overview/card.hbs b/mon-pix/app/components/campaign-participation-overview/card.hbs index 2e410f8c73b..7edcde71bf3 100644 --- a/mon-pix/app/components/campaign-participation-overview/card.hbs +++ b/mon-pix/app/components/campaign-participation-overview/card.hbs @@ -6,4 +6,6 @@ {{else if (eq @model.cardStatus "DISABLED")}} +{{else if (eq @model.cardStatus "DELETED")}} + {{/if}} \ No newline at end of file diff --git a/mon-pix/app/components/campaign-participation-overview/card/deleted.gjs b/mon-pix/app/components/campaign-participation-overview/card/deleted.gjs new file mode 100644 index 00000000000..7bcf0e02cda --- /dev/null +++ b/mon-pix/app/components/campaign-participation-overview/card/deleted.gjs @@ -0,0 +1,35 @@ +import PixTag from '@1024pix/pix-ui/components/pix-tag'; +import dayjsFormat from 'ember-dayjs/helpers/dayjs-format'; +import { t } from 'ember-intl'; + +function getStatusWording(state) { + const statusKey = { + started: 'started-at', + completed: 'finished-at', + }; + return `pages.campaign-participation-overview.card.${statusKey[state]}`; +} + + diff --git a/mon-pix/app/models/anonymised-campaign-assessment.js b/mon-pix/app/models/anonymised-campaign-assessment.js new file mode 100644 index 00000000000..a6b979da405 --- /dev/null +++ b/mon-pix/app/models/anonymised-campaign-assessment.js @@ -0,0 +1,10 @@ +import Model, { attr } from '@ember-data/model'; + +export default class AnonymisedCampaignAssessment extends Model { + @attr('string') state; + @attr('date') createdAt; + + get cardStatus() { + return 'DELETED'; + } +} diff --git a/mon-pix/app/routes/authenticated/user-tests.js b/mon-pix/app/routes/authenticated/user-tests.js index 2b1ed214753..ea571486eda 100644 --- a/mon-pix/app/routes/authenticated/user-tests.js +++ b/mon-pix/app/routes/authenticated/user-tests.js @@ -8,7 +8,7 @@ export default class UserTestsRoute extends Route { @service store; @service router; - model() { + async model() { const user = this.currentUser.user; const maximumDisplayed = 100; const queryParams = { @@ -17,8 +17,15 @@ export default class UserTestsRoute extends Route { 'page[size]': maximumDisplayed, 'filter[states]': ['ONGOING', 'TO_SHARE', 'ENDED', 'DISABLED'], }; + const campaignParticipationOverviews = await this.store.query('campaign-participation-overview', queryParams); - return this.store.query('campaign-participation-overview', queryParams); + const anonymisedCampaignAssessments = await this.store.findAll('anonymised-campaign-assessment', { + adapterOptions: { userId: user.id }, + }); + + return campaignParticipationOverviews + .concat(anonymisedCampaignAssessments) + .sort(({ createdAt1 }, { createdAt2 }) => createdAt1 - createdAt2); } redirect(model) { diff --git a/mon-pix/translations/fr.json b/mon-pix/translations/fr.json index 0ef73db5acc..e824d24cfe6 100644 --- a/mon-pix/translations/fr.json +++ b/mon-pix/translations/fr.json @@ -573,6 +573,7 @@ "finished": "Terminé", "started": "En cours" }, + "text-deleted": "Parcours désactivé par votre organisation.'
'Vous ne pouvez plus agir sur ce parcours.", "text-disabled": "Parcours désactivé par votre organisation.'
'Vous ne pouvez plus envoyer vos résultats." }, "title": "Parcours" @@ -2118,6 +2119,14 @@ "title": "Mon compte" }, "user-tests": { + "anonymised-participations": { + "course": "Parcours", + "states": { + "completed": "terminé", + "started": "commencé" + }, + "title": "Parcours désactivés" + }, "description": "Retrouvez ici les parcours d’évaluation que vous avez commencés ou terminés.", "title": "Parcours" },