Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into upgrade-sirene-import
Browse files Browse the repository at this point in the history
  • Loading branch information
ikarius authored Oct 9, 2024
2 parents b4d45e6 + b78bafe commit 408f930
Show file tree
Hide file tree
Showing 46 changed files with 789 additions and 171 deletions.
6 changes: 4 additions & 2 deletions config/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@

# Nécessaire pour la C.I. : fixe des valeurs par défaut pour les conteneurs
# faire correspondre les valeurs définies dans la configuration de la CI
CORS_ALLOWED_ORIGIN_REGEXES = [os.getenv("DJANGO_CORS_ALLOWED_ORIGIN_REGEXES","*")]
CORS_ALLOWED_ORIGIN_REGEXES = [os.getenv("DJANGO_CORS_ALLOWED_ORIGIN_REGEXES", "*")]
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000")
IC_TOKEN_URL = os.getenv("IC_TOKEN_URL", "https://whatever-oidc-token-url.com")
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME", "dora")

SIB_ONBOARDING_LIST = os.getenv("SIB_ONBOARDING_LIST", "1")
SIB_ONBOARDING_PUTATIVE_MEMBER_LIST = os.getenv("SIB_ONBOARDING_PUTATIVE_MEMBER_LIST", "2")
SIB_ONBOARDING_MEMBER_LIST = os.getenv("SIB_ONBOARDING_MEMBER_LIST", "3")
7 changes: 0 additions & 7 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import dora.structures.views
import dora.support.views
import dora.users.views
from dora.data_inclusion.client import di_client_factory
from dora.oidc.urls import oidc_patterns

from .url_converters import InseeCodeConverter, SiretConverter
Expand Down Expand Up @@ -66,26 +65,20 @@
register_converter(SiretConverter, "siret")


# injection conditionnelle du client D·I : voir conftest.py
di_client = di_client_factory()

private_api_patterns = [
path("auth/", include("dora.rest_auth.urls")),
path(
"search/",
dora.services.views.search,
{"di_client": di_client},
),
path("stats/event/", dora.stats.views.log_event),
path(
"services-di/<slug:di_id>/",
dora.services.views.service_di,
{"di_client": di_client},
),
path(
"services-di/<slug:di_id>/share/",
dora.services.views.share_di_service,
{"di_client": di_client},
),
path("admin-division-search/", dora.admin_express.views.search),
path("admin-division-reverse-search/", dora.admin_express.views.reverse_search),
Expand Down
2 changes: 1 addition & 1 deletion dora/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def patch_di_client():
# Remplace le client D·I par défaut :
# permet de s'affranchir de `settings.IS_TESTING` pour la plupart des cas.
# Chaque test peut par la suite choisir son instance de client (fake).
with patch("dora.data_inclusion.client.di_client_factory") as mocked_di_client:
with patch("dora.data_inclusion.di_client_factory") as mocked_di_client:
mocked_di_client.return_value = None
yield

Expand Down
9 changes: 8 additions & 1 deletion dora/data_inclusion/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import requests
from django.conf import settings

from .constants import THEMATIQUES_MAPPING_DORA_TO_DI

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -120,7 +122,12 @@ def search_services(
url.args["code_insee"] = code_insee

if thematiques is not None:
url.args["thematiques"] = thematiques
enriched_thematiques = []
for thematique in thematiques:
enriched_thematiques += THEMATIQUES_MAPPING_DORA_TO_DI.get(
thematique, [thematique]
)
url.args["thematiques"] = enriched_thematiques

if types is not None:
url.args["types"] = types
Expand Down
15 changes: 15 additions & 0 deletions dora/data_inclusion/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from collections import defaultdict

# À une thématique DI correspond une thématique Dora
THEMATIQUES_MAPPING_DI_TO_DORA = {
"logement-hebergement--etre-accompagne-dans-son-projet-accession": "logement-hebergement--etre-accompagne-pour-se-loger",
"logement-hebergement--etre-accompagne-en cas-de-difficultes-financieres": "logement-hebergement--gerer-son-budget",
"logement-hebergement--financer-son-projet-travaux": "logement-hebergement--autre",
}

# Inversion du dictionnaire
# À une thématique Dora correspond une liste de thématiques DI
THEMATIQUES_MAPPING_DORA_TO_DI = defaultdict(list)
for key, value in THEMATIQUES_MAPPING_DI_TO_DORA.items():
if not value.endswith("--autre"):
THEMATIQUES_MAPPING_DORA_TO_DI[value].append(key)
14 changes: 8 additions & 6 deletions dora/data_inclusion/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
get_update_status,
)

from .constants import THEMATIQUES_MAPPING_DI_TO_DORA

DI_TO_DORA_DIFFUSION_ZONE_TYPE_MAPPING = {
"commune": "city",
"epci": "epci",
Expand Down Expand Up @@ -114,12 +116,12 @@ def map_service(service_data: dict, is_authenticated: bool) -> dict:
categories = None
subcategories = None
if service_data["thematiques"] is not None:
categories = ServiceCategory.objects.filter(
value__in=service_data["thematiques"]
)
subcategories = ServiceSubCategory.objects.filter(
value__in=service_data["thematiques"]
)
thematiques = [
THEMATIQUES_MAPPING_DI_TO_DORA.get(thematique, thematique)
for thematique in service_data["thematiques"]
]
categories = ServiceCategory.objects.filter(value__in=thematiques)
subcategories = ServiceSubCategory.objects.filter(value__in=thematiques)

location_kinds = None
if service_data["modes_accueil"] is not None:
Expand Down
9 changes: 8 additions & 1 deletion dora/data_inclusion/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Optional
from uuid import uuid4

from .constants import THEMATIQUES_MAPPING_DORA_TO_DI


def make_di_service_data(**kwargs) -> dict:
return {
Expand Down Expand Up @@ -99,13 +101,18 @@ def search_services(
services = [r for r in services if r["source"] in sources]

if thematiques is not None:
enriched_thematiques = []
for thematique in thematiques:
enriched_thematiques += THEMATIQUES_MAPPING_DORA_TO_DI.get(
thematique, [thematique]
)
services = [
r
for r in services
if any(
t.startswith(requested_thematique)
for t in r["thematiques"]
for requested_thematique in thematiques
for requested_thematique in enriched_thematiques
)
]

Expand Down
59 changes: 26 additions & 33 deletions dora/data_inclusion/tests.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,36 @@
import unittest
from .constants import THEMATIQUES_MAPPING_DI_TO_DORA, THEMATIQUES_MAPPING_DORA_TO_DI
from .mappings import map_service
from .test_utils import FakeDataInclusionClient, make_di_service_data

from django.conf import settings
from rest_framework.test import APITestCase

from dora import data_inclusion
def test_map_service_thematiques_mapping():
input_thematiques = [
"logement-hebergement",
"logement-hebergement--connaissance-de-ses-droits-et-interlocuteurs",
"logement-hebergement--besoin-dadapter-mon-logement",
] + list(THEMATIQUES_MAPPING_DI_TO_DORA.keys())

expected_categories = ["logement-hebergement"]
expected_subcategories = [
"logement-hebergement--connaissance-de-ses-droits-et-interlocuteurs",
"logement-hebergement--besoin-dadapter-mon-logement",
] + list(THEMATIQUES_MAPPING_DI_TO_DORA.values())

class DataInclusionIntegrationTestCase(APITestCase):
"""These integration-level tests check the connection to data.inclusion.
di_service_data = make_di_service_data(thematiques=input_thematiques)
service = map_service(di_service_data, False)

They depend on the data.inclusion api and should not be run
systematically, because of their inherent high cost and instability.
"""
assert sorted(service["categories"]) == sorted(expected_categories)
assert sorted(service["subcategories"]) == sorted(expected_subcategories)

def setUp(self):
self.di_client = data_inclusion.di_client_factory()

@unittest.skipIf(
settings.SKIP_DI_INTEGRATION_TESTS, "data.inclusion api not available"
)
def test_search_services(self):
self.di_client.search_services(
code_insee="91223",
thematiques=["mobilite--comprendre-et-utiliser-les-transports-en-commun"],
)
def test_di_client_search_thematiques_mapping():
input_thematique = list(THEMATIQUES_MAPPING_DORA_TO_DI.keys())[0]
output_thematique = list(THEMATIQUES_MAPPING_DORA_TO_DI.values())[0][0]

@unittest.skipIf(
settings.SKIP_DI_INTEGRATION_TESTS, "data.inclusion api not available"
)
def test_list_services(self):
self.di_client.list_services(source="dora")
di_client = FakeDataInclusionClient()
di_service_data = make_di_service_data(thematiques=[output_thematique])
di_client.services.append(di_service_data)

@unittest.skipIf(
settings.SKIP_DI_INTEGRATION_TESTS, "data.inclusion api not available"
)
def test_retrieve_service(self):
services = self.di_client.list_services(source="dora")
results = di_client.search_services(thematiques=[input_thematique])

self.di_client.retrieve_service(
source="dora",
id=services[0]["id"],
)
assert len(results) == 1
29 changes: 29 additions & 0 deletions dora/onboarding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,29 @@ def _add_user_to_sib_list(
return True


def _remove_from_sib_list(
client: sib_api.ContactsApi, user: User, sib_list_id: int
) -> bool:
# retire un utilisateur donné d'une liste SiB
try:
client.remove_contact_from_list(
sib_list_id, sib_api.RemoveContactFromList(emails=[user.email])
)
logger.info(
"L'utilisateur #%s a été retiré de la liste SiB: %s", user.pk, sib_list_id
)
except SibApiException as exc:
logger.exception(exc)
logger.error(
"Impossible de retirer l'utilisateur #%s de la liste SiB: %s",
user.pk,
sib_list_id,
)
return False

return True


def _create_sib_contact(
client: sib_api.ContactsApi, user: User, attributes: dict, sib_list_id: int
) -> bool:
Expand Down Expand Up @@ -252,3 +275,9 @@ def onboard_user(user: User, structure: Structure):

# création ou maj du contact SiB
_create_or_update_sib_contact(client, user, attributes, sib_list_id)

# dans le cas d'un utilisateur passé membre, le retirer de la liste des invités
if sib_list_id == int(settings.SIB_ONBOARDING_MEMBER_LIST):
_remove_from_sib_list(
client, user, int(settings.SIB_ONBOARDING_PUTATIVE_MEMBER_LIST)
)
123 changes: 123 additions & 0 deletions dora/onboarding/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from unittest.mock import Mock, patch

import pytest
from django.conf import settings
from django.urls import reverse

from dora.core.test_utils import make_structure, make_user
from dora.users.enums import MainActivity


@pytest.mark.parametrize(
"main_activity,expected_sib_list",
[
(MainActivity.OFFREUR, settings.SIB_ONBOARDING_LIST),
(MainActivity.ACCOMPAGNATEUR, settings.SIB_ONBOARDING_PUTATIVE_MEMBER_LIST),
(
MainActivity.ACCOMPAGNATEUR_OFFREUR,
settings.SIB_ONBOARDING_PUTATIVE_MEMBER_LIST,
),
],
)
@patch("dora.onboarding._create_or_update_sib_contact")
@patch("dora.onboarding._setup_sib_client", Mock(return_value=True))
def test_onboard_other_activities(
mock_create_contact, main_activity, expected_sib_list, api_client
):
# Les utilisateurs ayant offreurs ou autre pour activité principale
# sont redirigés vers l'ancienne liste Brevo (onboarding "traditionnel").

# Les utilisateurs accompagnateurs ou accompagnateurs/offreurs
# sont "onboardés" sur la bonne liste Brevo des invités lors de leur première invitation.

# note : le deuxième patch n'est pas pris en compte comme un paramètre du test
# (à cause de l'association directe/explicite à un nouveau mock)

structure = make_structure()
# La création d'un admin de la structure est nécessaire pour que l'utilisateur
# soit rattaché en tant qu'invité (sinon il en devient le premier membre et admin).
make_user(structure=structure, is_admin=True)
invited_user = make_user(main_activity=main_activity)

api_client.force_authenticate(user=invited_user)
api_client.post(
reverse("join-structure"),
data={
# Utiliser le slug, car le SIRET sera invalide (random).
"structure_slug": structure.slug,
"cgu_version": "1",
},
)

assert (
invited_user in structure.putative_members.all()
), "L'utilisateur n'est pas un invité de la structure"
assert mock_create_contact.called, "Le contact Brevo n'a pas été créé"

_, user, attrs, sib_list = mock_create_contact.call_args.args

assert user == invited_user, "L'utilisateur ne correspond pas"
assert attrs, "Les attributs Brevo ne sont pas définis"
assert (
str(sib_list) == expected_sib_list
), "L'utilisateur n'est pas rattaché à la bonne liste Brevo"


@pytest.mark.parametrize(
"main_activity,expected_sib_list",
[
(MainActivity.ACCOMPAGNATEUR, settings.SIB_ONBOARDING_MEMBER_LIST),
(MainActivity.ACCOMPAGNATEUR_OFFREUR, settings.SIB_ONBOARDING_MEMBER_LIST),
],
)
@patch("dora.onboarding._remove_from_sib_list")
@patch("dora.onboarding._create_or_update_sib_contact")
@patch("dora.onboarding._setup_sib_client", Mock(return_value=True))
def test_onboard_new_member(
mock_create_contact,
mock_remove_from_list,
main_activity,
expected_sib_list,
api_client,
):
# Les utilisateurs accompagnateurs ou accompagnateurs/offreurs
# sont "onboardés" sur la liste Brevo des membres lors de leur premier rattachement à une structure.

member = make_user(main_activity=main_activity)
structure = make_structure(putative_member=member)
admin = make_user(structure=structure, is_admin=True)

# Cette action doit-être effectuée par un admin de la structure.
api_client.force_authenticate(user=admin)

# L'utilisateur accepte l'invitation.
r = api_client.post(
reverse(
"structure-putative-member-accept-membership-request",
kwargs={"pk": structure.putative_membership.first().pk},
),
)

# Etant différent du traditionnel 200, on teste le statut de retour.
assert 201 == r.status_code, "Code de status invalide (201 attendu)"

assert (
member in structure.members.all()
), "L'utilisateur n'est pas membre de la structure"
assert mock_create_contact.called, "Le contact Brevo n'a pas été créé"

_, user, attrs, sib_list = mock_create_contact.call_args.args

assert user == member, "L'utilisateur ne correspond pas"
assert attrs, "Les attributs Brevo ne sont pas définis"
assert (
str(sib_list) == expected_sib_list
), "L'utilisateur n'est pas rattaché à la bonne liste Brevo"

# On retire un utilisateur de la liste Brevo "invité" après qu'il soit devenu membre.
assert (
mock_remove_from_list.called
), "Pas de retrait de l'utilisateur de la liste Brevo des invités"
mock_remove_from_list.assert_called_with(
True, user, int(settings.SIB_ONBOARDING_PUTATIVE_MEMBER_LIST)
)
8 changes: 4 additions & 4 deletions dora/rest_auth/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from . import views

urlpatterns = [
path("user-info/", views.user_info),
path("join-structure/", views.join_structure),
path("invite-first-admin/", views.invite_first_admin),
path("accept-cgu/", views.accept_cgu),
path("user-info/", views.user_info, name="user-info"),
path("join-structure/", views.join_structure, name="join-structure"),
path("invite-first-admin/", views.invite_first_admin, name="invite-first-admin"),
path("accept-cgu/", views.accept_cgu, name="accept-cgu"),
]
Loading

0 comments on commit 408f930

Please sign in to comment.