Skip to content

Commit a64c613

Browse files
committed
ServiceSerializer: ajour champ average_orientation_response_delay_days
1 parent 5744c5c commit a64c613

3 files changed

Lines changed: 139 additions & 1 deletion

File tree

back/dora/emplois/serializers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import itertools
22

33
from django.core.files.storage import default_storage
4+
from django.db.models import Avg, DurationField, F
5+
from django.db.models.expressions import ExpressionWrapper
46
from rest_framework import serializers
57

8+
from dora.orientations.models import Orientation, OrientationStatus
69
from dora.services.models import Service
710

811
COACH_ORIENTATION_MODES_ORDER = {
@@ -40,6 +43,7 @@ class ServiceSerializer(serializers.ModelSerializer):
4043
kinds = serializers.SerializerMethodField()
4144
is_orientable_with_dora_form = serializers.SerializerMethodField()
4245
is_contact_info_public = serializers.BooleanField(read_only=True)
46+
average_orientation_response_delay_days = serializers.SerializerMethodField()
4347

4448
class Meta:
4549
model = Service
@@ -60,6 +64,7 @@ class Meta:
6064
"kinds",
6165
"is_orientable_with_dora_form",
6266
"is_contact_info_public",
67+
"average_orientation_response_delay_days",
6368
]
6469

6570
def get_id(self, obj):
@@ -173,3 +178,31 @@ def get_is_orientable_with_dora_form(self, obj):
173178
mode.value == "formulaire-dora"
174179
for mode in obj.coach_orientation_modes.all()
175180
)
181+
182+
def get_average_orientation_response_delay_days(self, obj):
183+
"""Délai moyen de réponse aux demandes d'orientation, en jours."""
184+
prefetched = getattr(obj, "_prefetched_objects_cache", {}).get("orientations")
185+
if prefetched is not None:
186+
delays = [(o.processing_date - o.creation_date).days for o in prefetched]
187+
return round(sum(delays) / len(delays)) if delays else None
188+
result = (
189+
Orientation.objects.filter(
190+
service=obj,
191+
status__in=[
192+
OrientationStatus.ACCEPTED,
193+
OrientationStatus.REJECTED,
194+
],
195+
processing_date__isnull=False,
196+
)
197+
.annotate(
198+
delay=ExpressionWrapper(
199+
F("processing_date") - F("creation_date"),
200+
output_field=DurationField(),
201+
),
202+
)
203+
.aggregate(avg_delay=Avg("delay"))
204+
)
205+
avg_delay = result["avg_delay"]
206+
if avg_delay is None:
207+
return None
208+
return round(avg_delay.total_seconds() / 86400)

back/dora/emplois/tests/test_serializers.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from datetime import timedelta
2+
13
import pytest
24
from data_inclusion.schema.v1.publics import Public as DiPublic
5+
from django.utils import timezone
36
from model_bakery import baker
47

5-
from dora.core.test_utils import make_published_service
8+
from dora.core.test_utils import make_orientation, make_published_service
69
from dora.emplois.serializers import ServiceSerializer
710
from dora.emplois.views import PREFETCH_RELATED_SERVICE_LIST
11+
from dora.orientations.models import OrientationStatus
812
from dora.services.models import (
913
AccessCondition,
1014
BeneficiaryAccessMode,
@@ -415,6 +419,99 @@ def test_service_serializer_is_orientable_with_dora_form_when_ft_whitelisted(
415419
assert data["is_orientable_with_dora_form"] is True
416420

417421

422+
def test_average_orientation_response_delay_days_none_when_no_orientations():
423+
service = make_published_service()
424+
data = ServiceSerializer(service).data
425+
assert data["average_orientation_response_delay_days"] is None
426+
427+
428+
def test_average_orientation_response_delay_days_none_when_only_pending_orientations():
429+
service = make_published_service()
430+
now = timezone.now()
431+
make_orientation(
432+
service=service,
433+
status=OrientationStatus.PENDING,
434+
creation_date=now - timedelta(days=5),
435+
processing_date=None,
436+
)
437+
data = ServiceSerializer(service).data
438+
assert data["average_orientation_response_delay_days"] is None
439+
440+
441+
def test_average_orientation_response_delay_days_single_orientation():
442+
service = make_published_service()
443+
creation = timezone.now() - timedelta(days=10)
444+
processing = creation + timedelta(days=3)
445+
make_orientation(
446+
service=service,
447+
status=OrientationStatus.ACCEPTED,
448+
creation_date=creation,
449+
processing_date=processing,
450+
)
451+
data = ServiceSerializer(service).data
452+
assert data["average_orientation_response_delay_days"] == 3
453+
454+
455+
def test_average_orientation_response_delay_days_average_of_multiple_orientations():
456+
service = make_published_service()
457+
base = timezone.now() - timedelta(days=30)
458+
make_orientation(
459+
service=service,
460+
status=OrientationStatus.ACCEPTED,
461+
creation_date=base,
462+
processing_date=base + timedelta(days=2),
463+
)
464+
make_orientation(
465+
service=service,
466+
status=OrientationStatus.REJECTED,
467+
creation_date=base - timedelta(days=10),
468+
processing_date=base - timedelta(days=10) + timedelta(days=4),
469+
)
470+
data = ServiceSerializer(service).data
471+
# (2 + 4) / 2 = 3
472+
assert data["average_orientation_response_delay_days"] == 3
473+
474+
475+
def test_average_orientation_response_delay_days_rounds_to_nearest_integer():
476+
service = make_published_service()
477+
base = timezone.now() - timedelta(days=20)
478+
make_orientation(
479+
service=service,
480+
status=OrientationStatus.ACCEPTED,
481+
creation_date=base,
482+
processing_date=base + timedelta(days=1),
483+
)
484+
make_orientation(
485+
service=service,
486+
status=OrientationStatus.ACCEPTED,
487+
creation_date=base - timedelta(days=5),
488+
processing_date=base - timedelta(days=5) + timedelta(days=2),
489+
)
490+
data = ServiceSerializer(service).data
491+
# (1 + 2) / 2 = 1.5 -> round to 2
492+
assert data["average_orientation_response_delay_days"] == 2
493+
494+
495+
def test_average_orientation_response_delay_days_only_counts_this_service():
496+
service = make_published_service()
497+
other_service = make_published_service()
498+
creation = timezone.now() - timedelta(days=10)
499+
make_orientation(
500+
service=service,
501+
status=OrientationStatus.ACCEPTED,
502+
creation_date=creation,
503+
processing_date=creation + timedelta(days=2),
504+
)
505+
make_orientation(
506+
service=other_service,
507+
status=OrientationStatus.ACCEPTED,
508+
creation_date=creation,
509+
processing_date=creation + timedelta(days=10),
510+
)
511+
data = ServiceSerializer(service).data
512+
assert data["average_orientation_response_delay_days"] == 2
513+
514+
418515
def test_service_serializer_does_not_add_queries_when_relations_prefetched(
419516
django_assert_max_num_queries,
420517
):

back/dora/emplois/views.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
from django.conf import settings
2+
from django.db.models import Prefetch
23
from rest_framework import permissions, viewsets
34
from rest_framework.renderers import JSONRenderer
45
from rest_framework.versioning import NamespaceVersioning
56

67
from dora.core.pagination import OptionalPageNumberPagination
8+
from dora.orientations.models import Orientation, OrientationStatus
79
from dora.services.models import Service
810

911
from .serializers import ServiceSerializer
1012

13+
_ANSWERED_ORIENTATIONS_QUERYSET = Orientation.objects.filter(
14+
status__in=[OrientationStatus.ACCEPTED, OrientationStatus.REJECTED],
15+
processing_date__isnull=False,
16+
).only("creation_date", "processing_date")
17+
1118
PREFETCH_RELATED_SERVICE_LIST = [
1219
"publics",
1320
"access_conditions",
@@ -18,6 +25,7 @@
1825
"coach_orientation_modes",
1926
"orientable_ft_services",
2027
"beneficiaries_access_modes",
28+
Prefetch("orientations", queryset=_ANSWERED_ORIENTATIONS_QUERYSET),
2129
]
2230

2331

0 commit comments

Comments
 (0)