Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] affiche les participations anonymisées dans mes parcours (Pix-14458) #11507

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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,
_,
Expand All @@ -179,6 +195,7 @@ const campaignParticipationController = {
deleteParticipation,
findPaginatedParticipationsForCampaignManagement,
getAnalysis,
getAnonymisedCampaignAssessments,
getCampaignAssessmentParticipation,
getCampaignAssessmentParticipationResult,
getCampaignParticipationOverviews,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class DetachedAssessment {
constructor({ id, createdAt, state }) {
this.id = id;
this.createdAt = createdAt;
this.state = state;
}
}

export { DetachedAssessment };
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const findUserAnonymisedCampaignAssessments = async function ({ userId, campaignAssessmentParticipationRepository }) {
return await campaignAssessmentParticipationRepository.getDetachedByUserId({ userId });
};
export { findUserAnonymisedCampaignAssessments };
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@ 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);

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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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;
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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 },
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});
});
});
});
Loading