(anct) fetch and display organization names of communes

ANCT-specific extraction of organization names for communes, front
end changes to match.
This commit is contained in:
Laurent Bossavit
2024-12-05 15:44:30 +01:00
committed by Laurent Bossavit
parent d495ef3e19
commit 20cc173e93
10 changed files with 93 additions and 22 deletions

View File

@@ -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

View File

@@ -62,7 +62,7 @@
"username": "e2e.marie",
"email": "marie.varzy@gmail.com",
"firstName": "Marie",
"lastName": "Devarzy",
"lastName": "Delamairie",
"enabled": true,
"attributes": {
"siret": "21580304000017"

View File

@@ -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",

View File

@@ -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

View File

@@ -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"

View File

@@ -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;

View File

@@ -14,7 +14,12 @@ export const AccountDropdown = () => {
<DropButton
button={
<Box $flex $direction="row" $align="center">
<Text $theme="primary">{userName}</Text>
<Box $flex $direction="column" $align="left">
<Text $theme="primary">{userName}</Text>
{userData?.organization?.registration_id_list?.at(0) && (
<Text $theme="primary">{userData?.organization?.name}</Text>
)}
</Box>
<Text className="material-icons" $theme="primary" aria-hidden="true">
arrow_drop_down
</Text>

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -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: