♻️(plugins) rewrite plugin system as django app
This allow more flexibility around the installed plugins, this will allow to add models in plugins if needed.
This commit is contained in:
committed by
Sabrina Demagny
parent
4ced342062
commit
28fdee868d
1
src/backend/plugins/la_suite/__init__.py
Normal file
1
src/backend/plugins/la_suite/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Plugin module for La Suite numérique."""
|
||||
10
src/backend/plugins/la_suite/apps.py
Normal file
10
src/backend/plugins/la_suite/apps.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""La Suite plugin application configuration."""
|
||||
|
||||
from core.plugins.base import BasePluginAppConfig
|
||||
|
||||
|
||||
class LaSuitePluginConfig(BasePluginAppConfig):
|
||||
"""Configuration for the La Suite plugin application."""
|
||||
|
||||
name = "plugins.la_suite"
|
||||
verbose_name = "La Suite Plugin"
|
||||
32
src/backend/plugins/la_suite/hooks.py
Normal file
32
src/backend/plugins/la_suite/hooks.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Hooks registration for the la_suite plugin.
|
||||
|
||||
This module is automagically loaded by the plugin system.
|
||||
Putting hooks registration here allows to test the "utils"
|
||||
function without registering the hook unwillingly.
|
||||
"""
|
||||
|
||||
from core.plugins.registry import register_hook
|
||||
|
||||
from plugins.la_suite.hooks_utils.all_organizations import (
|
||||
get_organization_name_and_metadata_from_siret,
|
||||
)
|
||||
from plugins.la_suite.hooks_utils.communes import CommuneCreation
|
||||
|
||||
|
||||
@register_hook("organization_created")
|
||||
def get_organization_name_and_metadata_from_siret_hook(organization):
|
||||
"""After creating an organization, update the organization name & metadata."""
|
||||
get_organization_name_and_metadata_from_siret(organization)
|
||||
|
||||
|
||||
@register_hook("organization_created")
|
||||
def commune_organization_created(organization):
|
||||
"""After creating an organization, update the organization name."""
|
||||
CommuneCreation().run_after_create(organization)
|
||||
|
||||
|
||||
@register_hook("organization_access_granted")
|
||||
def commune_organization_access_granted(organization_access):
|
||||
"""After granting an organization access, check for needed domain access grant."""
|
||||
CommuneCreation().run_after_grant_access(organization_access)
|
||||
1
src/backend/plugins/la_suite/hooks_utils/__init__.py
Normal file
1
src/backend/plugins/la_suite/hooks_utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Hook modules for La Suite"""
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
This hook 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.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
API_URL = "https://recherche-entreprises.api.gouv.fr/search?q={siret}"
|
||||
|
||||
|
||||
def _get_organization_name_and_metadata_from_results(data, siret):
|
||||
"""Return the organization name and metadata from the results of a SIRET search."""
|
||||
org_metadata = {}
|
||||
for result in data["results"]:
|
||||
for organization in result["matching_etablissements"]:
|
||||
if organization.get("siret") == siret:
|
||||
org_metadata["is_public_service"] = result.get("complements", {}).get(
|
||||
"est_service_public", False
|
||||
)
|
||||
org_metadata["is_commune"] = (
|
||||
str(result.get("nature_juridique", "")) == "7210"
|
||||
)
|
||||
|
||||
store_signs = organization.get("liste_enseignes") or []
|
||||
if store_signs:
|
||||
return store_signs[0].title(), org_metadata
|
||||
if name := result.get("nom_raison_sociale"):
|
||||
return name.title(), org_metadata
|
||||
|
||||
logger.warning("No organization name found for SIRET %s", siret)
|
||||
return None, org_metadata
|
||||
|
||||
|
||||
def get_organization_name_and_metadata_from_siret(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.
|
||||
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(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
|
||||
|
||||
name, metadata = _get_organization_name_and_metadata_from_results(data, siret)
|
||||
if not name: # don't consider metadata either
|
||||
return
|
||||
|
||||
organization.name = name
|
||||
organization.metadata = (organization.metadata or {}) | metadata
|
||||
|
||||
organization.save(update_fields=["name", "metadata", "updated_at"])
|
||||
logger.info("Organization %s name updated to %s", organization, name)
|
||||
@@ -9,92 +9,12 @@ from django.utils.text import slugify
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
from core.plugins.base import BaseOrganizationPlugin
|
||||
|
||||
from mailbox_manager.enums import MailDomainRoleChoices
|
||||
from mailbox_manager.models import MailDomain, MailDomainAccess
|
||||
|
||||
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 get_organization_name_and_metadata_from_results(data, siret):
|
||||
"""Return the organization name and metadata from the results of a SIRET search."""
|
||||
org_metadata = {}
|
||||
for result in data["results"]:
|
||||
for organization in result["matching_etablissements"]:
|
||||
if organization.get("siret") == siret:
|
||||
org_metadata["is_public_service"] = result.get(
|
||||
"complements", {}
|
||||
).get("est_service_public", False)
|
||||
org_metadata["is_commune"] = (
|
||||
str(result.get("nature_juridique", "")) == "7210"
|
||||
)
|
||||
|
||||
store_signs = organization.get("liste_enseignes") or []
|
||||
if store_signs:
|
||||
return store_signs[0].title(), org_metadata
|
||||
if name := result.get("nom_raison_sociale"):
|
||||
return name.title(), org_metadata
|
||||
|
||||
logger.warning("No organization name found for SIRET %s", siret)
|
||||
return None, org_metadata
|
||||
|
||||
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.
|
||||
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()
|
||||
except requests.RequestException as exc:
|
||||
logger.exception("%s: Unable to fetch organization name from SIRET", exc)
|
||||
return
|
||||
|
||||
name, metadata = self.get_organization_name_and_metadata_from_results(
|
||||
data, siret
|
||||
)
|
||||
if not name: # don't consider metadata either
|
||||
return
|
||||
|
||||
organization.name = name
|
||||
organization.metadata = (organization.metadata or {}) | metadata
|
||||
|
||||
organization.save(update_fields=["name", "metadata", "updated_at"])
|
||||
logger.info("Organization %s name updated to %s", organization, name)
|
||||
|
||||
def run_after_grant_access(self, organization_access):
|
||||
"""After granting an organization access, we don't need to do anything."""
|
||||
|
||||
|
||||
class ApiCall:
|
||||
"""Encapsulates a call to an external API"""
|
||||
|
||||
@@ -134,7 +54,7 @@ class ApiCall:
|
||||
)
|
||||
|
||||
|
||||
class CommuneCreation(BaseOrganizationPlugin):
|
||||
class CommuneCreation:
|
||||
"""
|
||||
This plugin handles setup tasks for French communes.
|
||||
"""
|
||||
@@ -6,7 +6,7 @@ from django.test.utils import override_settings
|
||||
import pytest
|
||||
import responses
|
||||
|
||||
from plugins.organizations import ApiCall, CommuneCreation
|
||||
from plugins.la_suite.hooks_utils.communes import ApiCall, CommuneCreation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -4,7 +4,11 @@ import pytest
|
||||
import responses
|
||||
|
||||
from core.models import Organization, get_organization_metadata_schema
|
||||
from core.plugins.loader import get_organization_plugins
|
||||
from core.plugins.registry import registry
|
||||
|
||||
from plugins.la_suite.hooks_utils.all_organizations import (
|
||||
get_organization_name_and_metadata_from_siret,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -14,21 +18,16 @@ pytestmark = pytest.mark.django_db
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
@pytest.fixture(name="organization_plugins_settings")
|
||||
def organization_plugins_settings_fixture(settings):
|
||||
@pytest.fixture(name="hook_settings")
|
||||
def hook_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
|
||||
_original_hooks = dict(registry._hooks.items()) # pylint: disable=protected-access
|
||||
registry.register_hook(
|
||||
"organization_created", get_organization_name_and_metadata_from_siret
|
||||
)
|
||||
|
||||
settings.ORGANIZATION_METADATA_SCHEMA = "fr/organization_metadata.json"
|
||||
|
||||
@@ -38,10 +37,8 @@ def organization_plugins_settings_fixture(settings):
|
||||
|
||||
yield
|
||||
|
||||
# reset get_organization_plugins cache
|
||||
settings.ORGANIZATION_PLUGINS = _original_plugins
|
||||
get_organization_plugins.cache_clear()
|
||||
get_organization_plugins() # call to populate the cache
|
||||
# reset the hooks
|
||||
registry._hooks = _original_hooks # pylint: disable=protected-access
|
||||
|
||||
settings.ORGANIZATION_METADATA_SCHEMA = None
|
||||
|
||||
@@ -61,7 +58,7 @@ def organization_plugins_settings_fixture(settings):
|
||||
],
|
||||
)
|
||||
def test_organization_plugins_run_after_create(
|
||||
organization_plugins_settings, nature_juridique, is_commune, is_public_service
|
||||
hook_settings, nature_juridique, is_commune, is_public_service
|
||||
):
|
||||
"""Test the run_after_create method of the organization plugins for nominal case."""
|
||||
responses.add(
|
||||
@@ -107,7 +104,7 @@ def test_organization_plugins_run_after_create(
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_organization_plugins_run_after_create_api_fail(organization_plugins_settings):
|
||||
def test_organization_plugins_run_after_create_api_fail(hook_settings):
|
||||
"""Test the plugin when the API call fails."""
|
||||
responses.add(
|
||||
responses.GET,
|
||||
@@ -140,9 +137,7 @@ def test_organization_plugins_run_after_create_api_fail(organization_plugins_set
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_organization_plugins_run_after_create_missing_data(
|
||||
organization_plugins_settings, results
|
||||
):
|
||||
def test_organization_plugins_run_after_create_missing_data(hook_settings, results):
|
||||
"""Test the plugin when the API call returns missing data."""
|
||||
responses.add(
|
||||
responses.GET,
|
||||
@@ -159,7 +154,7 @@ def test_organization_plugins_run_after_create_missing_data(
|
||||
|
||||
@responses.activate
|
||||
def test_organization_plugins_run_after_create_name_already_set(
|
||||
organization_plugins_settings,
|
||||
hook_settings,
|
||||
):
|
||||
"""Test the plugin does nothing when the name already differs from the registration ID."""
|
||||
organization = Organization.objects.create(
|
||||
@@ -170,7 +165,7 @@ def test_organization_plugins_run_after_create_name_already_set(
|
||||
|
||||
@responses.activate
|
||||
def test_organization_plugins_run_after_create_no_list_enseignes(
|
||||
organization_plugins_settings,
|
||||
hook_settings,
|
||||
):
|
||||
"""Test the run_after_create method of the organization plugins for nominal case."""
|
||||
responses.add(
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Test module to check all application hooks are loaded."""
|
||||
|
||||
from core.plugins.registry import registry
|
||||
|
||||
from plugins.la_suite.apps import LaSuitePluginConfig
|
||||
|
||||
|
||||
def test_hooks_loaded():
|
||||
"""Test to check all application hooks are loaded."""
|
||||
_original_hooks = dict(registry._hooks.items()) # pylint: disable=protected-access
|
||||
_original_registered_apps = set(registry._registered_apps) # pylint: disable=protected-access
|
||||
|
||||
registry.reset()
|
||||
|
||||
assert registry.get_callbacks("organization_created") == []
|
||||
assert registry.get_callbacks("organization_access_granted") == []
|
||||
|
||||
# Force the application to run "ready" method
|
||||
LaSuitePluginConfig(
|
||||
app_name="plugins.la_suite", app_module=__import__("plugins.la_suite")
|
||||
).ready()
|
||||
|
||||
# Check that the hooks are loaded
|
||||
organization_created_hook_names = [
|
||||
callback.__name__ for callback in registry.get_callbacks("organization_created")
|
||||
]
|
||||
assert organization_created_hook_names == [
|
||||
"get_organization_name_and_metadata_from_siret_hook",
|
||||
"commune_organization_created",
|
||||
]
|
||||
|
||||
organization_access_granted_hook_names = [
|
||||
callback.__name__
|
||||
for callback in registry.get_callbacks("organization_access_granted")
|
||||
]
|
||||
assert organization_access_granted_hook_names == [
|
||||
"commune_organization_access_granted"
|
||||
]
|
||||
|
||||
# cleanup the hooks
|
||||
registry._hooks = _original_hooks # pylint: disable=protected-access
|
||||
registry._registered_apps = _original_registered_apps # pylint: disable=protected-access
|
||||
Reference in New Issue
Block a user