From f0e332a80067c43d2a993e115df3d42fe8ca8ef1 Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 10:08:03 +0100 Subject: [PATCH 1/8] WIP --- conventions/forms/convention_form_add.py | 5 ++ conventions/services/add.py | 28 +++++++++++ .../tests/services/test_add_service.py | 0 conventions/tests/views/test_add_view.py | 0 conventions/urls.py | 18 +++++--- conventions/views/convention_form_add.py | 29 ++++++++---- templates/conventions/select_operation.html | 46 +++++++++++++++++++ 7 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 conventions/forms/convention_form_add.py create mode 100644 conventions/services/add.py create mode 100644 conventions/tests/services/test_add_service.py create mode 100644 conventions/tests/views/test_add_view.py create mode 100644 templates/conventions/select_operation.html diff --git a/conventions/forms/convention_form_add.py b/conventions/forms/convention_form_add.py new file mode 100644 index 000000000..427ddb48b --- /dev/null +++ b/conventions/forms/convention_form_add.py @@ -0,0 +1,5 @@ +from django import forms + + +class ConventionAddForm(forms.Form): + pass diff --git a/conventions/services/add.py b/conventions/services/add.py new file mode 100644 index 000000000..8eb16d181 --- /dev/null +++ b/conventions/services/add.py @@ -0,0 +1,28 @@ +from django.http import HttpRequest + +from conventions.forms.convention_form_add import ConventionAddForm + + +class SelectOperationService: + request: HttpRequest + search_filters: dict[str, str] | None + + def __init__( + self, request: HttpRequest, search_filters: dict[str, str] | None = None + ) -> None: + self.request = request + self.search_filters = search_filters + + +class ConventionAddService: + request: HttpRequest + form: ConventionAddForm + + def __init__(self, request: HttpRequest) -> None: + self.request = request + self.form = None + + def get_form(self) -> ConventionAddForm: + if self.form is None: + self.form = ConventionAddForm() + return self.form diff --git a/conventions/tests/services/test_add_service.py b/conventions/tests/services/test_add_service.py new file mode 100644 index 000000000..e69de29bb diff --git a/conventions/tests/views/test_add_view.py b/conventions/tests/views/test_add_view.py new file mode 100644 index 000000000..e69de29bb diff --git a/conventions/urls.py b/conventions/urls.py index 30d374a5d..05ce20968 100644 --- a/conventions/urls.py +++ b/conventions/urls.py @@ -65,13 +65,11 @@ views.journal, name="journal", ), - # Pages de troisième niveau : funnel d'instruction et d'action sur les conventions et avenants + # Pages pour l'ajout simplifié d'une convention finalisée path( - "new_convention", - permission_required("convention.add_convention")( - views.NewConventionView.as_view() - ), - name="new_convention", + "select_operation", + views.SelectOperationView.as_view(), + name="select_operation", ), path( "add_convention", @@ -80,6 +78,14 @@ ), name="add_convention", ), + # Pages de troisième niveau : funnel d'instruction et d'action sur les conventions et avenants + path( + "new_convention", + permission_required("convention.add_convention")( + views.NewConventionView.as_view() + ), + name="new_convention", + ), path( "bailleur/", views.ConventionBailleurView.as_view(), diff --git a/conventions/views/convention_form_add.py b/conventions/views/convention_form_add.py index ec48a700b..0593ce873 100644 --- a/conventions/views/convention_form_add.py +++ b/conventions/views/convention_form_add.py @@ -1,16 +1,27 @@ +from typing import Any + from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin -from django.shortcuts import render -from django.views import View +from django.views.generic import TemplateView from waffle.mixins import WaffleFlagMixin +from conventions.services.add import ConventionAddService + + +class SelectOperationView(WaffleFlagMixin, LoginRequiredMixin, TemplateView): + waffle_flag = settings.FLAG_ADD_CONVENTION + template_name = "conventions/select_operation.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + ctx = super().get_context_data(**kwargs) + ctx.update({}) + -class AddConventionView(WaffleFlagMixin, LoginRequiredMixin, View): +class AddConventionView(WaffleFlagMixin, LoginRequiredMixin, TemplateView): waffle_flag = settings.FLAG_ADD_CONVENTION + template_name = "conventions/add_convention.html" - def get(self, request): - return render( - request, - "conventions/add_convention.html", - {}, - ) + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + service = ConventionAddService(self.request) + ctx = super().get_context_data(**kwargs) + ctx.update({"form": service.get_form()}) diff --git a/templates/conventions/select_operation.html b/templates/conventions/select_operation.html new file mode 100644 index 000000000..5664db547 --- /dev/null +++ b/templates/conventions/select_operation.html @@ -0,0 +1,46 @@ +{% extends "layout/base.html" %} + +{% load static %} + +{% block page_title %}Sélectionner une opération{% endblock%} + +{% block content %} + +
+
+
+
+

Ajouter une convention existante

+
+
+
+
+
+
+
+
+
Nom de l'opération
+ +
+
+
Numéro de l'opération
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+ +{% endblock %} From bd25e984a35dcb7ce418bbadce97b1dae98d9131 Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 12:30:25 +0100 Subject: [PATCH 2/8] WIP --- conventions/services/add.py | 28 --------------------- conventions/tests/views/test_add_view.py | 0 conventions/urls.py | 2 +- conventions/views/__init__.py | 2 +- conventions/views/convention_form_add.py | 27 -------------------- templates/conventions/add_convention.html | 8 ------ templates/conventions/select_operation.html | 9 +++++-- 7 files changed, 9 insertions(+), 67 deletions(-) delete mode 100644 conventions/services/add.py delete mode 100644 conventions/tests/views/test_add_view.py delete mode 100644 conventions/views/convention_form_add.py delete mode 100644 templates/conventions/add_convention.html diff --git a/conventions/services/add.py b/conventions/services/add.py deleted file mode 100644 index 8eb16d181..000000000 --- a/conventions/services/add.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.http import HttpRequest - -from conventions.forms.convention_form_add import ConventionAddForm - - -class SelectOperationService: - request: HttpRequest - search_filters: dict[str, str] | None - - def __init__( - self, request: HttpRequest, search_filters: dict[str, str] | None = None - ) -> None: - self.request = request - self.search_filters = search_filters - - -class ConventionAddService: - request: HttpRequest - form: ConventionAddForm - - def __init__(self, request: HttpRequest) -> None: - self.request = request - self.form = None - - def get_form(self) -> ConventionAddForm: - if self.form is None: - self.form = ConventionAddForm() - return self.form diff --git a/conventions/tests/views/test_add_view.py b/conventions/tests/views/test_add_view.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/conventions/urls.py b/conventions/urls.py index 05ce20968..2e16c870c 100644 --- a/conventions/urls.py +++ b/conventions/urls.py @@ -74,7 +74,7 @@ path( "add_convention", permission_required("convention.add_convention")( - views.AddConventionView.as_view() + views.AddConventionFromOperationView.as_view() ), name="add_convention", ), diff --git a/conventions/views/__init__.py b/conventions/views/__init__.py index 7c9cdd702..b525205f4 100644 --- a/conventions/views/__init__.py +++ b/conventions/views/__init__.py @@ -1,5 +1,5 @@ from conventions.views.avenants import * -from conventions.views.convention_form_add import * +from conventions.views.convention_form_add_from_operation import * from conventions.views.convention_form_annexes import * from conventions.views.convention_form_bailleur import * from conventions.views.convention_form_cadastre import * diff --git a/conventions/views/convention_form_add.py b/conventions/views/convention_form_add.py deleted file mode 100644 index 0593ce873..000000000 --- a/conventions/views/convention_form_add.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any - -from django.conf import settings -from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic import TemplateView -from waffle.mixins import WaffleFlagMixin - -from conventions.services.add import ConventionAddService - - -class SelectOperationView(WaffleFlagMixin, LoginRequiredMixin, TemplateView): - waffle_flag = settings.FLAG_ADD_CONVENTION - template_name = "conventions/select_operation.html" - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - ctx = super().get_context_data(**kwargs) - ctx.update({}) - - -class AddConventionView(WaffleFlagMixin, LoginRequiredMixin, TemplateView): - waffle_flag = settings.FLAG_ADD_CONVENTION - template_name = "conventions/add_convention.html" - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - service = ConventionAddService(self.request) - ctx = super().get_context_data(**kwargs) - ctx.update({"form": service.get_form()}) diff --git a/templates/conventions/add_convention.html b/templates/conventions/add_convention.html deleted file mode 100644 index d902990a1..000000000 --- a/templates/conventions/add_convention.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "layout/base.html" %} - -{% load static %} - -{% block page_title %}Ajout de convention simplifié - APiLos{% endblock%} - -{% block content %} -{% endblock %} diff --git a/templates/conventions/select_operation.html b/templates/conventions/select_operation.html index 5664db547..46a4bddbf 100644 --- a/templates/conventions/select_operation.html +++ b/templates/conventions/select_operation.html @@ -14,17 +14,18 @@

Ajouter une convention existante

+
Nom de l'opération
- +
Numéro de l'opération
- +
@@ -38,7 +39,11 @@

Ajouter une convention existante

+
+
+ {% include "conventions/select_operation_list.html" %} +
From 928cf7de3a65bbcf615e53c5afb184415ae9f061 Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 16:07:05 +0100 Subject: [PATCH 3/8] WIP --- conventions/services/add_from_operation.py | 67 +++++++++++ .../tests/services/test_add_service.py | 0 conventions/tests/services/test_search.py | 20 +--- .../views/test_add_from_operation_views.py | 10 ++ .../convention_form_add_from_operation.py | 39 +++++++ core/tests/test_utils.py | 10 ++ templates/conventions/add_from_operation.html | 8 ++ templates/conventions/convention_list.html | 2 +- templates/conventions/select_operation.html | 25 ++-- .../conventions/select_operation_list.html | 109 ++++++++++++++++++ 10 files changed, 262 insertions(+), 28 deletions(-) create mode 100644 conventions/services/add_from_operation.py delete mode 100644 conventions/tests/services/test_add_service.py create mode 100644 conventions/tests/views/test_add_from_operation_views.py create mode 100644 conventions/views/convention_form_add_from_operation.py create mode 100644 templates/conventions/add_from_operation.html create mode 100644 templates/conventions/select_operation_list.html diff --git a/conventions/services/add_from_operation.py b/conventions/services/add_from_operation.py new file mode 100644 index 000000000..3ac2c32c6 --- /dev/null +++ b/conventions/services/add_from_operation.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass + +from django.http import HttpRequest + +from conventions.forms.convention_form_add import ConventionAddForm +from siap.exceptions import SIAPException +from siap.siap_client.client import SIAPClient + + +@dataclass +class Operation: + numero: str + nom: str + bailleur: str + nature: str + commune: str + + +class SelectOperationService: + request: HttpRequest + numero_operation: str + + def __init__(self, request: HttpRequest, numero_operation: str) -> None: + self.request = request + self.numero_operation = numero_operation + + def fetch_operations(self) -> tuple[bool, list[Operation]]: + if self.numero_operation is None: + return False, [] + + if operation := self._fetch_siap_operation(): + return True, [operation] + + # TODO: fetch operations from Apilos database + return False, [] + + def _fetch_siap_operation(self) -> Operation | None: + try: + payload = SIAPClient.get_instance().get_operation( + user_login=self.request.user.cerbere_login, + habilitation_id=self.request.session["habilitation_id"], + operation_identifier=self.numero_operation, + ) + except SIAPException: + return None + + return Operation( + nom=payload["donneesOperation"]["nomOperation"], + numero=payload["donneesOperation"]["numeroOperation"], + nature=payload["donneesOperation"]["natureLogement"], + bailleur=payload["gestionnaire"]["code"], + commune=payload["donneesLocalisation"]["adresseComplete"]["commune"], + ) + + +class ConventionAddService: + request: HttpRequest + form: ConventionAddForm + + def __init__(self, request: HttpRequest) -> None: + self.request = request + self.form = None + + def get_form(self) -> ConventionAddForm: + if self.form is None: + self.form = ConventionAddForm() + return self.form diff --git a/conventions/tests/services/test_add_service.py b/conventions/tests/services/test_add_service.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/conventions/tests/services/test_search.py b/conventions/tests/services/test_search.py index 6a519fa6e..498bb4e4a 100644 --- a/conventions/tests/services/test_search.py +++ b/conventions/tests/services/test_search.py @@ -1,4 +1,3 @@ -from django.db import connection from django.test import TestCase from unittest_parametrize import ParametrizedTestCase, param, parametrize @@ -11,22 +10,17 @@ UserConventionTermineesSearchService, ) from conventions.tests.factories import AvenantFactory, ConventionFactory +from core.tests.test_utils import PGTrgmTestMixin from programmes.models.choices import Financement, NatureLogement from programmes.tests.factories import ProgrammeFactory from users.tests.factories import UserFactory -class SearchServiceTestBase(ParametrizedTestCase, TestCase): +class SearchServiceTestBase(PGTrgmTestMixin, ParametrizedTestCase, TestCase): __test__ = False service_class: type - @classmethod - def setUpClass(cls): - super().setUpClass() - with connection.cursor() as cursor: - cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;") - def setUp(self) -> None: self.user = UserFactory(is_staff=True, is_superuser=True) ConventionFactory( @@ -116,13 +110,9 @@ def setUp(self) -> None: Convention.objects.all().update(statut=ConventionStatut.RESILIEE.label) -class TestUserConventionSmartSearchService(ParametrizedTestCase, TestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - with connection.cursor() as cursor: - cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;") - +class TestUserConventionSmartSearchService( + PGTrgmTestMixin, ParametrizedTestCase, TestCase +): def setUp(self) -> None: self.user = UserFactory(is_staff=True, is_superuser=True) diff --git a/conventions/tests/views/test_add_from_operation_views.py b/conventions/tests/views/test_add_from_operation_views.py new file mode 100644 index 000000000..0ff998c2e --- /dev/null +++ b/conventions/tests/views/test_add_from_operation_views.py @@ -0,0 +1,10 @@ +from django.test import TestCase +from django.urls import reverse + + +class TestSelectOperationView(TestCase): + def _login(self): + self.client.post(reverse("login"), {"username": "nicolas", "password": "12345"}) + + def test_search_filters_params(self): + pass diff --git a/conventions/views/convention_form_add_from_operation.py b/conventions/views/convention_form_add_from_operation.py new file mode 100644 index 000000000..e49ebfd5a --- /dev/null +++ b/conventions/views/convention_form_add_from_operation.py @@ -0,0 +1,39 @@ +from typing import Any + +from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import TemplateView +from waffle.mixins import WaffleFlagMixin + +from conventions.services.add_from_operation import ( + ConventionAddService, + SelectOperationService, +) + + +class SelectOperationView(WaffleFlagMixin, LoginRequiredMixin, TemplateView): + waffle_flag = settings.FLAG_ADD_CONVENTION + template_name = "conventions/select_operation.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + numero_operation = self.request.GET.get("numero_operation") + + exact_match, operations = SelectOperationService( + request=self.request, numero_operation=numero_operation + ).fetch_operations() + + return super().get_context_data(**kwargs) | { + "operations": operations, + "siap_assistance_url": settings.SIAP_ASSISTANCE_URL, + "exact_match": exact_match, + } + + +class AddConventionFromOperationView(WaffleFlagMixin, LoginRequiredMixin, TemplateView): + waffle_flag = settings.FLAG_ADD_CONVENTION + template_name = "conventions/add_from_operation.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + service = ConventionAddService(self.request) + ctx = super().get_context_data(**kwargs) + ctx.update({"form": service.get_form()}) diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py index f24e4cd06..821d99a65 100644 --- a/core/tests/test_utils.py +++ b/core/tests/test_utils.py @@ -1,6 +1,8 @@ import unittest import uuid +from django.db import connection + from core.utils import get_key_from_json_field, is_valid_uuid, round_half_up @@ -91,3 +93,11 @@ def test_get_key_from_json_field(self): ), {"myokey": "myovalue"}, ) + + +class PGTrgmTestMixin: + @classmethod + def setUpClass(cls): + super().setUpClass() + with connection.cursor() as cursor: + cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;") diff --git a/templates/conventions/add_from_operation.html b/templates/conventions/add_from_operation.html new file mode 100644 index 000000000..d902990a1 --- /dev/null +++ b/templates/conventions/add_from_operation.html @@ -0,0 +1,8 @@ +{% extends "layout/base.html" %} + +{% load static %} + +{% block page_title %}Ajout de convention simplifié - APiLos{% endblock%} + +{% block content %} +{% endblock %} diff --git a/templates/conventions/convention_list.html b/templates/conventions/convention_list.html index 62d5a14d5..caf9fc869 100644 --- a/templates/conventions/convention_list.html +++ b/templates/conventions/convention_list.html @@ -254,7 +254,7 @@
{% flag "ajout_convention" %} diff --git a/templates/conventions/select_operation.html b/templates/conventions/select_operation.html index 46a4bddbf..94a1c3418 100644 --- a/templates/conventions/select_operation.html +++ b/templates/conventions/select_operation.html @@ -10,27 +10,28 @@
-

Ajouter une convention existante

+

Ajouter une convention existante

+
+
+ {% comment %} TODO: add stepper {% endcomment %} +
+
+
+
Quelle opération est liée à cette convention ?
-
-
-
Nom de l'opération
- -
+
Numéro de l'opération
-
-
-
- @@ -40,8 +41,8 @@

Ajouter une convention existante

-
-
+
+
{% include "conventions/select_operation_list.html" %}
diff --git a/templates/conventions/select_operation_list.html b/templates/conventions/select_operation_list.html new file mode 100644 index 000000000..eabfab4dd --- /dev/null +++ b/templates/conventions/select_operation_list.html @@ -0,0 +1,109 @@ + +{% if operations %} + +
+ +
+
+
{{ operations|length }} résultat(s)
+ + + Réinitaliser la recherche + +
+
+ + + + + + + + + + + {% for operation in operations %} + + + + + + + + + {% endfor %} + +
+ Numéro de l'opération + + Nom de l'opération + + Bailleur + + Nature de l'opération + + Commune + +
{{ operation.numero }}{{ operation.nom }}{{ operation.bailleur }}{{ operation.nature }}{{ operation.commune }} + +
+
+ +
+ {% if exact_match %} +
+ {% else %} +
+
Vous ne trouvez pas l'opération liée ?
+
+ Vérifiez que vous avez saisi un numéro d'opération correct.
+ Sinon, peut-être que l'opération que vous recherchez n'est pas référencée dans le SIAP / Apilos. + Dans ce cas, nous vous invitons à contacter notre équipe support. +
+
+
+ {% if siap_assistance_url %} + + {% endif %} +
+
+
+ not_found + {% endif %} +
+ +{% else %} + + {% if request.GET.numero_operation %} +
+
+
Aucune opération trouvée
+
+ L'opération portant le numéro {{ request.GET.numero_operation }} n'est pas référencée dans le SIAP / Apilos.
+ Vérifiez que le numéro est correct, ou contactez notre équipe support. +
+
+
+ {% if siap_assistance_url %} + + {% endif %} +
+
+
+ not_found +
+ {% endif %} + + +{% endif %} + From c6ce01c6956a480f826e8de5c6d00da49cd06249 Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 17:16:47 +0100 Subject: [PATCH 4/8] WIP --- conventions/services/add_from_operation.py | 67 ++++++++++++++++++---- users/models.py | 2 +- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/conventions/services/add_from_operation.py b/conventions/services/add_from_operation.py index 3ac2c32c6..a537ec36d 100644 --- a/conventions/services/add_from_operation.py +++ b/conventions/services/add_from_operation.py @@ -1,8 +1,12 @@ from dataclasses import dataclass +from django.contrib.postgres.search import TrigramSimilarity +from django.db.models import QuerySet, Value +from django.db.models.functions import Replace from django.http import HttpRequest from conventions.forms.convention_form_add import ConventionAddForm +from programmes.models import NatureLogement, Programme from siap.exceptions import SIAPException from siap.siap_client.client import SIAPClient @@ -15,6 +19,26 @@ class Operation: nature: str commune: str + @classmethod + def from_siap_payload(cls, payload: dict[str, str]) -> "Operation": + return cls( + nom=payload["donneesOperation"]["nomOperation"], + numero=payload["donneesOperation"]["numeroOperation"], + nature=payload["donneesOperation"]["natureLogement"], + bailleur=payload["gestionnaire"]["code"], + commune=payload["donneesLocalisation"]["adresseComplete"]["commune"], + ) + + @classmethod + def from_apilos_programme(cls, programme: Programme) -> "Operation": + return cls( + numero=programme.numero_galion, + nom=programme.nom, + bailleur=programme.bailleur.nom, + nature=NatureLogement[programme.nature_logement].label, + commune=programme.ville, + ) + class SelectOperationService: request: HttpRequest @@ -31,26 +55,45 @@ def fetch_operations(self) -> tuple[bool, list[Operation]]: if operation := self._fetch_siap_operation(): return True, [operation] - # TODO: fetch operations from Apilos database - return False, [] + if operation := self._get_apilos_operation(): + return True, [operation] + + return False, self._get_nearby_apilos_operations() def _fetch_siap_operation(self) -> Operation | None: try: - payload = SIAPClient.get_instance().get_operation( - user_login=self.request.user.cerbere_login, - habilitation_id=self.request.session["habilitation_id"], - operation_identifier=self.numero_operation, + return Operation.from_siap_payload( + payload=SIAPClient.get_instance().get_operation( + user_login=self.request.user.cerbere_login, + habilitation_id=self.request.session["habilitation_id"], + operation_identifier=self.numero_operation, + ) ) except SIAPException: return None - return Operation( - nom=payload["donneesOperation"]["nomOperation"], - numero=payload["donneesOperation"]["numeroOperation"], - nature=payload["donneesOperation"]["natureLogement"], - bailleur=payload["gestionnaire"]["code"], - commune=payload["donneesLocalisation"]["adresseComplete"]["commune"], + def _user_programmes(self) -> QuerySet[Programme]: + return self.request.user.programmes() + + def _get_apilos_operation(self) -> Operation | None: + qs = self._user_programmes().filter(numero_galion=self.numero_operation) + if qs.count() == 1: + return Operation.from_apilos_programme(programme=qs.first()) + return None + + def _get_nearby_apilos_operations(self) -> list[Operation]: + qs = ( + self._user_programmes() + .annotate( + numero_operation_trgrm=TrigramSimilarity( + Replace(Replace("numero_galion", Value("/")), Value("-")), + self.numero_operation.replace("-", "").replace("/", ""), + ) + ) + .filter(numero_operation_trgrm__gt=0.5) + .order_by("-numero_operation_trgrm") ) + return [Operation.from_apilos_programme(programme=p) for p in qs[:10]] class ConventionAddService: diff --git a/users/models.py b/users/models.py index 6bee9972a..5b71ac074 100644 --- a/users/models.py +++ b/users/models.py @@ -208,7 +208,7 @@ def is_administration(self): def _is_role(self, role): return role in map(lambda r: r.typologie, self.roles.all()) - def programmes(self) -> list: + def programmes(self) -> QuerySet[Programme]: """ Programme of the user following is role : * super admin = all programme, filtre = {} From 69d637ad613e79c079bad9c02901f9ed89cac284 Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 17:31:27 +0100 Subject: [PATCH 5/8] WIP --- siap/siap_client/client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/siap/siap_client/client.py b/siap/siap_client/client.py index 72094685d..a7521dc51 100644 --- a/siap/siap_client/client.py +++ b/siap/siap_client/client.py @@ -275,6 +275,12 @@ def get_menu(self, user_login: str, habilitation_id: int = 0) -> dict: def get_operation( self, user_login: str, habilitation_id: int, operation_identifier: str ) -> dict: + if ( + operation_identifier + and operation_mock["donneesOperation"]["numeroOperation"] + != operation_identifier + ): + raise SIAPException("Operation not found") return operation_mock def get_fusion( From 4418ab9d652592602ad3e375ea4d2b789c890f47 Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 17:44:23 +0100 Subject: [PATCH 6/8] fixinf tests --- conventions/services/add_from_operation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/conventions/services/add_from_operation.py b/conventions/services/add_from_operation.py index a537ec36d..b809ccaa7 100644 --- a/conventions/services/add_from_operation.py +++ b/conventions/services/add_from_operation.py @@ -1,5 +1,6 @@ from dataclasses import dataclass +from django.conf import settings from django.contrib.postgres.search import TrigramSimilarity from django.db.models import QuerySet, Value from django.db.models.functions import Replace @@ -90,8 +91,8 @@ def _get_nearby_apilos_operations(self) -> list[Operation]: self.numero_operation.replace("-", "").replace("/", ""), ) ) - .filter(numero_operation_trgrm__gt=0.5) - .order_by("-numero_operation_trgrm") + .filter(numero_operation_trgrm__gt=settings.TRIGRAM_SIMILARITY_THRESHOLD) + .order_by("-numero_operation_trgrm", "-cree_le") ) return [Operation.from_apilos_programme(programme=p) for p in qs[:10]] From 108099e1df9936daf604702d4b6e164325db9a90 Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 17:49:41 +0100 Subject: [PATCH 7/8] cleanup --- .../tests/views/test_add_from_operation_views.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 conventions/tests/views/test_add_from_operation_views.py diff --git a/conventions/tests/views/test_add_from_operation_views.py b/conventions/tests/views/test_add_from_operation_views.py deleted file mode 100644 index 0ff998c2e..000000000 --- a/conventions/tests/views/test_add_from_operation_views.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.test import TestCase -from django.urls import reverse - - -class TestSelectOperationView(TestCase): - def _login(self): - self.client.post(reverse("login"), {"username": "nicolas", "password": "12345"}) - - def test_search_filters_params(self): - pass From 7041ae30712bcb5762b21cdfb3f80f94dfb95e0f Mon Sep 17 00:00:00 2001 From: Etchegoyen Matthieu Date: Wed, 28 Feb 2024 17:59:41 +0100 Subject: [PATCH 8/8] adding tests --- .../tests/services/test_add_from_operation.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 conventions/tests/services/test_add_from_operation.py diff --git a/conventions/tests/services/test_add_from_operation.py b/conventions/tests/services/test_add_from_operation.py new file mode 100644 index 000000000..dbdca8733 --- /dev/null +++ b/conventions/tests/services/test_add_from_operation.py @@ -0,0 +1,108 @@ +from django.test import TestCase, override_settings +from django.test.client import RequestFactory +from unittest_parametrize import ParametrizedTestCase, param, parametrize + +from conventions.services.add_from_operation import ( + ConventionAddService, + Operation, + SelectOperationService, +) +from core.tests.test_utils import PGTrgmTestMixin +from programmes.models import NatureLogement +from programmes.tests.factories import ProgrammeFactory +from users.tests.factories import UserFactory + + +@override_settings(USE_MOCKED_SIAP_CLIENT=True) +class TestSelectOperationService(PGTrgmTestMixin, ParametrizedTestCase, TestCase): + def setUp(self): + self.request = RequestFactory() + self.request.user = UserFactory(cerbere=True, is_superuser=True) + self.request.session = {"habilitation_id": 5} + + ProgrammeFactory( + uuid="67062edc-3ee8-4262-965f-98f885d418f4", + numero_galion="2017DD01100057", + nom="Programme 1", + nature_logement=NatureLogement.LOGEMENTSORDINAIRES, + ville="Bayonne", + bailleur__nom="Bailleur A", + ) + ProgrammeFactory( + uuid="7fb89bd6-62f8-4c06-b15a-4fc81bc02995", + numero_galion="2017DD01201254", + nom="Programme 3", + nature_logement=NatureLogement.RESISDENCESOCIALE, + ville="L'Isle-sur-la-Sorgue", + bailleur__nom="Bailleur B", + ) + + @parametrize( + "numero_operation, expected_matching, expected_operations", + [ + param( + "20220600006", + True, + [ + Operation( + numero="20220600006", + nom="Programme 2", + bailleur="13055", + nature="LOO", + commune="Marseille", + ) + ], + id="siap_match_exact", + ), + param( + "2017DD01100057", + True, + [ + Operation( + numero="2017DD01100057", + nom="Programme 1", + bailleur="Bailleur A", + nature="Logements ordinaires", + commune="Bayonne", + ) + ], + id="apilos_match_exact", + ), + param( + "2017DD01", + False, + [ + Operation( + numero="2017DD01201254", + nom="Programme 3", + bailleur="Bailleur B", + nature="Résidence sociale", + commune="L'Isle-sur-la-Sorgue", + ), + Operation( + numero="2017DD01100057", + nom="Programme 1", + bailleur="Bailleur A", + nature="Logements ordinaires", + commune="Bayonne", + ), + ], + id="apilos_search_trgrm", + ), + ], + ) + def test_fetch_operations( + self, numero_operation, expected_matching, expected_operations + ): + service = SelectOperationService( + request=self.request, numero_operation=numero_operation + ) + exact_match, operations = service.fetch_operations() + assert exact_match == expected_matching + assert operations == expected_operations + + +class TestConventionAddService(TestCase): + def basic_test(self): + service = ConventionAddService(request=RequestFactory()) + assert service.get_form() is not None