From 7837d284223f59d5adea9a62cb53d8484cda0240 Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 7 Nov 2024 17:07:35 +0100 Subject: [PATCH 1/5] feat(api): create attestations management feature with seed associated --- ...07142820_add-attestations-management-feature.js | 14 ++++++++++++++ api/db/seeds/data/common/constants.js | 2 ++ api/db/seeds/data/common/feature-builder.js | 6 ++++++ api/db/seeds/data/common/organization-builder.js | 2 ++ api/src/shared/domain/constants.js | 4 ++++ 5 files changed, 28 insertions(+) create mode 100644 api/db/migrations/20241107142820_add-attestations-management-feature.js diff --git a/api/db/migrations/20241107142820_add-attestations-management-feature.js b/api/db/migrations/20241107142820_add-attestations-management-feature.js new file mode 100644 index 00000000000..b0c9d186840 --- /dev/null +++ b/api/db/migrations/20241107142820_add-attestations-management-feature.js @@ -0,0 +1,14 @@ +import { ORGANIZATION_FEATURE } from '../../src/shared/domain/constants.js'; + +const up = async function (knex) { + await knex('features').insert({ + key: ORGANIZATION_FEATURE.ATTESTATIONS_MANAGEMENT.key, + description: ORGANIZATION_FEATURE.ATTESTATIONS_MANAGEMENT.description, + }); +}; + +const down = async function (knex) { + await knex('features').where({ key: ORGANIZATION_FEATURE.ATTESTATIONS_MANAGEMENT.key }).delete(); +}; + +export { down, up }; diff --git a/api/db/seeds/data/common/constants.js b/api/db/seeds/data/common/constants.js index 5e7a192c78b..253e130b177 100644 --- a/api/db/seeds/data/common/constants.js +++ b/api/db/seeds/data/common/constants.js @@ -22,6 +22,7 @@ const FEATURE_LEARNER_IMPORT_ID = COMMON_OFFSET_ID + 5; const FEATURE_CAN_REGISTER_FOR_A_COMPLEMENTARY_CERTIFICATION_ALONE_ID = COMMON_OFFSET_ID + 6; const FEATURE_CAMPAIGN_EXTERNAL_ID = COMMON_OFFSET_ID + 7; const FEATURE_ORALIZATION_ID = COMMON_OFFSET_ID + 8; +const FEATURE_ATTESTATIONS_MANAGEMENT_ID = COMMON_OFFSET_ID + 9; //ORGANIZATIONS const SCO_MANAGING_ORGANIZATION_ID = COMMON_OFFSET_ID; @@ -80,6 +81,7 @@ export { CFA_TAG, COLLEGE_TAG, DEFAULT_PASSWORD, + FEATURE_ATTESTATIONS_MANAGEMENT_ID, FEATURE_CAMPAIGN_EXTERNAL_ID, FEATURE_CAN_REGISTER_FOR_A_COMPLEMENTARY_CERTIFICATION_ALONE_ID, FEATURE_COMPUTE_ORGANIZATION_LEARNER_CERTIFICABILITY_ID, diff --git a/api/db/seeds/data/common/feature-builder.js b/api/db/seeds/data/common/feature-builder.js index 388fe6b2d9b..58b64e63908 100644 --- a/api/db/seeds/data/common/feature-builder.js +++ b/api/db/seeds/data/common/feature-builder.js @@ -1,6 +1,7 @@ import { CERTIFICATION_FEATURES } from '../../../../src/certification/shared/domain/constants.js'; import { CAMPAIGN_FEATURES, ORGANIZATION_FEATURE } from '../../../../src/shared/domain/constants.js'; import { + FEATURE_ATTESTATIONS_MANAGEMENT_ID, FEATURE_CAMPAIGN_EXTERNAL_ID, FEATURE_CAN_REGISTER_FOR_A_COMPLEMENTARY_CERTIFICATION_ALONE_ID, FEATURE_COMPUTE_ORGANIZATION_LEARNER_CERTIFICABILITY_ID, @@ -27,6 +28,11 @@ const featuresBuilder = async function ({ databaseBuilder }) { key: ORGANIZATION_FEATURE.PLACES_MANAGEMENT.key, description: ORGANIZATION_FEATURE.PLACES_MANAGEMENT.description, }); + databaseBuilder.factory.buildFeature({ + id: FEATURE_ATTESTATIONS_MANAGEMENT_ID, + key: ORGANIZATION_FEATURE.ATTESTATIONS_MANAGEMENT.key, + description: ORGANIZATION_FEATURE.ATTESTATIONS_MANAGEMENT.description, + }); databaseBuilder.factory.buildFeature({ id: FEATURE_MISSIONS_MANAGEMENT_ID, key: ORGANIZATION_FEATURE.MISSIONS_MANAGEMENT.key, diff --git a/api/db/seeds/data/common/organization-builder.js b/api/db/seeds/data/common/organization-builder.js index dfa50d960e6..ce10daa8dcb 100644 --- a/api/db/seeds/data/common/organization-builder.js +++ b/api/db/seeds/data/common/organization-builder.js @@ -1,6 +1,7 @@ import { AGRICULTURE_TAG, DEFAULT_PASSWORD, + FEATURE_ATTESTATIONS_MANAGEMENT_ID, FEATURE_COMPUTE_ORGANIZATION_LEARNER_CERTIFICABILITY_ID, FEATURE_LEARNER_IMPORT_ID, FEATURE_MULTIPLE_SENDING_ASSESSMENT_ID, @@ -31,6 +32,7 @@ async function _createScoOrganization(databaseBuilder) { features: [ { id: FEATURE_COMPUTE_ORGANIZATION_LEARNER_CERTIFICABILITY_ID }, { id: FEATURE_MULTIPLE_SENDING_ASSESSMENT_ID }, + { id: FEATURE_ATTESTATIONS_MANAGEMENT_ID }, ], }); diff --git a/api/src/shared/domain/constants.js b/api/src/shared/domain/constants.js index d11baa5ecac..23a15aad419 100644 --- a/api/src/shared/domain/constants.js +++ b/api/src/shared/domain/constants.js @@ -30,6 +30,10 @@ const ORGANIZATION_FEATURE = { key: 'PLACES_MANAGEMENT', description: "Permet l'affichage de la page de gestion des places sur PixOrga", }, + ATTESTATIONS_MANAGEMENT: { + key: 'ATTESTATIONS_MANAGEMENT', + description: "Permet l'affichage de la page attestations sur PixOrga", + }, MULTIPLE_SENDING_ASSESSMENT: { key: 'MULTIPLE_SENDING_ASSESSMENT', description: "Permet d'activer l'envoi multiple sur les campagnes d'évaluation", From 711510cc6dd39fe4d3dc22905c46953b7cf80940 Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 7 Nov 2024 17:09:51 +0100 Subject: [PATCH 2/5] feat(orga): add getter to retrieve if current user have attestations management feature enabled --- orga/app/models/prescriber.js | 5 ++++ orga/app/services/current-user.js | 4 +++ orga/tests/unit/models/prescriber-test.js | 28 +++++++++++++++++++ orga/tests/unit/services/current-user-test.js | 18 ++++++++++++ 4 files changed, 55 insertions(+) diff --git a/orga/app/models/prescriber.js b/orga/app/models/prescriber.js index 55485b2a1e9..c9a9ccdbfb6 100644 --- a/orga/app/models/prescriber.js +++ b/orga/app/models/prescriber.js @@ -13,6 +13,7 @@ export default class Prescriber extends Model { static get featureList() { return { + ATTESTATIONS_MANAGEMENT: 'ATTESTATIONS_MANAGEMENT', MULTIPLE_SENDING_ASSESSMENT: 'MULTIPLE_SENDING_ASSESSMENT', COMPUTE_ORGANIZATION_LEARNER_CERTIFICABILITY: 'COMPUTE_ORGANIZATION_LEARNER_CERTIFICABILITY', PLACES_MANAGEMENT: 'PLACES_MANAGEMENT', @@ -41,6 +42,10 @@ export default class Prescriber extends Model { return this.features[Prescriber.featureList.PLACES_MANAGEMENT]; } + get attestationsManagement() { + return this.features[Prescriber.featureList.ATTESTATIONS_MANAGEMENT]; + } + get missionsManagement() { return this.features[Prescriber.featureList.MISSIONS_MANAGEMENT]; } diff --git a/orga/app/services/current-user.js b/orga/app/services/current-user.js index 61011aed9ff..5ce4deacc19 100644 --- a/orga/app/services/current-user.js +++ b/orga/app/services/current-user.js @@ -72,6 +72,10 @@ export default class CurrentUserService extends Service { ); } + get canAccessAttestationsPage() { + return this.prescriber.attestationsManagement; + } + get canAccessPlacesPage() { return this.isAdminInOrganization && this.prescriber.placesManagement; } diff --git a/orga/tests/unit/models/prescriber-test.js b/orga/tests/unit/models/prescriber-test.js index b7c01000e22..2bc9e501f20 100644 --- a/orga/tests/unit/models/prescriber-test.js +++ b/orga/tests/unit/models/prescriber-test.js @@ -162,6 +162,34 @@ module('Unit | Model | prescriber', function (hooks) { }); }); + module('#attestationsManagement', function () { + test('it returns true when feature is enabled', function (assert) { + // given + const store = this.owner.lookup('service:store'); + const model = store.createRecord('prescriber', { + features: { ['ATTESTATIONS_MANAGEMENT']: true }, + }); + // when + const attestationsManagement = model.attestationsManagement; + + // then + assert.true(attestationsManagement); + }); + + test('it returns false when feature is disabled ', function (assert) { + // given + const store = this.owner.lookup('service:store'); + const model = store.createRecord('prescriber', { + features: { ['ATTESTATIONS_MANAGEMENT']: false }, + }); + // when + const attestationsManagement = model.attestationsManagement; + + // then + assert.false(attestationsManagement); + }); + }); + module('#missionsManagement', function () { test('it returns true when feature is enabled', function (assert) { // given diff --git a/orga/tests/unit/services/current-user-test.js b/orga/tests/unit/services/current-user-test.js index 17648073253..c06ab1a43b5 100644 --- a/orga/tests/unit/services/current-user-test.js +++ b/orga/tests/unit/services/current-user-test.js @@ -302,6 +302,24 @@ module('Unit | Service | current-user', function (hooks) { }); }); + module('#canAccessAttestationsPage', function () { + test('should return true if organization has feature activated', function (assert) { + currentUserService.prescriber = { + attestationsManagement: true, + }; + + assert.true(currentUserService.canAccessAttestationsPage); + }); + + test('should return false if organization does not have feature activated', function (assert) { + currentUserService.prescriber = { + attestationsManagement: false, + }; + + assert.false(currentUserService.canAccessAttestationsPage); + }); + }); + module('#canAccessMissionsPage', function () { test('should return true if user has feature activated', function (assert) { currentUserService.prescriber = { From eb19f5a0426e13c37187508996b143fbbc93483d Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 7 Nov 2024 17:10:37 +0100 Subject: [PATCH 3/5] feat(orga): add fileName param on file-saver service --- orga/app/services/file-saver.js | 7 +-- orga/tests/unit/services/file-saver-test.js | 49 +++++++++++++++------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/orga/app/services/file-saver.js b/orga/app/services/file-saver.js index cd058864f2b..a2dc521c14b 100644 --- a/orga/app/services/file-saver.js +++ b/orga/app/services/file-saver.js @@ -7,6 +7,7 @@ export default class FileSaverService extends Service { async save({ url, token, + fileName, fetcher = _fetchData, downloadFileForIEBrowser = _downloadFileForIEBrowser, downloadFileForModernBrowsers = _downloadFileForModernBrowsers, @@ -18,14 +19,14 @@ export default class FileSaverService extends Service { throw jsonResponse.errors; } - const fileName = _getFileNameFromHeader(response.headers); + const newFileName = fileName ?? _getFileNameFromHeader(response.headers); const fileContent = await response.blob(); const browserIsInternetExplorer = window.document.documentMode; browserIsInternetExplorer - ? downloadFileForIEBrowser({ fileContent, fileName }) - : downloadFileForModernBrowsers({ fileContent, fileName }); + ? downloadFileForIEBrowser({ fileContent, fileName: newFileName }) + : downloadFileForModernBrowsers({ fileContent, fileName: newFileName }); } get locale() { diff --git a/orga/tests/unit/services/file-saver-test.js b/orga/tests/unit/services/file-saver-test.js index 3dc45280241..4a1130c8adc 100644 --- a/orga/tests/unit/services/file-saver-test.js +++ b/orga/tests/unit/services/file-saver-test.js @@ -29,9 +29,8 @@ module('Unit | Service | file-saver', function (hooks) { downloadFileForModernBrowsersStub = sinon.stub().returns(); }); - module('when response has a status 200', function () { - test('should use fileName and fileContent from response', async function (assert) { - // given + module('when response has a status 200', function (hooks) { + hooks.beforeEach(function () { const headers = { get: sinon.stub(), }; @@ -40,19 +39,43 @@ module('Unit | Service | file-saver', function (hooks) { const response = { headers, blob: blobStub, status: 200, json: jsonStub }; fetchStub = sinon.stub().resolves(response); + }); - // when - await fileSaver.save({ - url, - token, - fetcher: fetchStub, - downloadFileForIEBrowser: downloadFileForIEBrowserStub, - downloadFileForModernBrowsers: downloadFileForModernBrowsersStub, + module('when fileName is provided', function () { + test('should use provided fileName', async function (assert) { + // given + const otherFileName = 'other-file-name'; + + // when + await fileSaver.save({ + url, + token, + fileName: otherFileName, + fetcher: fetchStub, + downloadFileForIEBrowser: downloadFileForIEBrowserStub, + downloadFileForModernBrowsers: downloadFileForModernBrowsersStub, + }); + + // then + const expectedArgs = { fileContent: responseContent, fileName: otherFileName }; + assert.ok(downloadFileForModernBrowsersStub.calledWith(expectedArgs)); }); + }); + module('when fileName is not provided', function () { + test('should use fileName and fileContent from response', async function (assert) { + // when + await fileSaver.save({ + url, + token, + fetcher: fetchStub, + downloadFileForIEBrowser: downloadFileForIEBrowserStub, + downloadFileForModernBrowsers: downloadFileForModernBrowsersStub, + }); - // then - const expectedArgs = { fileContent: responseContent, fileName: responseFileName }; - assert.ok(downloadFileForModernBrowsersStub.calledWith(expectedArgs)); + // then + const expectedArgs = { fileContent: responseContent, fileName: responseFileName }; + assert.ok(downloadFileForModernBrowsersStub.calledWith(expectedArgs)); + }); }); }); From a5192ada3fbb61789cae66dc9a1b18050a24c0ce Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 7 Nov 2024 17:25:30 +0100 Subject: [PATCH 4/5] feat(orga): create attestations page --- .../components/attestations/sixth-grade.gjs | 51 ++++++++++ .../controllers/authenticated/attestations.js | 31 +++++++ orga/app/routes/authenticated/attestations.js | 19 ++++ orga/app/styles/app.scss | 1 + .../pages/authenticated/attestations.scss | 24 +++++ .../templates/authenticated/attestations.hbs | 5 + .../attestations/sixth-grade-test.gjs | 70 ++++++++++++++ .../authenticated/attestations-test.js | 93 +++++++++++++++++++ .../routes/authenticated/attestations-test.js | 89 ++++++++++++++++++ orga/translations/en.json | 8 ++ orga/translations/fr.json | 8 ++ orga/translations/nl.json | 8 ++ 12 files changed, 407 insertions(+) create mode 100644 orga/app/components/attestations/sixth-grade.gjs create mode 100644 orga/app/controllers/authenticated/attestations.js create mode 100644 orga/app/routes/authenticated/attestations.js create mode 100644 orga/app/styles/pages/authenticated/attestations.scss create mode 100644 orga/app/templates/authenticated/attestations.hbs create mode 100644 orga/tests/integration/components/attestations/sixth-grade-test.gjs create mode 100644 orga/tests/unit/controllers/authenticated/attestations-test.js create mode 100644 orga/tests/unit/routes/authenticated/attestations-test.js diff --git a/orga/app/components/attestations/sixth-grade.gjs b/orga/app/components/attestations/sixth-grade.gjs new file mode 100644 index 00000000000..88a2e7b5841 --- /dev/null +++ b/orga/app/components/attestations/sixth-grade.gjs @@ -0,0 +1,51 @@ +import PixButton from '@1024pix/pix-ui/components/pix-button'; +import PixMultiSelect from '@1024pix/pix-ui/components/pix-multi-select'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { t } from 'ember-intl'; + +export default class AttestationsSixthGrade extends Component { + @tracked selectedDivisions = []; + + @action + onSubmit(event) { + event.preventDefault(); + + this.args.onSubmit(this.selectedDivisions); + } + + @action + onSelectDivision(value) { + this.selectedDivisions = value; + } + + get isDisabled() { + return !this.selectedDivisions.length; + } + + +} diff --git a/orga/app/controllers/authenticated/attestations.js b/orga/app/controllers/authenticated/attestations.js new file mode 100644 index 00000000000..91d711c6d09 --- /dev/null +++ b/orga/app/controllers/authenticated/attestations.js @@ -0,0 +1,31 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; + +export const SIXTH_GRADE_ATTESTATION_KEY = 'SIXTH_GRADE'; +export const SIXTH_GRADE_ATTESTATION_FILE_NAME = 'attestations'; + +export default class AuthenticatedAttestationsController extends Controller { + @service fileSaver; + @service session; + @service currentUser; + @service notifications; + + @action + async downloadSixthGradeAttestationsFile(selectedDivisions) { + try { + const organizationId = this.currentUser.organization.id; + const formatedDivisionsForQuery = selectedDivisions + .map((division) => `divisions[]=${encodeURIComponent(division)}`) + .join('&'); + + const url = `/api/organizations/${organizationId}/attestations/${SIXTH_GRADE_ATTESTATION_KEY}?${formatedDivisionsForQuery}`; + + const token = this.session.isAuthenticated ? this.session.data.authenticated.access_token : ''; + + await this.fileSaver.save({ url, token, fileName: SIXTH_GRADE_ATTESTATION_FILE_NAME }); + } catch (error) { + this.notifications.sendError(error.message, { autoClear: false }); + } + } +} diff --git a/orga/app/routes/authenticated/attestations.js b/orga/app/routes/authenticated/attestations.js new file mode 100644 index 00000000000..671056adbbb --- /dev/null +++ b/orga/app/routes/authenticated/attestations.js @@ -0,0 +1,19 @@ +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +export default class AuthenticatedAttestationsRoute extends Route { + @service currentUser; + @service router; + + beforeModel() { + if (!this.currentUser.canAccessAttestationsPage) { + this.router.replaceWith('application'); + } + } + + async model() { + const divisions = await this.currentUser.organization.divisions; + const options = divisions.map(({ name }) => ({ label: name, value: name })); + return { options }; + } +} diff --git a/orga/app/styles/app.scss b/orga/app/styles/app.scss index 513f60332e0..dbc39f979e6 100644 --- a/orga/app/styles/app.scss +++ b/orga/app/styles/app.scss @@ -58,6 +58,7 @@ @import 'pages/join'; @import 'pages/join-request'; @import 'pages/authenticated'; +@import 'pages/authenticated/attestations'; @import 'pages/authenticated/campaigns/campaign'; @import 'pages/authenticated/campaigns/create-form'; @import 'pages/authenticated/campaigns/new'; diff --git a/orga/app/styles/pages/authenticated/attestations.scss b/orga/app/styles/pages/authenticated/attestations.scss new file mode 100644 index 00000000000..4114f4a6596 --- /dev/null +++ b/orga/app/styles/pages/authenticated/attestations.scss @@ -0,0 +1,24 @@ +.attestations-page { + display: flex; + flex-direction: column; + + &__title { + @extend %pix-title-m; + + margin: var(--pix-spacing-8x) 0; + } + + &__text { + @extend %pix-body-m; + + margin-bottom: var(--pix-spacing-8x); + + } + + &__action { + display: flex; + flex-wrap: wrap; + gap: var(--pix-spacing-4x); + align-items: end + } +} diff --git a/orga/app/templates/authenticated/attestations.hbs b/orga/app/templates/authenticated/attestations.hbs new file mode 100644 index 00000000000..6f78b8cd424 --- /dev/null +++ b/orga/app/templates/authenticated/attestations.hbs @@ -0,0 +1,5 @@ +{{page-title (t "pages.attestations.title")}} + +
+ +
\ No newline at end of file diff --git a/orga/tests/integration/components/attestations/sixth-grade-test.gjs b/orga/tests/integration/components/attestations/sixth-grade-test.gjs new file mode 100644 index 00000000000..ff076bdeecb --- /dev/null +++ b/orga/tests/integration/components/attestations/sixth-grade-test.gjs @@ -0,0 +1,70 @@ +import { render } from '@1024pix/ember-testing-library'; +import { click } from '@ember/test-helpers'; +import { t } from 'ember-intl/test-support'; +import SixthGrade from 'pix-orga/components/attestations/sixth-grade'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../../helpers/setup-intl-rendering'; + +module('Integration | Component | Attestations | Sixth-grade', function (hooks) { + setupIntlRenderingTest(hooks); + + test('it should display all basics informations', async function (assert) { + // given + const onSubmit = sinon.stub(); + const divisions = []; + + // when + const screen = await render(); + // then + assert.ok(screen.getByRole('heading', { name: t('pages.attestations.title') })); + assert.ok(screen.getByText(t('pages.attestations.description'))); + assert.ok(screen.getByRole('textbox', { name: t('pages.attestations.select-label') })); + assert.ok(screen.getByPlaceholderText(t('pages.attestations.placeholder'))); + assert.ok(screen.getByRole('button', { name: t('pages.attestations.download-attestations-button') })); + }); + + test('download button is disabled if there is no selected divisions', async function (assert) { + // given + const onSubmit = sinon.stub(); + const divisions = []; + + // when + const screen = await render(); + + // then + const downloadButton = await screen.getByRole('button', { + name: t('pages.attestations.download-attestations-button'), + }); + assert.dom(downloadButton).isDisabled(); + }); + + test('it should call onSubmit action with selected divisions', async function (assert) { + // given + const onSubmit = sinon.stub(); + + const divisions = [{ label: 'division1', value: 'division1' }]; + + // when + const screen = await render(); + + const multiSelect = await screen.getByRole('textbox', { name: t('pages.attestations.select-label') }); + await click(multiSelect); + + const firstDivisionOption = await screen.findByRole('checkbox', { name: 'division1' }); + await click(firstDivisionOption); + + const downloadButton = await screen.getByRole('button', { + name: t('pages.attestations.download-attestations-button'), + }); + + // we need to get out of input choice to click on download button, so we have to click again on the multiselect to close it + await click(multiSelect); + await click(downloadButton); + + // then + sinon.assert.calledWithExactly(onSubmit, ['division1']); + assert.ok(true); + }); +}); diff --git a/orga/tests/unit/controllers/authenticated/attestations-test.js b/orga/tests/unit/controllers/authenticated/attestations-test.js new file mode 100644 index 00000000000..68c6bb87bce --- /dev/null +++ b/orga/tests/unit/controllers/authenticated/attestations-test.js @@ -0,0 +1,93 @@ +import Service from '@ember/service'; +import { + SIXTH_GRADE_ATTESTATION_FILE_NAME, + SIXTH_GRADE_ATTESTATION_KEY, +} from 'pix-orga/controllers/authenticated/attestations'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +import setupIntlRenderingTest from '../../../helpers/setup-intl-rendering'; + +module('Unit | Controller | authenticated/attestations', function (hooks) { + setupIntlRenderingTest(hooks); + + module('#downloadSixthGradeAttestationsFile', function () { + test('should call the file-saver service with the right parameters', async function (assert) { + // given + const controller = this.owner.lookup('controller:authenticated/attestations'); + + const token = 'a token'; + const organizationId = 12345; + const selectedDivision = ['3èmea']; + + controller.session = { + isAuthenticated: true, + data: { + authenticated: { + access_token: token, + }, + }, + }; + + controller.currentUser = { + organization: { + id: organizationId, + }, + }; + + controller.fileSaver = { + save: sinon.stub(), + }; + + controller.model = { + options: [{ label: '3èmeA', value: '3èmeA' }], + }; + + // when + await controller.downloadSixthGradeAttestationsFile(selectedDivision); + + // then + assert.ok( + controller.fileSaver.save.calledWith({ + token, + url: `/api/organizations/${organizationId}/attestations/${SIXTH_GRADE_ATTESTATION_KEY}?divisions[]=${encodeURIComponent(selectedDivision)}`, + fileName: SIXTH_GRADE_ATTESTATION_FILE_NAME, + }), + ); + }); + + test('it should not call file-save service and display an error if an error occurs', async function (assert) { + // given + const controller = this.owner.lookup('controller:authenticated/attestations'); + const selectedDivision = ['3emeA']; + const organizationId = 123; + const errorMessage = 'oops'; + + controller.currentUser = { + organization: { + id: organizationId, + }, + }; + controller.fileSaver = { + save: sinon.stub(), + }; + controller.fileSaver.save.rejects(new Error(errorMessage)); + + controller.model = { + options: [{ label: '3èmeA', value: '3èmeA' }], + }; + class NotificationsStub extends Service { + sendError = errorMock; + } + this.owner.register('service:notifications', NotificationsStub); + const errorMock = sinon.stub(); + + // when + await controller.downloadSixthGradeAttestationsFile(selectedDivision); + + // then + sinon.assert.calledWith(errorMock, errorMessage, { autoClear: false }); + assert.ok(true); + }); + }); +}); diff --git a/orga/tests/unit/routes/authenticated/attestations-test.js b/orga/tests/unit/routes/authenticated/attestations-test.js new file mode 100644 index 00000000000..b27baa2dfa8 --- /dev/null +++ b/orga/tests/unit/routes/authenticated/attestations-test.js @@ -0,0 +1,89 @@ +import Service from '@ember/service'; +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +module('Unit | Route | authenticated/attestations', function (hooks) { + setupTest(hooks); + + module('beforeModel', function () { + test('should redirect to application when currentUser.canAccessAttestationsPage is false', function (assert) { + // given + class CurrentUserStub extends Service { + canAccessAttestationsPage = false; + } + + this.owner.register('service:current-user', CurrentUserStub); + const route = this.owner.lookup('route:authenticated.attestations'); + const replaceWithStub = sinon.stub(); + route.router.replaceWith = replaceWithStub; + + // when + route.beforeModel(); + + // then + sinon.assert.calledOnceWithExactly(replaceWithStub, 'application'); + assert.ok(true); + }); + + test('should not redirect to application when currentUser.canAccessAttestationsPage is true', function (assert) { + // given + class CurrentUserStub extends Service { + canAccessAttestationsPage = true; + } + + this.owner.register('service:current-user', CurrentUserStub); + const route = this.owner.lookup('route:authenticated.attestations'); + const replaceWithStub = sinon.stub(); + route.router.replaceWith = replaceWithStub; + + // when + route.beforeModel(); + + // then + sinon.assert.notCalled(replaceWithStub); + assert.ok(true); + }); + }); + + module('#model', function () { + test('it should return a list of options based on organization divisions', async function (assert) { + // given + const divisions = [{ name: '3èmeA' }, { name: '2ndE' }]; + class CurrentUserStub extends Service { + canAccessAttestationsPage = true; + organization = { + id: 12345, + divisions, + }; + } + + const findRecordStub = sinon.stub(); + class StoreStub extends Service { + findRecord = findRecordStub; + } + + this.owner.register('service:current-user', CurrentUserStub); + this.owner.register('service:store', StoreStub); + + const route = this.owner.lookup('route:authenticated/attestations'); + + // when + const actualOptions = await route.model(); + + // then + assert.deepEqual(actualOptions, { + options: [ + { + label: '3èmeA', + value: '3èmeA', + }, + { + label: '2ndE', + value: '2ndE', + }, + ], + }); + }); + }); +}); diff --git a/orga/translations/en.json b/orga/translations/en.json index b1090443c83..4e52d37c817 100644 --- a/orga/translations/en.json +++ b/orga/translations/en.json @@ -348,6 +348,7 @@ }, "main": { "aria-label": "Main navigation", + "attestations": "Attestations", "campaigns": "Campaigns", "certifications": "Certifications", "documentation": "Documentation", @@ -410,6 +411,13 @@ "row-title": "Competence" } }, + "attestations": { + "title": "Attestations", + "description": "Select the class for which you wish to export attestations in PDF format.", + "download-attestations-button": "Download", + "placeholder": "-- Select --", + "select-label": "Select one or more classes" + }, "campaign": { "actions": { "export-results": "Export the results (.csv)", diff --git a/orga/translations/fr.json b/orga/translations/fr.json index b0adad8c269..bd70c7a3336 100644 --- a/orga/translations/fr.json +++ b/orga/translations/fr.json @@ -354,6 +354,7 @@ }, "main": { "aria-label": "Navigation principale", + "attestations": "Attestations", "campaigns": "Campagnes", "certifications": "Certifications", "documentation": "Documentation", @@ -416,6 +417,13 @@ "row-title": "Compétence" } }, + "attestations": { + "title": "Attestations", + "description": "Sélectionnez la classe pour laquelle vous souhaitez exporter les attestations au format PDF.", + "download-attestations-button": "Télécharger", + "placeholder": "-- Sélectionner --", + "select-label": "Sélectionnez une ou plusieurs classes" + }, "campaign": { "actions": { "export-results": "Exporter les résultats (.csv)", diff --git a/orga/translations/nl.json b/orga/translations/nl.json index 463c364be1b..9465752c7c5 100644 --- a/orga/translations/nl.json +++ b/orga/translations/nl.json @@ -348,6 +348,7 @@ }, "main": { "aria-label": "Hoofdnavigatie", + "attestations": "certificaten", "campaigns": "Campagnes", "certifications": "Certificeringen", "documentation": "Documentatie", @@ -410,6 +411,13 @@ }, "title": "Resultaten voor {firstName} {lastName}" }, + "attestations": { + "title": "Certificaten", + "description": "Selecteer de klas waarvoor je de attesten in PDF-formaat wilt exporteren.", + "download-attestations-button": "Downloaden", + "placeholder": "-- Selecteer --", + "select-label": "Selecteer een of meer klassen" + }, "campaign-activity": { "actions": { "delete": "Verwijderen" From 1533294e34bead4b8fe277f04d7f9fe20ed473da Mon Sep 17 00:00:00 2001 From: Alexandre Monney Date: Thu, 7 Nov 2024 17:26:19 +0100 Subject: [PATCH 5/5] feat(orga): add attestations in ember router and one entry in sidebar --- orga/app/components/layout/sidebar.hbs | 8 ++++++++ orga/app/components/layout/sidebar.js | 4 ++++ orga/app/router.js | 1 + .../integration/components/layout/sidebar-test.js | 15 +++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/orga/app/components/layout/sidebar.hbs b/orga/app/components/layout/sidebar.hbs index f10a5a9a5d7..459c41eff58 100644 --- a/orga/app/components/layout/sidebar.hbs +++ b/orga/app/components/layout/sidebar.hbs @@ -23,6 +23,14 @@ {{t "navigation.main.certifications"}} {{/if}} + {{#if this.shouldDisplayAttestationsEntry}} + + + + + {{t "navigation.main.attestations"}} + + {{/if}} {{#if this.shouldDisplayMissionsEntry}} diff --git a/orga/app/components/layout/sidebar.js b/orga/app/components/layout/sidebar.js index 95c55b9d3b7..c505befdbdd 100644 --- a/orga/app/components/layout/sidebar.js +++ b/orga/app/components/layout/sidebar.js @@ -21,6 +21,10 @@ export default class SidebarMenu extends Component { return this.currentUser.isAdminInOrganization && this.currentUser.isSCOManagingStudents; } + get shouldDisplayAttestationsEntry() { + return this.currentUser.canAccessAttestationsPage; + } + get shouldDisplayPlacesEntry() { return this.currentUser.canAccessPlacesPage; } diff --git a/orga/app/router.js b/orga/app/router.js index f17c0597969..ab9d2155d22 100644 --- a/orga/app/router.js +++ b/orga/app/router.js @@ -77,6 +77,7 @@ Router.map(function () { }); }); this.route('certifications'); + this.route('attestations'); this.route('preselect-target-profile', { path: '/selection-sujets' }); this.route('places'); this.route('missions', function () { diff --git a/orga/tests/integration/components/layout/sidebar-test.js b/orga/tests/integration/components/layout/sidebar-test.js index 29850fec3ef..a2a6d05ec64 100644 --- a/orga/tests/integration/components/layout/sidebar-test.js +++ b/orga/tests/integration/components/layout/sidebar-test.js @@ -316,6 +316,21 @@ module('Integration | Component | Layout::Sidebar', function (hooks) { }); }); + module('When the user has the attestations feature', function () { + test('should display Attestations entry with link to attestation page', async function (assert) { + class CurrentUserStub extends Service { + organization = Object.create({ id: 5 }); + canAccessAttestationsPage = true; + } + this.owner.register('service:current-user', CurrentUserStub); + + const screen = await render(hbs``); + + const attestationsLink = screen.getByRole('link', { name: t('navigation.main.attestations') }); + assert.dom(attestationsLink).hasAttribute('href', '/attestations'); + }); + }); + test('[a11y] it should contain accessibility aria-label nav', async function (assert) { // given class CurrentUserStub extends Service {