Skip to content

Commit 883b15a

Browse files
authored
Merge pull request #287 from betagouv/filter-by-survey
Filtrage des réponses par enquête
2 parents 8f861bf + 370df72 commit 883b15a

3 files changed

Lines changed: 91 additions & 9 deletions

File tree

backend/responses/tests/test_get_responses.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,28 @@ def test_user_with_both_responder_and_manager_roles_sees_only_own_responses(self
340340
self.assertIn(own_response.id, ids)
341341
# Les réponses des autres ne sont pas visibles, même si MANAGER dans la même org
342342
self.assertNotIn(other_response.id, ids)
343+
344+
345+
class TestFilterResponses(APITestCase):
346+
def get_ids(self, response):
347+
return [r["id"] for r in response.json()["results"]]
348+
349+
@authenticate
350+
def test_filter_by_survey_returns_only_matching_responses(self):
351+
"""
352+
Le paramètre ?survey=<id> ne retourne que les réponses de cette enquête,
353+
et exclut celles des autres enquêtes.
354+
"""
355+
org = OrganisationFactory()
356+
survey_a = SurveyFactory(organisation=org)
357+
survey_b = SurveyFactory(organisation=org)
358+
MembershipFactory(user=authenticate.user, organisation=org, membership_type=MembershipType.MANAGER)
359+
360+
response_a = ResponseFactory(survey=survey_a)
361+
ResponseFactory(survey=survey_b)
362+
363+
response = self.client.get(reverse("response_list_create"), {"survey": survey_a.id}, format="json")
364+
365+
self.assertEqual(response.status_code, status.HTTP_200_OK)
366+
ids = self.get_ids(response)
367+
self.assertEqual(ids, [response_a.id])

backend/responses/views/response.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from rest_framework.generics import GenericAPIView, ListAPIView, ListCreateAPIView, RetrieveAPIView
1111
from rest_framework.pagination import LimitOffsetPagination
1212
from rest_framework.permissions import IsAuthenticated
13+
from rest_framework.response import Response as DRFResponse
14+
from surveys.models import Survey
15+
from surveys.serializers import SurveyDisplaySerializer
1316

1417
from responses.models import Response
1518
from responses.permissions import CanCreateResponse
@@ -22,13 +25,37 @@
2225

2326

2427
class ResponsePagination(LimitOffsetPagination):
28+
"""
29+
On ajoute dans le payload les enquêtes afin de pouvoir filtrer par enquête.
30+
"""
31+
2532
default_limit = 20
2633
max_limit = 100
2734

35+
surveys = []
36+
37+
def paginate_queryset(self, queryset, request, view=None):
38+
original_queryset = view.get_queryset()
39+
survey_ids = original_queryset.values_list("survey", flat=True).distinct().order_by()
40+
self.surveys = Survey.objects.filter(id__in=survey_ids)
41+
return super().paginate_queryset(queryset, request, view)
42+
43+
def get_paginated_response(self, data):
44+
return DRFResponse(
45+
{
46+
"count": self.count,
47+
"next": self.get_next_link(),
48+
"previous": self.get_previous_link(),
49+
"results": data,
50+
"surveys": SurveyDisplaySerializer(self.surveys, many=True).data,
51+
}
52+
)
53+
2854

2955
class ResponseFilterSet(django_filters.FilterSet):
3056
created_after = django_filters.DateTimeFilter(field_name="creation_date", lookup_expr="gte")
3157
created_before = django_filters.DateTimeFilter(field_name="creation_date", lookup_expr="lte")
58+
survey = django_filters.NumberFilter(field_name="survey_id")
3259

3360
class Meta:
3461
model = Response

web/src/pages/ResponseListPage.vue

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"limit": 10,
1010
"created_after": "",
1111
"created_before": "",
12-
"triage": ""
12+
"triage": "",
13+
"survey": ""
1314
}
1415
}
1516
}
@@ -19,6 +20,7 @@
1920
import { computed, watch } from "vue"
2021
import { useApiFetch } from "../utils/data-fetching.ts"
2122
import type { ResponseDisplay } from "@shared-types/response"
23+
import type { SurveyDisplay } from "@shared-types/api"
2224
import { useRouter, useRoute } from "vue-router"
2325
import ProgressSpinner from "../components/ProgressSpinner.vue"
2426
import PaginationSizeSelect from "../components/ResponseListPage/PaginationSizeSelect.vue"
@@ -36,12 +38,14 @@ const createdBefore = computed(
3638
() => (route.query.created_before as string) ?? ""
3739
)
3840
const ordering = computed(() => route.query.triage as string)
41+
const surveyFilter = computed(() => (route.query.survey as string) ?? "")
3942
4043
const filterParams = computed(() => {
4144
const params = new URLSearchParams()
4245
if (createdAfter.value) params.set("created_after", createdAfter.value)
4346
if (createdBefore.value) params.set("created_before", createdBefore.value)
4447
if (ordering.value) params.set("ordering", String(ordering.value))
48+
if (surveyFilter.value) params.set("survey", surveyFilter.value)
4549
return params
4650
})
4751
@@ -62,7 +66,11 @@ const exportCsvUrl = computed(() => {
6266
return `${base}/responses/export/csv/${q ? `?${q}` : ""}`
6367
})
6468
65-
type PaginatedResponse = { count: number; results: ResponseDisplay[] }
69+
type PaginatedResponse = {
70+
count: number
71+
results: ResponseDisplay[]
72+
surveys: SurveyDisplay[]
73+
}
6674
6775
const { data, execute, isFetching } = useApiFetch(url)
6876
.get()
@@ -122,6 +130,7 @@ const updateCreatedAfter = (value: string) =>
122130
const updateCreatedBefore = (value: string) =>
123131
updateQuery({ created_before: value, page: 1 })
124132
const updateOrdering = (value: string) => updateQuery({ triage: value })
133+
const updateSurvey = (value: string) => updateQuery({ survey: value, page: 1 })
125134
126135
const exportJson = () => {
127136
window.location.href = exportJsonUrl.value
@@ -130,7 +139,10 @@ const exportCsv = () => {
130139
window.location.href = exportCsvUrl.value
131140
}
132141
133-
watch([page, limit, createdAfter, createdBefore, ordering], fetchSearchResults)
142+
watch(
143+
[page, limit, createdAfter, createdBefore, ordering, surveyFilter],
144+
fetchSearchResults
145+
)
134146
</script>
135147

136148
<template>
@@ -139,12 +151,30 @@ watch([page, limit, createdAfter, createdBefore, ordering], fetchSearchResults)
139151
:links="[{ to: '/dashboard', text: 'Dashboard' }, { text: 'Réponses' }]"
140152
/>
141153
<div class="filters border mb-2 rounded border-gray-300 p-4 flex gap-8">
142-
<DateRangeFilter
143-
:created-after="createdAfter"
144-
:created-before="createdBefore"
145-
@update:created-after="updateCreatedAfter"
146-
@update:created-before="updateCreatedBefore"
147-
/>
154+
<div>
155+
<DateRangeFilter
156+
class="mb-3"
157+
:created-after="createdAfter"
158+
:created-before="createdBefore"
159+
@update:created-after="updateCreatedAfter"
160+
@update:created-before="updateCreatedBefore"
161+
/>
162+
<DsfrInputGroup>
163+
<DsfrSelect
164+
label="Enquête"
165+
:model-value="surveyFilter"
166+
:options="[
167+
{ value: '', text: 'Toutes les enquêtes' },
168+
...(data?.surveys ?? []).map((s) => ({
169+
value: String(s.id),
170+
text: s.title,
171+
})),
172+
]"
173+
class="text-sm!"
174+
@update:modelValue="updateSurvey"
175+
/>
176+
</DsfrInputGroup>
177+
</div>
148178
<div class="flex flex-col gap-4">
149179
<PaginationSizeSelect
150180
:modelValue="limit"

0 commit comments

Comments
 (0)