From 20cc173e9321b78a654cda1bbedf52b4b325bdd8 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Thu, 5 Dec 2024 15:44:30 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(anct)=20fetch=20and=20display=20organ?= =?UTF-8?q?ization=20names=20of=20communes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ANCT-specific extraction of organization names for communes, front end changes to match. --- CHANGELOG.md | 1 + docker/auth/realm.json | 2 +- src/backend/people/settings.py | 6 +++ src/backend/plugins/organizations.py | 39 ++++++++++++------- ...est_name_from_siret_organization_plugin.py | 33 ++++++++++++++++ .../apps/desk/src/core/auth/api/types.ts | 7 ++++ .../src/features/header/AccountDropdown.tsx | 7 +++- .../apps/e2e/__tests__/app-desk/siret.spec.ts | 16 ++++++-- .../env.d/preprod/values.desk.yaml.gotmpl | 2 +- .../env.d/production/values.desk.yaml.gotmpl | 2 +- 10 files changed, 93 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 274877a..915d297 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Added +- ✨(anct) fetch and display organization names of communes #583 - ✨(frontend) display email if no username #562 - 🧑‍💻(oidc) add ability to pull registration ID (e.g. SIRET) from OIDC #577 diff --git a/docker/auth/realm.json b/docker/auth/realm.json index 05930cb..c92fb9f 100644 --- a/docker/auth/realm.json +++ b/docker/auth/realm.json @@ -62,7 +62,7 @@ "username": "e2e.marie", "email": "marie.varzy@gmail.com", "firstName": "Marie", - "lastName": "Devarzy", + "lastName": "Delamairie", "enabled": true, "attributes": { "siret": "21580304000017" diff --git a/src/backend/people/settings.py b/src/backend/people/settings.py index f1f5e7a..a9cbe78 100755 --- a/src/backend/people/settings.py +++ b/src/backend/people/settings.py @@ -641,6 +641,8 @@ class Development(Base): OIDC_ORGANIZATION_REGISTRATION_ID_FIELD = "siret" + ORGANIZATION_PLUGINS = ["plugins.organizations.NameFromSiretOrganizationPlugin"] + def __init__(self): """In dev, force installs needed for Swagger API.""" # pylint: disable=invalid-name @@ -686,6 +688,10 @@ class Test(Base): # this is a dev credentials for mail provisioning API MAIL_PROVISIONING_API_CREDENTIALS = "bGFfcmVnaWU6cGFzc3dvcmQ=" + OIDC_ORGANIZATION_REGISTRATION_ID_FIELD = "siret" + + ORGANIZATION_PLUGINS = ["plugins.organizations.NameFromSiretOrganizationPlugin"] + ORGANIZATION_REGISTRATION_ID_VALIDATORS = [ { "NAME": "django.core.validators.RegexValidator", diff --git a/src/backend/plugins/organizations.py b/src/backend/plugins/organizations.py index 0108542..1dcbc47 100644 --- a/src/backend/plugins/organizations.py +++ b/src/backend/plugins/organizations.py @@ -3,6 +3,7 @@ import logging import requests +from requests.adapters import HTTPAdapter, Retry from core.plugins.base import BaseOrganizationPlugin @@ -33,20 +34,17 @@ class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin): logger.warning("Empty list 'liste_enseignes' in %s", organization_data) return None - def _get_organization_name_from_siret(self, siret): - """Return the organization name from the SIRET.""" - try: - response = requests.get(self._api_url.format(siret=siret), timeout=10) - response.raise_for_status() - data = response.json() - except requests.RequestException as exc: - logger.exception("%s: Unable to fetch organization name from SIRET", exc) - return None - + def get_organization_name_from_results(self, data, siret): + """Return the organization name from the results of a SIRET search.""" for result in data["results"]: + nature = "nature_juridique" + commune = nature in result and result[nature] == "7210" for organization in result["matching_etablissements"]: if organization.get("siret") == siret: - return self._extract_name_from_organization_data(organization) + if commune: + return organization["libelle_commune"].title() + + return self._extract_name_from_organization_data(organization) logger.warning("No organization name found for SIRET %s", siret) return None @@ -63,10 +61,21 @@ class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin): # In the nominal case, there is only one registration ID because # the organization as been created from it. - name = self._get_organization_name_from_siret( - organization.registration_id_list[0] - ) - if not name: + try: + # Retry logic as the API may be rate limited + s = requests.Session() + retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[429]) + s.mount("https://", HTTPAdapter(max_retries=retries)) + + siret = organization.registration_id_list[0] + response = s.get(self._api_url.format(siret=siret), timeout=10) + response.raise_for_status() + data = response.json() + name = self.get_organization_name_from_results(data, siret) + if not name: + return + except requests.RequestException as exc: + logger.exception("%s: Unable to fetch organization name from SIRET", exc) return organization.name = name diff --git a/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py b/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py index b7b33ee..56c8ac2 100644 --- a/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py +++ b/src/backend/plugins/tests/organizations/test_name_from_siret_organization_plugin.py @@ -6,6 +6,8 @@ import responses from core.models import Organization from core.plugins.loader import get_organization_plugins +from plugins.organizations import NameFromSiretOrganizationPlugin + pytestmark = pytest.mark.django_db @@ -135,3 +137,34 @@ def test_organization_plugins_run_after_create_name_already_set( name="Magic WOW", registration_id_list=["12345678901234"] ) assert organization.name == "Magic WOW" + + +def test_extract_name_from_org_data_when_commune( + organization_plugins_settings, +): + """Test the name is extracted correctly for a French commune.""" + data = { + "results": [ + { + "nom_complet": "COMMUNE DE VARZY", + "nom_raison_sociale": "COMMUNE DE VARZY", + "siege": { + "libelle_commune": "VARZY", + "liste_enseignes": ["MAIRIE"], + "siret": "21580304000017", + }, + "nature_juridique": "7210", + "matching_etablissements": [ + { + "siret": "21580304000017", + "libelle_commune": "VARZY", + "liste_enseignes": ["MAIRIE"], + } + ], + } + ] + } + + plugin = NameFromSiretOrganizationPlugin() + name = plugin.get_organization_name_from_results(data, "21580304000017") + assert name == "Varzy" diff --git a/src/frontend/apps/desk/src/core/auth/api/types.ts b/src/frontend/apps/desk/src/core/auth/api/types.ts index 54d981f..54227a6 100644 --- a/src/frontend/apps/desk/src/core/auth/api/types.ts +++ b/src/frontend/apps/desk/src/core/auth/api/types.ts @@ -9,6 +9,7 @@ export interface User { id: string; email: string; name?: string; + organization?: Organization; abilities?: { mailboxes: UserAbilities; contacts: UserAbilities; @@ -16,6 +17,12 @@ export interface User { }; } +export interface Organization { + id: string; + name: string; + registration_id_list: [string]; +} + export type UserAbilities = { can_view?: boolean; can_create?: boolean; diff --git a/src/frontend/apps/desk/src/features/header/AccountDropdown.tsx b/src/frontend/apps/desk/src/features/header/AccountDropdown.tsx index 0e489dc..c72c7ee 100644 --- a/src/frontend/apps/desk/src/features/header/AccountDropdown.tsx +++ b/src/frontend/apps/desk/src/features/header/AccountDropdown.tsx @@ -14,7 +14,12 @@ export const AccountDropdown = () => { - {userName} + + {userName} + {userData?.organization?.registration_id_list?.at(0) && ( + {userData?.organization?.name} + )} + diff --git a/src/frontend/apps/e2e/__tests__/app-desk/siret.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/siret.spec.ts index b6b7877..1e9515b 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/siret.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/siret.spec.ts @@ -9,9 +9,6 @@ test.beforeEach(async ({ page, browserName }) => { test.describe('OIDC interop with SIRET', () => { test('it checks the SIRET is displayed in /me endpoint', async ({ page }) => { - const header = page.locator('header').first(); - await expect(header.getByAltText('Marianne Logo')).toBeVisible(); - const response = await page.request.get( 'http://localhost:8071/api/v1.0/users/me/', ); @@ -21,3 +18,16 @@ test.describe('OIDC interop with SIRET', () => { }); }); }); + +test.describe('When a commune, display commune name below user name', () => { + test('it checks the name is added below the user name', async ({ page }) => { + const header = page.locator('header').first(); + await expect(header.getByAltText('Marianne Logo')).toBeVisible(); + + const logout = page.getByRole('button', { + name: 'Marie Delamairie', + }); + + await expect(logout.getByText('Varzy')).toBeVisible(); + }); +}); diff --git a/src/helm/env.d/preprod/values.desk.yaml.gotmpl b/src/helm/env.d/preprod/values.desk.yaml.gotmpl index fb71d0b..92b3e3e 100644 --- a/src/helm/env.d/preprod/values.desk.yaml.gotmpl +++ b/src/helm/env.d/preprod/values.desk.yaml.gotmpl @@ -55,7 +55,7 @@ backend: OIDC_RP_SCOPES: "openid email siret" OIDC_REDIRECT_ALLOWED_HOSTS: https://desk-preprod.beta.numerique.gouv.fr OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}" - ORGANIZATION_PLUGINS: "plugins.organizations.NameFromSiretOrganizationPlugin" + ORGANIZATION_PLUGINS: ["plugins.organizations.NameFromSiretOrganizationPlugin"] ORGANIZATION_REGISTRATION_ID_VALIDATORS: '[{"NAME": "django.core.validators.RegexValidator", "OPTIONS": {"regex": "^[0-9]{14}$"}}]' LOGIN_REDIRECT_URL: https://desk-preprod.beta.numerique.gouv.fr LOGIN_REDIRECT_URL_FAILURE: https://desk-preprod.beta.numerique.gouv.fr diff --git a/src/helm/env.d/production/values.desk.yaml.gotmpl b/src/helm/env.d/production/values.desk.yaml.gotmpl index 54bd8c8..13214a1 100644 --- a/src/helm/env.d/production/values.desk.yaml.gotmpl +++ b/src/helm/env.d/production/values.desk.yaml.gotmpl @@ -44,7 +44,7 @@ backend: OIDC_OP_TOKEN_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/token OIDC_OP_USER_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/userinfo OIDC_OP_LOGOUT_ENDPOINT: https://auth.agentconnect.gouv.fr/api/v2/session/end - ORGANIZATION_PLUGINS: "plugins.organizations.NameFromSiretOrganizationPlugin" + ORGANIZATION_PLUGINS: ["plugins.organizations.NameFromSiretOrganizationPlugin"] OIDC_ORGANIZATION_REGISTRATION_ID_FIELD: "siret" OIDC_RP_CLIENT_ID: secretKeyRef: