Skip to content

Commit 248f0ce

Browse files
committed
API périmètres - Limiter aux événements avant une date
fix #1251
1 parent faf75f6 commit 248f0ce

File tree

5 files changed

+159
-6
lines changed

5 files changed

+159
-6
lines changed

django/core/models.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import uuid
2+
from datetime import date
23
from enum import StrEnum, auto
34
from itertools import groupby
45
from json import load
@@ -134,7 +135,11 @@ class EventImpact(StrEnum):
134135

135136

136137
class ProcedureQuerySet(models.QuerySet):
137-
def with_events(self) -> Self:
138+
def with_events(self, *, avant: date | None = None) -> Self:
139+
events = Event.objects.all()
140+
if avant:
141+
events = events.filter(date_evenement_string__lt=str(avant))
142+
138143
approbation_event_types = [
139144
event_type
140145
for event_impact_by_event_type in EVENT_IMPACT_BY_TYPE_DOCUMENT.values()
@@ -146,7 +151,7 @@ def with_events(self) -> Self:
146151
models.When(
147152
type_document=type_document,
148153
then=models.Subquery(
149-
Event.objects.filter(
154+
events.filter(
150155
procedure=models.OuterRef("pk"),
151156
is_valid=True,
152157
type__in=event_impact_by_event_type.keys(),
@@ -162,7 +167,7 @@ def with_events(self) -> Self:
162167
return self.annotate(
163168
date_approbation=Coalesce(
164169
models.Subquery(
165-
Event.objects.filter(
170+
events.filter(
166171
procedure=models.OuterRef("pk"),
167172
type__in=approbation_event_types,
168173
).values("date_evenement_string")[:1]
@@ -271,13 +276,16 @@ def with_opposabilite(
271276
departement: str | None = None,
272277
collectivite_code: str | None = None,
273278
collectivite_type: str | None = None,
279+
avant: date | None = None,
274280
) -> list["CommuneProcedure"]:
275281
communes_procedures = (
276282
self.filter(
277283
procedure__is_principale=True,
278284
procedure__archived=False,
279285
)
280-
.prefetch_related(models.Prefetch("procedure", Procedure.objects.all()))
286+
.prefetch_related(
287+
models.Prefetch("procedure", Procedure.objects.with_events(avant=avant))
288+
)
281289
.order_by("collectivite_code", "collectivite_type")
282290
)
283291

django/core/tests/test_models.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import date
2+
13
import pytest
24
from pytest_django import DjangoAssertNumQueries
35

@@ -94,6 +96,31 @@ def test_date_approbation_quand_event_approbation_manquant(
9496

9597
assert procedure_with_events.date_approbation == "0000-00-00"
9698

99+
@pytest.mark.django_db
100+
def test_date_approbation_ignore_event_apres(
101+
self, django_assert_num_queries: DjangoAssertNumQueries
102+
) -> None:
103+
procedure = Procedure.objects.create(type_document=TypeDocument.PLUI)
104+
procedure.event_set.create(
105+
type="Caractère exécutoire",
106+
date_evenement_string="2022-12-01",
107+
)
108+
procedure.event_set.create(
109+
type="Délibération d'approbation",
110+
date_evenement_string="2023-12-01",
111+
)
112+
113+
assert [event.impact for event in procedure.event_set.all()] == [
114+
EventImpact.OPPOSABLE,
115+
EventImpact.OPPOSABLE,
116+
]
117+
with django_assert_num_queries(1):
118+
procedure_with_events = Procedure.objects.with_events(
119+
avant=date(2023, 12, 1)
120+
).get(id=procedure.id)
121+
122+
assert procedure_with_events.date_approbation == "2022-12-01"
123+
97124

98125
class TestProcedureStatut:
99126
@pytest.mark.django_db
@@ -111,6 +138,42 @@ def test_principale_opposable(
111138

112139
assert procedure_with_events.statut == EventImpact.OPPOSABLE
113140

141+
@pytest.mark.django_db
142+
@pytest.mark.parametrize(
143+
("jour_limite", "impact"),
144+
[
145+
(3, None),
146+
(4, EventImpact.EN_COURS),
147+
(5, EventImpact.EN_COURS),
148+
(6, EventImpact.OPPOSABLE),
149+
],
150+
)
151+
def test_ignore_event_apres(
152+
self,
153+
django_assert_num_queries: DjangoAssertNumQueries,
154+
jour_limite: int,
155+
impact: EventImpact,
156+
) -> None:
157+
procedure = Procedure.objects.create(
158+
is_principale=True, type_document=TypeDocument.PLUI
159+
)
160+
event_prescription = procedure.event_set.create(
161+
type="Délibération de prescription du conseil municipal ou communautaire",
162+
date_evenement_string="2024-12-03",
163+
)
164+
event_approbation = procedure.event_set.create(
165+
type="Caractère exécutoire", date_evenement_string="2024-12-05"
166+
)
167+
168+
assert event_prescription.impact == EventImpact.EN_COURS
169+
assert event_approbation.impact == EventImpact.OPPOSABLE
170+
with django_assert_num_queries(1):
171+
procedure_with_events = Procedure.objects.with_events(
172+
avant=date(2024, 12, jour_limite)
173+
).get(id=procedure.id)
174+
175+
assert procedure_with_events.statut == impact
176+
114177
@pytest.mark.django_db
115178
def test_principale_sans_evenement(
116179
self, django_assert_num_queries: DjangoAssertNumQueries
@@ -454,3 +517,46 @@ def test_ignore_procedures_archivees(
454517
perimetres = CommuneProcedure.objects.with_opposabilite()
455518
with django_assert_num_queries(0):
456519
assert perimetres == [commune_procedure_reelle]
520+
521+
@pytest.mark.django_db
522+
def test_ignore_event_apres(
523+
self, django_assert_num_queries: DjangoAssertNumQueries
524+
) -> None:
525+
procedure_opposable_fevrier = Procedure.objects.create(
526+
is_principale=True, type_document=TypeDocument.PLUI
527+
)
528+
procedure_opposable_fevrier.event_set.create(
529+
type="Caractère exécutoire", date_evenement_string="2024-02-01"
530+
)
531+
commune_procedure_opposable_fevrier = (
532+
procedure_opposable_fevrier.perimetre.create(collectivite_code="12345")
533+
)
534+
535+
procedure_opposable_janvier = Procedure.objects.create(
536+
is_principale=True, type_document=TypeDocument.PLU
537+
)
538+
procedure_opposable_janvier.event_set.create(
539+
type="Caractère exécutoire", date_evenement_string="2024-01-01"
540+
)
541+
commune_procedure_opposable_janvier = (
542+
procedure_opposable_janvier.perimetre.create(collectivite_code="12345")
543+
)
544+
545+
with django_assert_num_queries(1):
546+
procedures = Procedure.objects.with_events()
547+
548+
assert all(
549+
procedure.statut == EventImpact.OPPOSABLE for procedure in procedures
550+
)
551+
552+
with django_assert_num_queries(2):
553+
perimetres = CommuneProcedure.objects.with_opposabilite(
554+
avant=date(2024, 2, 1)
555+
)
556+
with django_assert_num_queries(0):
557+
assert perimetres == [
558+
commune_procedure_opposable_fevrier,
559+
commune_procedure_opposable_janvier,
560+
]
561+
assert not perimetres[0].opposable
562+
assert perimetres[1].opposable

django/core/tests/test_views.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,28 @@ def test_retourne_tout_sans_filtre_departement(self, client: Client) -> None:
5757
response = client.get("/api/perimetres")
5858
reader = DictReader(response.content.decode().splitlines())
5959
assert len(list(reader)) == 2
60+
61+
@pytest.mark.django_db
62+
def test_ignore_event_apres(self, client: Client) -> None:
63+
commune_procedure = create_commune_procedure(code="12345", departement="12")
64+
commune_procedure.procedure.event_set.create(
65+
type="Caractère exécutoire", date_evenement_string="2023-01-01"
66+
)
67+
68+
response = client.get("/api/perimetres", {"avant": "2023-01-01"})
69+
reader = DictReader(response.content.decode().splitlines())
70+
assert [cp["opposable"] for cp in reader] == ["False"]
71+
72+
@pytest.mark.django_db
73+
@pytest.mark.parametrize(
74+
"invalid_avant",
75+
["2023-1-01", "2023-02-30", "invalid-date", "2023/01/01"],
76+
)
77+
def test_parsing_avant(self, client: Client, invalid_avant: str) -> None:
78+
response = client.get("/api/perimetres", {"avant": invalid_avant})
79+
80+
assert response.status_code == 400
81+
assert (
82+
response.content.decode()
83+
== "Le paramètre 'avant' doit être une date valide au format YYYY-MM-DD."
84+
)

django/core/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from csv import DictWriter
2+
from datetime import date
23
from operator import attrgetter
34

4-
from django.http import HttpRequest, HttpResponse
5+
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
56
from django.shortcuts import render
67
from django.views.decorators.http import require_safe
78

@@ -11,8 +12,20 @@
1112
@require_safe
1213
def api_perimetres(request: HttpRequest) -> HttpResponse:
1314
departement = request.GET.get("departement")
15+
16+
try:
17+
avant = (
18+
date.fromisoformat(request.GET.get("avant"))
19+
if request.GET.get("avant")
20+
else None
21+
)
22+
except ValueError:
23+
return HttpResponseBadRequest(
24+
"Le paramètre 'avant' doit être une date valide au format YYYY-MM-DD."
25+
)
26+
1427
communes_procedures = CommuneProcedure.objects.with_opposabilite(
15-
departement=departement
28+
departement=departement, avant=avant
1629
)
1730

1831
response = HttpResponse(content_type="text/csv;charset=utf-8")

nuxt/content/Dev/API/urba/perimetres.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Retourne les procédures de chaque commune et leur opposabilité.
2020
#### Paramètres de requête disponibles
2121

2222
- `departement` : Filtrer par le code INSEE du département.
23+
- `avant` : Ne prend en compte que les événements avant ce jour, au format `YYYY-MM-DD`.
2324

2425
### Réponse
2526

0 commit comments

Comments
 (0)