From 4e0565464cdbc53a12c19f29a99d912fddbb4d0d Mon Sep 17 00:00:00 2001
From: Xavier Carron <33637571+xav-car@users.noreply.github.com>
Date: Fri, 27 Sep 2024 18:01:18 +0200
Subject: [PATCH] feat(admin): add page to update organization import format
---
admin/app/adapters/campaigns-import.js | 10 --
admin/app/adapters/import-files.js | 14 +++
.../campaigns/campaigns-import.gjs | 2 +-
admin/app/components/administration/nav.gjs | 4 +
.../administration/organizations/index.gjs | 3 +
.../update-organization-import-format.gjs | 57 +++++++++
admin/app/router.js | 1 +
.../administration/organizations.hbs | 1 +
admin/mirage/handlers/badge-criteria.js | 2 -
.../campaigns/campaigns-import-test.gjs | 2 +-
...update-organization-import-format-test.gjs | 108 ++++++++++++++++++
.../users/campaign-participations-test.gjs | 2 +-
.../tests/unit/adapters/import-files-test.js | 22 +++-
admin/translations/en.json | 11 ++
admin/translations/fr.json | 11 ++
15 files changed, 234 insertions(+), 16 deletions(-)
delete mode 100644 admin/app/adapters/campaigns-import.js
create mode 100644 admin/app/components/administration/organizations/index.gjs
create mode 100644 admin/app/components/administration/organizations/update-organization-import-format.gjs
create mode 100644 admin/app/templates/authenticated/administration/organizations.hbs
create mode 100644 admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs
diff --git a/admin/app/adapters/campaigns-import.js b/admin/app/adapters/campaigns-import.js
deleted file mode 100644
index d4e7976e64c..00000000000
--- a/admin/app/adapters/campaigns-import.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import ApplicationAdapter from './application';
-
-export default class CampaignsImportAdapter extends ApplicationAdapter {
- addCampaignsCsv(files) {
- if (!files || files.length === 0) return;
-
- const url = `${this.host}/${this.namespace}/admin/campaigns`;
- return this.ajax(url, 'POST', { data: files[0] });
- }
-}
diff --git a/admin/app/adapters/import-files.js b/admin/app/adapters/import-files.js
index e2a6ec4783e..eeab7d2c596 100644
--- a/admin/app/adapters/import-files.js
+++ b/admin/app/adapters/import-files.js
@@ -9,4 +9,18 @@ export default class ImportFilesAdapter extends ApplicationAdapter {
const url = `${this.host}/${this.namespace}/campaigns/archive-campaigns`;
return this.ajax(url, 'POST', { data: files[0] });
}
+
+ updateOrganizationImportFormat(files) {
+ if (!files || files.length === 0) return;
+
+ const url = `${this.host}/${this.namespace}/import-organization-learners-format`;
+ return this.ajax(url, 'POST', { data: files[0] });
+ }
+
+ addCampaignsCsv(files) {
+ if (!files || files.length === 0) return;
+
+ const url = `${this.host}/${this.namespace}/campaigns`;
+ return this.ajax(url, 'POST', { data: files[0] });
+ }
}
diff --git a/admin/app/components/administration/campaigns/campaigns-import.gjs b/admin/app/components/administration/campaigns/campaigns-import.gjs
index 82051cea956..3398db471a6 100644
--- a/admin/app/components/administration/campaigns/campaigns-import.gjs
+++ b/admin/app/components/administration/campaigns/campaigns-import.gjs
@@ -16,7 +16,7 @@ export default class CampaignsImport extends Component {
async importCampaigns(files) {
this.notifications.clearAll();
- const adapter = this.store.adapterFor('campaigns-import');
+ const adapter = this.store.adapterFor('import-files');
try {
await adapter.addCampaignsCsv(files);
this.notifications.success(this.intl.t('components.administration.campaigns-import.notifications.success'));
diff --git a/admin/app/components/administration/nav.gjs b/admin/app/components/administration/nav.gjs
index e955e79c4e0..3a7e0d60fac 100644
--- a/admin/app/components/administration/nav.gjs
+++ b/admin/app/components/administration/nav.gjs
@@ -12,6 +12,10 @@ import { t } from 'ember-intl';
{{t "pages.administration.navigation.campaigns.label"}}
+
+ {{t "pages.administration.navigation.organizations.label"}}
+
+
{{t "pages.administration.navigation.certification.label"}}
diff --git a/admin/app/components/administration/organizations/index.gjs b/admin/app/components/administration/organizations/index.gjs
new file mode 100644
index 00000000000..a6a18df4e7c
--- /dev/null
+++ b/admin/app/components/administration/organizations/index.gjs
@@ -0,0 +1,3 @@
+import UpdateOrganizationImportFormat from './update-organization-import-format';
+
+
diff --git a/admin/app/components/administration/organizations/update-organization-import-format.gjs b/admin/app/components/administration/organizations/update-organization-import-format.gjs
new file mode 100644
index 00000000000..c87e1e7fb1d
--- /dev/null
+++ b/admin/app/components/administration/organizations/update-organization-import-format.gjs
@@ -0,0 +1,57 @@
+import PixButtonUpload from '@1024pix/pix-ui/components/pix-button-upload';
+import { action } from '@ember/object';
+import { service } from '@ember/service';
+import Component from '@glimmer/component';
+import { t } from 'ember-intl';
+
+import AdministrationBlockLayout from '../block-layout';
+
+export default class UpdateOrganizationImportFormat extends Component {
+ @service intl;
+ @service notifications;
+ @service router;
+ @service store;
+
+ @action
+ async uploadOrganizationImportFile(files) {
+ this.notifications.clearAll();
+ const adapter = this.store.adapterFor('import-files');
+ try {
+ await adapter.updateOrganizationImportFormat(files);
+ this.notifications.success(
+ this.intl.t('components.administration.organization-import-format.notifications.success'),
+ );
+ } catch (errorResponse) {
+ const errors = errorResponse.errors;
+ if (!errors) {
+ return this.notifications.error(this.intl.t('common.notifications.generic-error'));
+ }
+
+ errors.forEach((error) => {
+ switch (error.code) {
+ case 'MISSING_REQUIRED_FIELD_NAMES':
+ this.notifications.error(`${error.meta}`, { autoClear: false });
+ break;
+ default:
+ this.notifications.error(error.detail, { autoClear: false });
+ }
+ });
+ } finally {
+ this.isLoading = false;
+ }
+ }
+
+
+
+ {{t "components.administration.organization-import-format.upload-button"}}
+
+
+
+}
diff --git a/admin/app/router.js b/admin/app/router.js
index 243c31336f1..5ee9137d2c8 100644
--- a/admin/app/router.js
+++ b/admin/app/router.js
@@ -141,6 +141,7 @@ Router.map(function () {
this.route('administration', function () {
this.route('common');
this.route('campaigns');
+ this.route('organizations');
this.route('certification');
this.route('deployment');
this.route('access');
diff --git a/admin/app/templates/authenticated/administration/organizations.hbs b/admin/app/templates/authenticated/administration/organizations.hbs
new file mode 100644
index 00000000000..2eef8f4c678
--- /dev/null
+++ b/admin/app/templates/authenticated/administration/organizations.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/admin/mirage/handlers/badge-criteria.js b/admin/mirage/handlers/badge-criteria.js
index e5b798526ef..03f00aefb7e 100644
--- a/admin/mirage/handlers/badge-criteria.js
+++ b/admin/mirage/handlers/badge-criteria.js
@@ -3,8 +3,6 @@ function updateBadgeCriterion(schema, request) {
const badgeCriterionId = request.params.id;
const params = JSON.parse(request.requestBody);
- console.log(schema);
-
const badgeCriterion = schema.badgeCriteria.find(badgeCriterionId);
badgeCriterion.update(params.data.attributes);
diff --git a/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs b/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs
index 7463ed8c9ca..c800a9be63c 100644
--- a/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs
+++ b/admin/tests/integration/components/administration/campaigns/campaigns-import-test.gjs
@@ -16,7 +16,7 @@ module('Integration | Component | administration/campaigns-import', function (h
let store, adapter, notificationSuccessStub, clearAllStub, saveAdapterStub, notificationErrorStub;
hooks.beforeEach(function () {
store = this.owner.lookup('service:store');
- adapter = store.adapterFor('campaigns-import');
+ adapter = store.adapterFor('import-files');
saveAdapterStub = sinon.stub(adapter, 'addCampaignsCsv');
notificationSuccessStub = sinon.stub();
notificationErrorStub = sinon.stub().returns();
diff --git a/admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs b/admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs
new file mode 100644
index 00000000000..0c5622c3bb1
--- /dev/null
+++ b/admin/tests/integration/components/administration/organizations/update-organization-import-format-test.gjs
@@ -0,0 +1,108 @@
+import { render } from '@1024pix/ember-testing-library';
+import Service from '@ember/service';
+import { triggerEvent } from '@ember/test-helpers';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { t } from 'ember-intl/test-support';
+import UpdateOrganizationImportFormat from 'pix-admin/components/administration/organizations/update-organization-import-format';
+import { module, test } from 'qunit';
+import sinon from 'sinon';
+
+import setupIntlRenderingTest from '../../../../helpers/setup-intl-rendering';
+
+module('Integration | Component | administration/update-organization-import-format', function (hooks) {
+ setupIntlRenderingTest(hooks);
+ setupMirage(hooks);
+
+ let store, adapter, notificationSuccessStub, clearAllStub, saveAdapterStub, notificationErrorStub;
+ hooks.beforeEach(function () {
+ store = this.owner.lookup('service:store');
+ adapter = store.adapterFor('import-files');
+ saveAdapterStub = sinon.stub(adapter, 'updateOrganizationImportFormat');
+ notificationSuccessStub = sinon.stub();
+ notificationErrorStub = sinon.stub().returns();
+
+ clearAllStub = sinon.stub();
+ });
+
+ module('when import succeeds', function () {
+ test('it displays a success notification', async function (assert) {
+ // given
+ const files = Symbol('file');
+ class NotificationsStub extends Service {
+ success = notificationSuccessStub;
+ error = notificationErrorStub;
+ clearAll = clearAllStub;
+ }
+ this.owner.register('service:notifications', NotificationsStub);
+ saveAdapterStub.withArgs([files]).resolves();
+
+ // when
+ const screen = await render();
+ const input = await screen.findByLabelText(
+ t('components.administration.organization-import-format.upload-button'),
+ );
+ await triggerEvent(input, 'change', { files: [files] });
+
+ // then
+ assert.ok(true);
+ assert.ok(notificationErrorStub.notCalled);
+ assert.ok(
+ notificationSuccessStub.calledWith(
+ t('components.administration.organization-import-format.notifications.success'),
+ ),
+ );
+ });
+ });
+
+ module('when import fails', function () {
+ test('it displays a specific error notification on missing required field', async function (assert) {
+ // given
+ const files = Symbol('file');
+ class NotificationsStub extends Service {
+ error = notificationErrorStub;
+ success = notificationSuccessStub;
+ clearAll = clearAllStub;
+ }
+ saveAdapterStub.withArgs([files]).rejects({
+ errors: [{ status: '422', meta: 'POUET', code: 'MISSING_REQUIRED_FIELD_NAMES' }],
+ });
+ this.owner.register('service:notifications', NotificationsStub);
+
+ // when
+ const screen = await render();
+ const input = await screen.findByLabelText(
+ t('components.administration.organization-import-format.upload-button'),
+ );
+ await triggerEvent(input, 'change', { files: [files] });
+
+ // then
+ assert.ok(notificationSuccessStub.notCalled);
+ assert.ok(notificationErrorStub.calledWithExactly('POUET', { autoClear: false }));
+ });
+
+ test('it displays an error notification', async function (assert) {
+ // given
+ const files = Symbol('file');
+ class NotificationsStub extends Service {
+ error = notificationErrorStub;
+ success = notificationSuccessStub;
+ clearAll = clearAllStub;
+ }
+ saveAdapterStub.withArgs([files]).rejects({
+ errors: [{ status: '422', title: "Un soucis avec l'import", code: '422', detail: 'Erreur d’import' }],
+ });
+ this.owner.register('service:notifications', NotificationsStub);
+
+ // when
+ const screen = await render();
+ const input = await screen.findByLabelText(
+ t('components.administration.organization-import-format.upload-button'),
+ );
+ await triggerEvent(input, 'change', { files: [files] });
+
+ // then
+ assert.ok(notificationSuccessStub.notCalled);
+ assert.ok(notificationErrorStub.called);
+ });
+ });
+});
diff --git a/admin/tests/integration/components/users/campaign-participations-test.gjs b/admin/tests/integration/components/users/campaign-participations-test.gjs
index 6aa89ff0407..9d1d49c5b18 100644
--- a/admin/tests/integration/components/users/campaign-participations-test.gjs
+++ b/admin/tests/integration/components/users/campaign-participations-test.gjs
@@ -62,7 +62,7 @@ module('Integration | Component | users | campaign-participation', function (hoo
assert.dom(screen.getByRole('link', { name: 'SOMECODE' })).exists();
});
- test('it should display orgnaization learner information', async function (assert) {
+ test('it should display organization learner information', async function (assert) {
// given
const participation = EmberObject.create({
organizationLearnerFullName: 'Un nom bien long',
diff --git a/admin/tests/unit/adapters/import-files-test.js b/admin/tests/unit/adapters/import-files-test.js
index c82ea84a052..55393af4d0c 100644
--- a/admin/tests/unit/adapters/import-files-test.js
+++ b/admin/tests/unit/adapters/import-files-test.js
@@ -12,7 +12,7 @@ module('Unit | Adapter | ImportFiles', function (hooks) {
});
module('#importCampaignsToArchive', function () {
- test('should build importCampaignsToArchive url from organizationId', async function (assert) {
+ test('should build importCampaignsToArchive url', async function (assert) {
// when
await adapter.importCampaignsToArchive([Symbol()]);
@@ -20,4 +20,24 @@ module('Unit | Adapter | ImportFiles', function (hooks) {
assert.ok(adapter.ajax.calledWith('http://localhost:3000/api/admin/campaigns/archive-campaigns', 'POST'));
});
});
+
+ module('#updateOrganizationImportFormat', function () {
+ test('should build updateOrganizationImportFormat url', async function (assert) {
+ // when
+ await adapter.updateOrganizationImportFormat([Symbol()]);
+
+ // then
+ assert.ok(adapter.ajax.calledWith('http://localhost:3000/api/admin/import-organization-learners-format', 'POST'));
+ });
+ });
+
+ module('#addCampaignsCsv', function () {
+ test('should build addCampaignsCsv url', async function (assert) {
+ // when
+ await adapter.addCampaignsCsv([Symbol()]);
+
+ // then
+ assert.ok(adapter.ajax.calledWith('http://localhost:3000/api/admin/campaigns', 'POST'));
+ });
+ });
});
diff --git a/admin/translations/en.json b/admin/translations/en.json
index e80a03e076b..0e4001ddb73 100644
--- a/admin/translations/en.json
+++ b/admin/translations/en.json
@@ -68,6 +68,14 @@
},
"upload-button": "Import JSON file"
},
+ "organization-import-format": {
+ "title": "Update existing import formats",
+ "description": "Only existing format imports will be updated. If the name does not match, no format import will be created.",
+ "notifications": {
+ "success": "Format imports are updated correctly"
+ },
+ "upload-button": "Importing a JSON file"
+ },
"organization-tags-import": {
"title": "Bulk addition of tags on organisations",
"description": "Allow to add tags to organisations. Each line of the CSV file must be made of the ID of an organisation, followed by the name of a tag.",
@@ -407,6 +415,9 @@
},
"deployment": {
"label": "Deployment"
+ },
+ "organizations": {
+ "label": "Organizations"
}
}
},
diff --git a/admin/translations/fr.json b/admin/translations/fr.json
index 24b625b7949..eee1c067d37 100644
--- a/admin/translations/fr.json
+++ b/admin/translations/fr.json
@@ -76,6 +76,14 @@
},
"upload-button": "Importer un fichier JSON"
},
+ "organization-import-format": {
+ "title": "Mise à jour d'import à format existants",
+ "description": "Seul les imports à format existants seront mis à jour. Dans le cas où le nom ne correspond pas, il n'y aura pas de création d'import à format.",
+ "notifications": {
+ "success": "Les imports à formats sont bien mis à jour"
+ },
+ "upload-button": "Importer un fichier JSON"
+ },
"organization-tags-import": {
"title": "Ajout de tags en masse sur des organisations",
"description": "Permet d’ajouter des tags à des organisations. Chaque ligne du fichier CSV doit être constituée de l’ID d’une organisation, suivie du nom d’un tag.",
@@ -417,6 +425,9 @@
},
"deployment": {
"label": "Déploiement"
+ },
+ "organizations": {
+ "label": "Organisations"
}
}
},