From be4d2632ca03f77c97954732e797a0bb7688d78a Mon Sep 17 00:00:00 2001 From: PY Date: Thu, 18 Dec 2025 12:00:31 +0100 Subject: [PATCH 1/4] add user_type and alternative in some event --- envergo/moulinette/tests/test_views_haie.py | 49 +++++++++++++++++++-- envergo/moulinette/views.py | 45 ++++++++++++------- envergo/pages/views.py | 6 +-- envergo/petitions/tests/test_views.py | 9 ++++ envergo/petitions/views.py | 4 ++ envergo/users/models.py | 11 +++++ 6 files changed, 100 insertions(+), 24 deletions(-) diff --git a/envergo/moulinette/tests/test_views_haie.py b/envergo/moulinette/tests/test_views_haie.py index 9562e3b9b7..84f33c0a1d 100644 --- a/envergo/moulinette/tests/test_views_haie.py +++ b/envergo/moulinette/tests/test_views_haie.py @@ -61,6 +61,9 @@ def test_triage(client): assert res.status_code == 200 content = res.content.decode() assert "

Doctrine du département

" in content + assert Event.objects.get( + category="simulateur", event="localisation", metadata__user_type="anonymous" + ) @pytest.mark.urls("config.urls_haie") @@ -83,6 +86,9 @@ def test_triage_result(client): content = res.content.decode() assert "Votre projet n'est pas encore pris en compte par le simulateur" in content assert "

kikoo

" in content + assert Event.objects.get( + category="simulateur", event="soumission_autre", metadata__user_type="anonymous" + ) params = "department=44&element=bosquet&travaux=entretien" full_url = f"{url}?{params}" @@ -158,7 +164,7 @@ def test_debug_result(client): ENVERGO_HAIE_DOMAIN="testserver", ENVERGO_AMENAGEMENT_DOMAIN="otherserver" ) @patch("envergo.hedges.services.get_replantation_coefficient") -def test_result_p_view_with_R_gt_0(mock_R, client): +def test_result_d_view_with_R_gt_0(mock_R, client): DCConfigHaieFactory() hedges = HedgeDataFactory() data = { @@ -179,6 +185,9 @@ def test_result_p_view_with_R_gt_0(mock_R, client): res = client.get(f"{url}?{query}") assert "Déposer une demande sans plantation" not in res.content.decode() + assert Event.objects.get( + category="simulateur", event="soumission_d", metadata__user_type="anonymous" + ) @pytest.mark.urls("config.urls_haie") @@ -186,7 +195,7 @@ def test_result_p_view_with_R_gt_0(mock_R, client): ENVERGO_HAIE_DOMAIN="testserver", ENVERGO_AMENAGEMENT_DOMAIN="otherserver" ) @patch("envergo.hedges.services.get_replantation_coefficient") -def test_result_p_view_with_R_eq_0(mock_R, client): +def test_result_d_view_with_R_eq_0(mock_R, client): DCConfigHaieFactory() hedges = HedgeDataFactory() data = { @@ -214,7 +223,7 @@ def test_result_p_view_with_R_eq_0(mock_R, client): @override_settings( ENVERGO_HAIE_DOMAIN="testserver", ENVERGO_AMENAGEMENT_DOMAIN="otherserver" ) -def test_result_p_view_non_soumis_with_r_gt_0(client): +def test_result_d_view_non_soumis_with_r_gt_0(client): DCConfigHaieFactory() hedge_lt5m = HedgeFactory( latLngs=[ @@ -242,6 +251,36 @@ def test_result_p_view_non_soumis_with_r_gt_0(client): assert "Déposer une demande sans plantation" not in res.content.decode() +@pytest.mark.urls("config.urls_haie") +@override_settings( + ENVERGO_HAIE_DOMAIN="testserver", ENVERGO_AMENAGEMENT_DOMAIN="otherserver" +) +@patch("envergo.hedges.services.get_replantation_coefficient") +def test_result_p_view(mock_R, client): + DCConfigHaieFactory() + hedges = HedgeDataFactory() + data = { + "element": "haie", + "travaux": "destruction", + "motif": "amelioration_culture", + "reimplantation": "remplacement", + "localisation_pac": "oui", + "department": "44", + "haies": hedges.id, + "lineaire_total": 100, + "transfert_parcelles": "non", + "meilleur_emplacement": "non", + } + url = reverse("moulinette_result_plantation") + query = urlencode(data) + mock_R.return_value = 0.0 + client.get(f"{url}?{query}") + + assert Event.objects.get( + category="simulateur", event="soumission_p", metadata__user_type="anonymous" + ) + + @pytest.mark.urls("config.urls_haie") @override_settings( ENVERGO_HAIE_DOMAIN="testserver", ENVERGO_AMENAGEMENT_DOMAIN="otherserver" @@ -260,7 +299,9 @@ def test_moulinette_post_form_error(client): assert res.status_code == 200 assert HOME_TITLE in res.content.decode() assert FORM_ERROR in res.content.decode() - error_event = Event.objects.filter(category="erreur", event="formulaire-simu").get() + error_event = Event.objects.get( + category="erreur", event="formulaire-simu", metadata__user_type="anonymous" + ) assert "errors" in error_event.metadata assert error_event.metadata["errors"] == { "haies": [ diff --git a/envergo/moulinette/views.py b/envergo/moulinette/views.py index ea6079ff61..998adf618d 100644 --- a/envergo/moulinette/views.py +++ b/envergo/moulinette/views.py @@ -24,6 +24,7 @@ from envergo.moulinette.forms import TriageFormHaie from envergo.moulinette.models import ConfigHaie, get_moulinette_class_from_site from envergo.users.mixins import InstructorDepartmentAuthorised +from envergo.users.models import User from envergo.utils.urls import copy_qs, remove_from_qs, remove_mtm_params, update_qs @@ -251,6 +252,10 @@ def get_result_url(self): return url_with_params def log_moulinette_event(self, moulinette, context, **kwargs): + if not moulinette.is_triage_valid(): + super().log_moulinette_event(moulinette, context) + return + export = moulinette.summary() export.update(kwargs) export["url"] = self.request.build_absolute_uri() @@ -268,6 +273,7 @@ def log_moulinette_event(self, moulinette, context, **kwargs): action, self.request, **export, + user_type=User.get_type(self.request.user), ) @@ -323,6 +329,7 @@ def form_invalid(self, form): self.request, data=form.data, errors=form_errors, + user_type=User.get_type(self.request.user), ) return self.render_to_response(context) @@ -518,20 +525,18 @@ def get(self, request, *args, **kwargs): return res def log_moulinette_event(self, moulinette, context): - if moulinette.is_triage_valid(): - super().log_moulinette_event(moulinette, context) - else: - # TODO Why is matomo param cleanup only happens here? - # Matomo parameters are stored in session, but some might remain in the url. - # We need to prevent duplicate values - params = get_matomo_tags(self.request) - params.update(self.request.GET.dict()) - log_event( - "simulateur", - "soumission_autre", - self.request, - **params, - ) + # TODO Why is matomo param cleanup only happens here? + # Matomo parameters are stored in session, but some might remain in the url. + # We need to prevent duplicate values + params = get_matomo_tags(self.request) + params.update(self.request.GET.dict()) + log_event( + "simulateur", + "soumission_autre", + self.request, + **params, + user_type=User.get_type(self.request.user), + ) class MoulinetteAmenagementResult( @@ -648,13 +653,19 @@ def get(self, request, *args, **kwargs): if not self.moulinette.department: return HttpResponseRedirect(reverse("home")) + event_params = { + "department": self.moulinette.department.department, + "user_type": User.get_type(request.user), + } + is_alternative = bool(request.GET.get("alternative", False)) + if is_alternative: + event_params["alternative"] = "true" + log_event( "simulateur", "localisation", self.request, - **{ - "department": self.moulinette.department.department, - }, + **event_params, **get_matomo_tags(self.request), ) return self.render_to_response(self.get_context_data()) diff --git a/envergo/pages/views.py b/envergo/pages/views.py index c9ebc7a5f5..4ce4294942 100644 --- a/envergo/pages/views.py +++ b/envergo/pages/views.py @@ -21,6 +21,7 @@ from envergo.moulinette.models import ConfigAmenagement from envergo.moulinette.views import MoulinetteMixin from envergo.pages.models import NewsItem +from envergo.users.models import User logger = logging.getLogger(__name__) @@ -79,9 +80,8 @@ def post(self, request, *args, **kwargs): "simulateur", "localisation", self.request, - **{ - "department": department.department, - }, + department=department.department, + user_type=User.get_type(request.user), ) return self.render_to_response(context) diff --git a/envergo/petitions/tests/test_views.py b/envergo/petitions/tests/test_views.py index af0845a6b7..b1ed3d9792 100644 --- a/envergo/petitions/tests/test_views.py +++ b/envergo/petitions/tests/test_views.py @@ -201,6 +201,9 @@ def test_petition_project_detail(mock_post, client, site): response = client.get(petition_project_url) assert response.status_code == 200 assert "moulinette" in response.context + assert Event.objects.get( + category="simulateur", event="consultation", metadata__user_type="anonymous" + ) # default PetitionProjectFactory has hedges near Aniane but is declared in department 44 assert response.context["has_hedges_outside_department"] assert "Le projet est hors du département sélectionné" in response.content.decode() @@ -977,6 +980,9 @@ def test_petition_project_alternative(client, haie_user, haie_instructor_44, sit in content ) assert "Partager cette page par email" not in content + assert Event.objects.get( + category="simulateur", event="soumission_d", metadata__alternative="true" + ) # WHEN the user visit the result plantation page of an alternative result_url = alternative_url.replace("/formulaire", "/resultat-plantation") @@ -992,6 +998,9 @@ def test_petition_project_alternative(client, haie_user, haie_instructor_44, sit assert "Partager cette page par email" not in content assert "La demande d'autorisation est prête à être complétée" not in content assert "Copier le lien de cette page" in content + assert Event.objects.get( + category="simulateur", event="soumission_p", metadata__alternative="true" + ) def test_instructor_view_with_hedges_outside_department(client, haie_instructor_44): diff --git a/envergo/petitions/views.py b/envergo/petitions/views.py index a25c31162f..97b523f07b 100644 --- a/envergo/petitions/views.py +++ b/envergo/petitions/views.py @@ -71,6 +71,7 @@ send_message_dossier_ds, update_demarches_simplifiees_status, ) +from envergo.users.models import User from envergo.utils.mattermost import notify from envergo.utils.tools import generate_key from envergo.utils.urls import extract_param_from_url, remove_mtm_params, update_qs @@ -199,6 +200,7 @@ def form_valid(self, form): "creation", self.request, **petition_project.get_log_event_data(), + user_type=User.get_type(self.request.user), **get_matomo_tags(self.request), ) @@ -529,6 +531,7 @@ def get(self, request, *args, **kwargs): "consultation", self.request, **self.object.get_log_event_data(), + user_type=User.get_type(self.request.user), **get_matomo_tags(self.request), ) @@ -780,6 +783,7 @@ def log_event_action(self, request): self.event_action, self.request, **self.get_log_event_data(), + user_type=User.get_type(request.user), **get_matomo_tags(self.request), ) diff --git a/envergo/users/models.py b/envergo/users/models.py index 1793f7f82a..8e8b997815 100644 --- a/envergo/users/models.py +++ b/envergo/users/models.py @@ -74,3 +74,14 @@ class Meta: def __str__(self): return f"{self.name}" + + @classmethod + def get_type(cls, user): + if not user or not user.is_authenticated: + return "anonymous" + if user.is_superuser or user.is_staff: + return "administrator" + elif user.is_instructor: + return "instructor" + else: + return "guest" From 5dda908f5e9e2bcc7c12ac66f1c82f3c8c349923 Mon Sep 17 00:00:00 2001 From: PY Date: Mon, 5 Jan 2026 09:09:43 +0100 Subject: [PATCH 2/4] soumission_autre in the mixin --- envergo/moulinette/views.py | 27 ++++++++++++--------------- envergo/users/models.py | 1 + 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/envergo/moulinette/views.py b/envergo/moulinette/views.py index 998adf618d..a38a0edcf5 100644 --- a/envergo/moulinette/views.py +++ b/envergo/moulinette/views.py @@ -253,7 +253,18 @@ def get_result_url(self): def log_moulinette_event(self, moulinette, context, **kwargs): if not moulinette.is_triage_valid(): - super().log_moulinette_event(moulinette, context) + # TODO Why is matomo param cleanup only happens here? + # Matomo parameters are stored in session, but some might remain in the url. + # We need to prevent duplicate values + params = get_matomo_tags(self.request) + params.update(self.request.GET.dict()) + log_event( + "simulateur", + "soumission_autre", + self.request, + **params, + user_type=User.get_type(self.request.user), + ) return export = moulinette.summary() @@ -524,20 +535,6 @@ def get(self, request, *args, **kwargs): return res - def log_moulinette_event(self, moulinette, context): - # TODO Why is matomo param cleanup only happens here? - # Matomo parameters are stored in session, but some might remain in the url. - # We need to prevent duplicate values - params = get_matomo_tags(self.request) - params.update(self.request.GET.dict()) - log_event( - "simulateur", - "soumission_autre", - self.request, - **params, - user_type=User.get_type(self.request.user), - ) - class MoulinetteAmenagementResult( MoulinetteResultMixin, MoulinetteMixin, BaseMoulinetteResult diff --git a/envergo/users/models.py b/envergo/users/models.py index 8e8b997815..fdc067703e 100644 --- a/envergo/users/models.py +++ b/envergo/users/models.py @@ -77,6 +77,7 @@ def __str__(self): @classmethod def get_type(cls, user): + """Return the type of user as a string depending on its attributes.""" if not user or not user.is_authenticated: return "anonymous" if user.is_superuser or user.is_staff: From 47f512d54181c477af0105dec39e4c5d8e3bbdac Mon Sep 17 00:00:00 2001 From: PY Date: Tue, 6 Jan 2026 12:12:07 +0100 Subject: [PATCH 3/4] move the logic to analytics app --- envergo/analytics/utils.py | 12 ++++++++++++ envergo/moulinette/views.py | 30 ++++++++++-------------------- envergo/pages/views.py | 5 ++--- envergo/petitions/views.py | 8 ++++---- envergo/users/models.py | 12 ------------ 5 files changed, 28 insertions(+), 39 deletions(-) diff --git a/envergo/analytics/utils.py b/envergo/analytics/utils.py index a7d20c1b92..7a7f0076b5 100644 --- a/envergo/analytics/utils.py +++ b/envergo/analytics/utils.py @@ -108,3 +108,15 @@ def update_url_with_matomo_params(url, request): def get_matomo_tags(request): return {k: v for k, v in request.session.items() if k.startswith("mtm_")} + + +def get_user_type(user): + """Return the type of user as a string depending on its attributes.""" + if not user or not user.is_authenticated: + return "anonymous" + if user.is_superuser or user.is_staff: + return "administrator" + elif user.is_instructor: + return "instructor" + else: + return "guest" diff --git a/envergo/moulinette/views.py b/envergo/moulinette/views.py index a38a0edcf5..f670a61b4e 100644 --- a/envergo/moulinette/views.py +++ b/envergo/moulinette/views.py @@ -14,6 +14,7 @@ from envergo.analytics.forms import FeedbackFormUseful, FeedbackFormUseless from envergo.analytics.utils import ( get_matomo_tags, + get_user_type, is_request_from_a_bot, log_event, update_url_with_matomo_params, @@ -24,7 +25,6 @@ from envergo.moulinette.forms import TriageFormHaie from envergo.moulinette.models import ConfigHaie, get_moulinette_class_from_site from envergo.users.mixins import InstructorDepartmentAuthorised -from envergo.users.models import User from envergo.utils.urls import copy_qs, remove_from_qs, remove_mtm_params, update_qs @@ -252,21 +252,6 @@ def get_result_url(self): return url_with_params def log_moulinette_event(self, moulinette, context, **kwargs): - if not moulinette.is_triage_valid(): - # TODO Why is matomo param cleanup only happens here? - # Matomo parameters are stored in session, but some might remain in the url. - # We need to prevent duplicate values - params = get_matomo_tags(self.request) - params.update(self.request.GET.dict()) - log_event( - "simulateur", - "soumission_autre", - self.request, - **params, - user_type=User.get_type(self.request.user), - ) - return - export = moulinette.summary() export.update(kwargs) export["url"] = self.request.build_absolute_uri() @@ -274,7 +259,12 @@ def log_moulinette_event(self, moulinette, context, **kwargs): if self.request.site.domain == settings.ENVERGO_AMENAGEMENT_DOMAIN: action = self.event_action_amenagement else: - action = self.event_action_haie + # if the triage is not valid, we log a "soumission_autre" action + action = ( + self.event_action_haie + if moulinette.is_triage_valid() + else "soumission_autre" + ) mtm_keys = get_matomo_tags(self.request) export.update(mtm_keys) @@ -284,7 +274,7 @@ def log_moulinette_event(self, moulinette, context, **kwargs): action, self.request, **export, - user_type=User.get_type(self.request.user), + user_type=get_user_type(self.request.user), ) @@ -340,7 +330,7 @@ def form_invalid(self, form): self.request, data=form.data, errors=form_errors, - user_type=User.get_type(self.request.user), + user_type=get_user_type(self.request.user), ) return self.render_to_response(context) @@ -652,7 +642,7 @@ def get(self, request, *args, **kwargs): event_params = { "department": self.moulinette.department.department, - "user_type": User.get_type(request.user), + "user_type": get_user_type(request.user), } is_alternative = bool(request.GET.get("alternative", False)) if is_alternative: diff --git a/envergo/pages/views.py b/envergo/pages/views.py index 4ce4294942..28d954df8d 100644 --- a/envergo/pages/views.py +++ b/envergo/pages/views.py @@ -16,12 +16,11 @@ from django.views.generic import FormView, ListView, TemplateView from config.settings.base import GEOMETRICIAN_WEBINAR_FORM_URL -from envergo.analytics.utils import log_event +from envergo.analytics.utils import get_user_type, log_event from envergo.geodata.models import Department from envergo.moulinette.models import ConfigAmenagement from envergo.moulinette.views import MoulinetteMixin from envergo.pages.models import NewsItem -from envergo.users.models import User logger = logging.getLogger(__name__) @@ -81,7 +80,7 @@ def post(self, request, *args, **kwargs): "localisation", self.request, department=department.department, - user_type=User.get_type(request.user), + user_type=get_user_type(request.user), ) return self.render_to_response(context) diff --git a/envergo/petitions/views.py b/envergo/petitions/views.py index 97b523f07b..c613f24c1e 100644 --- a/envergo/petitions/views.py +++ b/envergo/petitions/views.py @@ -34,6 +34,7 @@ from envergo.analytics.utils import ( get_matomo_tags, + get_user_type, log_event, update_url_with_matomo_params, ) @@ -71,7 +72,6 @@ send_message_dossier_ds, update_demarches_simplifiees_status, ) -from envergo.users.models import User from envergo.utils.mattermost import notify from envergo.utils.tools import generate_key from envergo.utils.urls import extract_param_from_url, remove_mtm_params, update_qs @@ -200,7 +200,7 @@ def form_valid(self, form): "creation", self.request, **petition_project.get_log_event_data(), - user_type=User.get_type(self.request.user), + user_type=get_user_type(self.request.user), **get_matomo_tags(self.request), ) @@ -531,7 +531,7 @@ def get(self, request, *args, **kwargs): "consultation", self.request, **self.object.get_log_event_data(), - user_type=User.get_type(self.request.user), + user_type=get_user_type(self.request.user), **get_matomo_tags(self.request), ) @@ -783,7 +783,7 @@ def log_event_action(self, request): self.event_action, self.request, **self.get_log_event_data(), - user_type=User.get_type(request.user), + user_type=get_user_type(request.user), **get_matomo_tags(self.request), ) diff --git a/envergo/users/models.py b/envergo/users/models.py index fdc067703e..1793f7f82a 100644 --- a/envergo/users/models.py +++ b/envergo/users/models.py @@ -74,15 +74,3 @@ class Meta: def __str__(self): return f"{self.name}" - - @classmethod - def get_type(cls, user): - """Return the type of user as a string depending on its attributes.""" - if not user or not user.is_authenticated: - return "anonymous" - if user.is_superuser or user.is_staff: - return "administrator" - elif user.is_instructor: - return "instructor" - else: - return "guest" From 592bea252b8473b2151432e20a7581f7ddfde6ff Mon Sep 17 00:00:00 2001 From: PY Date: Wed, 7 Jan 2026 08:07:47 +0100 Subject: [PATCH 4/4] linter --- .secrets.baseline | 4 +- envergo/moulinette/models.py | 1 - .../instructor_view_alternatives.html | 108 +++++++++--------- 3 files changed, 56 insertions(+), 57 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index f643d41038..daf78062d8 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -194,7 +194,7 @@ "filename": "envergo/evaluations/models.py", "hashed_secret": "f8c6f1ff98c5ee78c27d34a3ca68f35ad79847af", "is_verified": false, - "line_number": 59 + "line_number": 60 } ], "envergo/petitions/demarches_simplifiees/data/fake_dossier.json": [ @@ -287,5 +287,5 @@ } ] }, - "generated_at": "2025-12-16T09:30:33Z" + "generated_at": "2026-01-07T07:05:40Z" } diff --git a/envergo/moulinette/models.py b/envergo/moulinette/models.py index 99b00285f3..870875d5c5 100644 --- a/envergo/moulinette/models.py +++ b/envergo/moulinette/models.py @@ -6,7 +6,6 @@ from operator import attrgetter from typing import Literal -from django.conf import settings from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models.functions import Centroid, Distance from django.contrib.gis.geos import Point diff --git a/envergo/templates/haie/petitions/instructor_view_alternatives.html b/envergo/templates/haie/petitions/instructor_view_alternatives.html index afb87d953c..99301afdda 100644 --- a/envergo/templates/haie/petitions/instructor_view_alternatives.html +++ b/envergo/templates/haie/petitions/instructor_view_alternatives.html @@ -180,63 +180,63 @@

-
  • - - - +
  • + - - - - -
    -
    -
    -
    -
    -

    - - Supprimer cette simulation ? -

    -

    La simulation sera définitivement supprimée.

    -
    - - -
    -
    - -
    + +
    + +
    + + +
    +
    +
    +
    +
    +

    + + Supprimer cette simulation ? +

    +

    La simulation sera définitivement supprimée.

    +
    + + -
    - {% endfor %} + + - {% endblock %} + + + + {% endfor %} - {% block extra_js %} - {{ block.super }} - - - - {% endblock %} +{% endblock %} + +{% block extra_js %} + {{ block.super }} + + + +{% endblock %}