(organizations) add siret to name conversion

This adds the plugin system to easily manage
Organization related customizations. This first
plugin tries (best effort) to get a proper name
for the Organization, using its SIRET. This
is French specificities but another plugin can
be defined for other cases.
This commit is contained in:
Quentin BEY
2024-12-05 18:19:11 +01:00
committed by BEY Quentin
parent 6e14c2e61f
commit 38a5f158b5
11 changed files with 277 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Concrete implementation of plugins which can be used or not in the application."""

View File

@@ -0,0 +1,74 @@
"""Organization related plugins."""
import logging
import requests
from core.plugins.base import BaseOrganizationPlugin
logger = logging.getLogger(__name__)
class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin):
"""
This plugin is used to convert the organization registration ID
to the proper name. For French organization the registration ID
is the SIRET.
This is a very specific plugin for French organizations and this
first implementation is very basic. It surely needs to be improved
later.
"""
_api_url = "https://recherche-entreprises.api.gouv.fr/search?q={siret}"
@staticmethod
def _extract_name_from_organization_data(organization_data):
"""Extract the name from the organization data."""
try:
return organization_data["liste_enseignes"][0].title()
except KeyError:
logger.warning("Missing key 'liste_enseignes' in %s", organization_data)
except IndexError:
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
for result in data["results"]:
for organization in result["matching_etablissements"]:
if organization.get("siret") == siret:
return self._extract_name_from_organization_data(organization)
logger.warning("No organization name found for SIRET %s", siret)
return None
def run_after_create(self, organization):
"""After creating an organization, update the organization name."""
if not organization.registration_id_list:
# No registration ID to convert...
return
if organization.name not in organization.registration_id_list:
# The name has probably already been customized
return
# 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:
return
organization.name = name
organization.save(update_fields=["name", "updated_at"])
logger.info("Organization %s name updated to %s", organization, name)

View File

@@ -0,0 +1 @@
"""Tests for the plugins module."""

View File

@@ -0,0 +1 @@
"""Test for the Organization plugins module."""

View File

@@ -0,0 +1,137 @@
"""Tests for the NameFromSiretOrganizationPlugin plugin."""
import pytest
import responses
from core.models import Organization
from core.plugins.loader import get_organization_plugins
pytestmark = pytest.mark.django_db
# disable unused-argument for because organization_plugins_settings
# is used to set the settings not to be used in the test
# pylint: disable=unused-argument
@pytest.fixture(name="organization_plugins_settings")
def organization_plugins_settings_fixture(settings):
"""
Fixture to set the organization plugins settings and
leave the initial state after the test.
"""
_original_plugins = settings.ORGANIZATION_PLUGINS
settings.ORGANIZATION_PLUGINS = [
"plugins.organizations.NameFromSiretOrganizationPlugin"
]
# reset get_organization_plugins cache
get_organization_plugins.cache_clear()
get_organization_plugins() # call to populate the cache
yield
# reset get_organization_plugins cache
settings.ORGANIZATION_PLUGINS = _original_plugins
get_organization_plugins.cache_clear()
get_organization_plugins() # call to populate the cache
@responses.activate
def test_organization_plugins_run_after_create(organization_plugins_settings):
"""Test the run_after_create method of the organization plugins for nominal case."""
responses.add(
responses.GET,
"https://recherche-entreprises.api.gouv.fr/search?q=12345678901234",
json={
"results": [
{
# skipping some fields
"matching_etablissements": [
# skipping some fields
{
"liste_enseignes": ["AMAZING ORGANIZATION"],
"siret": "12345678901234",
}
]
}
],
"total_results": 1,
"page": 1,
"per_page": 10,
"total_pages": 1,
},
status=200,
)
organization = Organization.objects.create(
name="12345678901234", registration_id_list=["12345678901234"]
)
assert organization.name == "Amazing Organization"
# Check that the organization has been updated in the database also
organization.refresh_from_db()
assert organization.name == "Amazing Organization"
@responses.activate
def test_organization_plugins_run_after_create_api_fail(organization_plugins_settings):
"""Test the plugin when the API call fails."""
responses.add(
responses.GET,
"https://recherche-entreprises.api.gouv.fr/search?q=12345678901234",
json={"error": "Internal Server Error"},
status=500,
)
organization = Organization.objects.create(
name="12345678901234", registration_id_list=["12345678901234"]
)
assert organization.name == "12345678901234"
@responses.activate
@pytest.mark.parametrize(
"results",
[
{"results": []},
{"results": [{"matching_etablissements": []}]},
{"results": [{"matching_etablissements": [{"siret": "12345678901234"}]}]},
{
"results": [
{
"matching_etablissements": [
{"siret": "12345678901234", "liste_enseignes": []}
]
}
]
},
],
)
def test_organization_plugins_run_after_create_missing_data(
organization_plugins_settings, results
):
"""Test the plugin when the API call returns missing data."""
responses.add(
responses.GET,
"https://recherche-entreprises.api.gouv.fr/search?q=12345678901234",
json=results,
status=200,
)
organization = Organization.objects.create(
name="12345678901234", registration_id_list=["12345678901234"]
)
assert organization.name == "12345678901234"
@responses.activate
def test_organization_plugins_run_after_create_name_already_set(
organization_plugins_settings,
):
"""Test the plugin does nothing when the name already differs from the registration ID."""
organization = Organization.objects.create(
name="Magic WOW", registration_id_list=["12345678901234"]
)
assert organization.name == "Magic WOW"