Skip to content

Commit

Permalink
Merge pull request #463 from MTES-MCT/feature/multi-sage-results
Browse files Browse the repository at this point in the history
Anomalie cadre rose quand deux SAGE
  • Loading branch information
pyDez authored Nov 12, 2024
2 parents ace5cb9 + 4b7d680 commit 195e261
Show file tree
Hide file tree
Showing 18 changed files with 414 additions and 250 deletions.
5 changes: 3 additions & 2 deletions envergo/evaluations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,9 @@ def get_bcc_recipients(self):
"action_requise",
)
):
perimeters = self.moulinette.sage.perimeters.all()
for perimeter in perimeters:
for perimeter, result in self.moulinette.sage.results_by_perimeter.items():
if result not in ("interdit", "soumis", "action_requise"):
continue
if perimeter.contact_email:
bcc_recipients.append(perimeter.contact_email)
else:
Expand Down
58 changes: 54 additions & 4 deletions envergo/evaluations/tests/test_eval_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ def moulinette_url(footprint):
return f"https://envergo.beta.gouv.fr?{url}"


def fake_moulinette(url, lse, n2000, evalenv, sage, **eval_kwargs):
def fake_moulinette(
url, lse, n2000, evalenv, sage, sage_results_by_perimeter=None, **eval_kwargs
):
"""Create a moulinette with custom regulation results."""

eval_params = {
Expand All @@ -79,6 +81,7 @@ def fake_moulinette(url, lse, n2000, evalenv, sage, **eval_kwargs):

# We create mocks based on a real regulation, so it's easier to fake results
regulation = RegulationFactory()
sage_perimeter = Mock(contact_email="[email protected]")
moulinette.regulations = [
Mock(
regulation,
Expand All @@ -105,11 +108,14 @@ def fake_moulinette(url, lse, n2000, evalenv, sage, **eval_kwargs):
regulation,
wraps=regulation,
result=sage,
perimeters=Mock(
all=MagicMock(return_value=[Mock(contact_email="[email protected]")])
),
perimeters=Mock(all=MagicMock(return_value=[sage_perimeter])),
slug="sage",
do_not_call_in_templates=True,
results_by_perimeter=(
sage_results_by_perimeter
if sage_results_by_perimeter
else {sage_perimeter: sage}
),
),
]

Expand Down Expand Up @@ -650,3 +656,47 @@ def test_n2000_ein_out_of_n2000_site_no_bcc(rf, moulinette_url):
eval_email = eval.get_evaluation_email()
email = eval_email.get_email(req)
assert "[email protected]" not in email.bcc


@pytest.mark.parametrize("footprint", [1200])
def test_multiple_sage(rf, moulinette_url):
"""Test email when evalreq is:
- created by an instructor
- the eval result is "soumis"
- there is multiple Sage perimeter impacted with different results
"""
eval_kwargs = {
"user_type": USER_TYPES.instructor,
"moulinette_url": moulinette_url,
"send_eval_to_project_owner": True,
}
eval, moulinette = fake_moulinette(
moulinette_url,
"soumis",
"soumis",
"systematique",
"soumis",
sage_results_by_perimeter={
Mock(contact_email="[email protected]"): "interdit",
Mock(contact_email="[email protected]"): "action_requise",
Mock(contact_email="[email protected]"): "non_disponible",
},
**eval_kwargs,
)

req = rf.get("/")
eval_email = eval.get_evaluation_email()
email = eval_email.get_email(req)
assert email.to == ["[email protected]", "[email protected]"]
assert email.cc == ["[email protected]"]

assert email.bcc == [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
]

body = email.body
assert "À transmettre au porteur" not in body
81 changes: 67 additions & 14 deletions envergo/moulinette/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
from abc import ABC, abstractmethod
from collections import OrderedDict
from itertools import groupby
from operator import attrgetter

from django.conf import settings
from django.contrib.gis.db.models import MultiPolygonField
Expand Down Expand Up @@ -72,6 +74,19 @@ def all_regulations():
class Regulation(models.Model):
"""A single regulation (e.g Loi sur l'eau)."""

result_cascade = [
RESULTS.interdit,
RESULTS.systematique,
RESULTS.cas_par_cas,
RESULTS.soumis,
RESULTS.action_requise,
RESULTS.a_verifier,
RESULTS.iota_a_verifier,
RESULTS.non_soumis,
RESULTS.non_concerne,
RESULTS.non_disponible,
]

regulation = models.CharField(_("Regulation"), max_length=64, choices=REGULATIONS)
weight = models.PositiveIntegerField(_("Order"), default=1)

Expand Down Expand Up @@ -242,21 +257,9 @@ def result(self):
# From this point, we made sure every data (regulation, perimeter) is existing
# and activated

cascade = [
RESULTS.interdit,
RESULTS.systematique,
RESULTS.cas_par_cas,
RESULTS.soumis,
RESULTS.action_requise,
RESULTS.a_verifier,
RESULTS.iota_a_verifier,
RESULTS.non_soumis,
RESULTS.non_concerne,
RESULTS.non_disponible,
]
results = [criterion.result for criterion in self.criteria.all()]
result = None
for status in cascade:
for status in self.result_cascade:
if status in results:
result = status
break
Expand All @@ -267,9 +270,59 @@ def result(self):
result = RESULTS.non_soumis
else:
result = RESULTS.non_disponible

return result

@property
def results_by_perimeter(self):
"""Compute global result for each perimeter for which this regulation is activated.
When there is several perimeters, we may want to display some different
information depending on the result of each single perimeter.
E.g. if the project is impacting two different SAGE, we may have some
different required actions for each of them.
This method is using the same cascading logic as the `result` property,
to reduce multiple criteria results to a single value.
The results are sorted based on the result cascade, because we want to
display first the most restrictive results.
"""
if not self.has_perimeters:
return None

results_by_perimeter = {}

# Fetch already evaluated criteria
criteria_list = list(self.criteria.all())
criteria_list.sort(key=attrgetter("perimeter"))
grouped_criteria = {
k: list(v) for k, v in groupby(criteria_list, key=attrgetter("perimeter"))
}

for perimeter in self.perimeters.all():
perimeter_criteria = grouped_criteria.get(perimeter, [])
results = [criterion.result for criterion in perimeter_criteria]
result = None
for status in self.result_cascade:
if status in results:
result = status
break
# If there is no criterion at all, we have to set a default value
if result is None:
if perimeter.is_activated:
result = RESULTS.non_soumis
else:
result = RESULTS.non_disponible
results_by_perimeter[perimeter] = result

# sort based on the results cascade
return OrderedDict(
sorted(
results_by_perimeter.items(),
key=lambda item: self.result_cascade.index(item[1]),
)
)

def required_actions(self, stake=None):
"""Return the list of required actions for the given stake."""

Expand Down
31 changes: 31 additions & 0 deletions envergo/moulinette/tests/test_sage.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,34 @@ def test_several_perimeter_maps_display(
in res.content.decode()
)
assert "« Sage Test »" in res.content.decode()


@pytest.mark.parametrize("footprint", [1000])
def test_several_perimeter_may_have_different_results(
moulinette_data, sage_criteria, france_map, client # noqa
):
"""When several perimeters are found, their respective results are displayed."""

ConfigAmenagementFactory(is_activated=True)

sage_non_disponible = PerimeterFactory(
name="Sage Non Disponible",
activation_map=france_map,
regulation=sage_criteria[0].regulation,
is_activated=False,
)

sage_test = PerimeterFactory(
name="Sage Test",
activation_map=france_map,
regulation=sage_criteria[0].regulation,
)

moulinette = MoulinetteAmenagement(moulinette_data, moulinette_data, False)
moulinette.catalog["forbidden_wetlands_within_25m"] = True
moulinette.evaluate()
assert moulinette.sage.results_by_perimeter == {
sage_criteria[0].perimeter: "interdit",
sage_test: "non_soumis",
sage_non_disponible: "non_disponible",
}
6 changes: 6 additions & 0 deletions envergo/pages/templatetags/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,9 @@ def envergo_submit_row(context):
def to_list(item):
"""turn a single item into a list"""
return [item]


@register.filter
def add_string(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
26 changes: 26 additions & 0 deletions envergo/templates/moulinette/sage/_result_multiple_perimeters.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% for perimeter, result in regulation.results_by_perimeter.items %}
{% if result == "interdit" %}
{% include 'moulinette/sage/result_interdit_single_perimeter.html' %}

{% elif result == "action_requise" %}
{% include 'moulinette/sage/result_action_requise_single_perimeter.html' %}

{% elif result == "a_verifier" %}
{% include 'moulinette/sage/result_a_verifier_single_perimeter.html' %}

{% elif result == "non_soumis" %}
{% include 'moulinette/sage/result_non_soumis_single_perimeter.html' %}

{% elif result == "non_concerne" %}
{% include 'moulinette/sage/result_non_concerne_single_perimeter.html' %}

{% elif result == "non_disponible" %}
{% include 'moulinette/sage/result_non_disponible_single_perimeter.html' %}

{% else %}
{% include 'moulinette/sage/result_non_active.html' %}

{% endif %}

{% if not forloop.last %}<hr>{% endif %}
{% endfor %}
59 changes: 1 addition & 58 deletions envergo/templates/moulinette/sage/result_a_verifier.html
Original file line number Diff line number Diff line change
@@ -1,58 +1 @@
<p>
Le projet est <strong>susceptible d'être interdit</strong>.
</p>

<p>
En effet, le règlement de SAGE interdit tout projet soumis à la Loi sur l'eau qui impacterait une zone humide.
Or, le projet se situe sur une zone humide référencée et au vu des informations saisies, il est susceptible d'être soumis à la Loi sur l’eau (<a href="#regulation_loi_sur_leau">voir section « Loi sur l’eau »)</a>.
</p>

<h3>Marche à suivre</h3>

<p>
Pour déterminer si le projet est autorisé, le porteur doit mener les études pour établir s'il est soumis à la Loi sur l'eau.
</p>

{% include 'moulinette/_read_more_btn.html' with aria_controls=regulation.slug %}

<div class="more fr-collapse" id="read-more-{{ regulation.slug }}">

<p>Il convient de :</p>

<p>
<strong>1/ Se rapprocher d'un bureau d'études disposant d'une expertise environnementale et lui communiquer ces éléments.</strong>
</p>

<p>
Les études pourront révéler que le projet est interdit en l’état, qui devra donc être modifié pour respecter le règlement de SAGE.
</p>

<p>
<strong>2/ Se rapprocher de la structure en charge de l’animation du SAGE</strong>
</p>

{% for perimeter in regulation.perimeters.all %}{{ perimeter.contact }}{% endfor %}

<h4 class="fr-h4">Sanctions en cas d'omission</h4>

<div class="fr-alert fr-alert--info fr-my-3w">
<p>
S'il s'avère que le projet est réalisé sans respecter le règlement de SAGE, le responsable s'expose à des sanctions administratives et pénales, qui peuvent aller jusqu'à :
</p>

<ul class="fr-mb-0">
<li>
<strong>obligation de remettre le terrain en son état initial ;</strong>
</li>
<li>
amende de catégorie 5 : 1 500 € (<a href="https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000006836872/"
target="_blank"
rel="noopener">article R.212-48 du code de l'environnement</a>).
</li>
</ul>

</div>

{% include 'moulinette/_read_more_btn.html' with aria_controls=regulation.slug %}

</div>
{% include 'moulinette/sage/_result_multiple_perimeters.html' %}
Loading

0 comments on commit 195e261

Please sign in to comment.