diff --git a/itou/templates/common/tables/th_with_sort.html b/itou/templates/common/tables/th_with_sort.html new file mode 100644 index 0000000000..5a243de23c --- /dev/null +++ b/itou/templates/common/tables/th_with_sort.html @@ -0,0 +1,5 @@ + + + diff --git a/itou/templates/job_seekers_views/includes/list_results.html b/itou/templates/job_seekers_views/includes/list_results.html index 88d3f1b0d1..cd14ce81ce 100644 --- a/itou/templates/job_seekers_views/includes/list_results.html +++ b/itou/templates/job_seekers_views/includes/list_results.html @@ -13,14 +13,14 @@ {% else %}
- +
- + {% include 'common/tables/th_with_sort.html' with order=order ascending_value=order.FULL_NAME_ASC name="Prénom NOM" only %} - - + {% include 'common/tables/th_with_sort.html' with order=order ascending_value=order.JOB_APPLICATIONS_NB_ASC name="Nombre de candidatures" only %} + {% include 'common/tables/th_with_sort.html' with order=order ascending_value=order.LAST_UPDATED_AT_ASC name="Nombre de candidatures" only %} diff --git a/itou/templates/job_seekers_views/list.html b/itou/templates/job_seekers_views/list.html index 63bf43cd2e..a739857342 100644 --- a/itou/templates/job_seekers_views/list.html +++ b/itou/templates/job_seekers_views/list.html @@ -39,12 +39,18 @@

Candidats

{% include "job_seekers_views/includes/list_counter.html" with paginator=paginator request=request only %}
-
+ {% bootstrap_field filters_form.job_seeker wrapper_class="w-lg-400px" show_label=False %} + {% bootstrap_field filters_form.order %}
- {% include "job_seekers_views/includes/list_results.html" with page_obj=page_obj request=request only %} + {% include "job_seekers_views/includes/list_results.html" with page_obj=page_obj request=request order=order only %} diff --git a/itou/utils/staticfiles.py b/itou/utils/staticfiles.py index 475fcfef70..89a0c0a32b 100644 --- a/itou/utils/staticfiles.py +++ b/itou/utils/staticfiles.py @@ -219,11 +219,11 @@ }, "theme-inclusion": { "download": { - "url": "https://github.com/gip-inclusion/itou-theme/archive/refs/tags/v2.6.6.zip", - "sha256": "34107c71ba0afecb36d157f57d7ec39e20b33ccc66440e62e954faca4a8fba53", + "url": "https://github.com/gip-inclusion/itou-theme/archive/refs/tags/v2.6.9.zip", + "sha256": "93bf5f758ca3c5daf70b07eeb7720957d2e6bd9b7ce91efb1414c01ddd6139ab", }, "extract": { - "origin": "itou-theme-2.6.6/dist", + "origin": "itou-theme-2.6.9/dist", "destination": "vendor/theme-inclusion/", "files": [ "javascripts/app.js", diff --git a/itou/www/job_seekers_views/enums.py b/itou/www/job_seekers_views/enums.py index cd272a1f2f..8a235db72e 100644 --- a/itou/www/job_seekers_views/enums.py +++ b/itou/www/job_seekers_views/enums.py @@ -1,7 +1,29 @@ -from enum import StrEnum +import enum -class JobSeekerSessionKinds(StrEnum): +class JobSeekerSessionKinds(enum.StrEnum): CHECK_NIR_JOB_SEEKER = "job-seeker-check-nir-job-seeker" GET_OR_CREATE = "job-seeker-get-or-create" UPDATE = "job-seeker-update" + + +class JobSeekerOrder(enum.StrEnum): + FULL_NAME_ASC = "full_name" + FULL_NAME_DESC = "-full_name" + LAST_UPDATED_AT_ASC = "last_updated_at" + LAST_UPDATED_AT_DESC = "-last_updated_at" + JOB_APPLICATIONS_NB_ASC = "job_applications_nb" + JOB_APPLICATIONS_NB_DESC = "-job_applications_nb" + + @property + def opposite(self): + if self.value.startswith("-"): + return self.__class__(self.value[1:]) + else: + return self.__class__(f"-{self.value}") + + # Make the Enum work in Django's templates + # See : + # - https://docs.djangoproject.com/en/dev/ref/templates/api/#variables-and-lookups + # - https://github.com/django/django/pull/12304 + do_not_call_in_templates = enum.nonmember(True) diff --git a/itou/www/job_seekers_views/forms.py b/itou/www/job_seekers_views/forms.py index 436f0a402f..81233b2604 100644 --- a/itou/www/job_seekers_views/forms.py +++ b/itou/www/job_seekers_views/forms.py @@ -14,6 +14,7 @@ from itou.utils.templatetags.str_filters import mask_unless from itou.utils.validators import validate_nir from itou.utils.widgets import DuetDatePickerWidget +from itou.www.job_seekers_views.enums import JobSeekerOrder class FilterForm(forms.Form): @@ -27,6 +28,12 @@ class FilterForm(forms.Form): ), ) + order = forms.ChoiceField( + choices=[(item.value, item.name) for item in JobSeekerOrder], + required=False, + widget=forms.HiddenInput(), + ) + def __init__(self, job_seeker_qs, data, *args, request_user, **kwargs): super().__init__(data, *args, **kwargs) self.fields["job_seeker"].choices = [ diff --git a/itou/www/job_seekers_views/views.py b/itou/www/job_seekers_views/views.py index 8a9532e0ac..a6043e995c 100644 --- a/itou/www/job_seekers_views/views.py +++ b/itou/www/job_seekers_views/views.py @@ -28,7 +28,7 @@ from itou.utils.session import SessionNamespace from itou.utils.urls import get_safe_url from itou.www.apply.views.submit_views import ApplicationBaseView -from itou.www.job_seekers_views.enums import JobSeekerSessionKinds +from itou.www.job_seekers_views.enums import JobSeekerOrder, JobSeekerSessionKinds from itou.www.job_seekers_views.forms import ( CheckJobSeekerInfoForm, CheckJobSeekerNirForm, @@ -111,10 +111,7 @@ def get_context_data(self, **kwargs): class JobSeekerListView(UserPassesTestMixin, ListView): model = User queryset = ( - User.objects.filter(kind=UserKind.JOB_SEEKER) - .order_by("first_name", "last_name") - .prefetch_related("approvals") - .select_related("jobseeker_profile") + User.objects.filter(kind=UserKind.JOB_SEEKER).prefetch_related("approvals").select_related("jobseeker_profile") ) paginate_by = 10 paginator_class = ItouPaginator @@ -122,6 +119,7 @@ class JobSeekerListView(UserPassesTestMixin, ListView): def __init__(self): super().__init__() self.form = None + self.order = None def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) @@ -146,9 +144,11 @@ def setup(self, request, *args, **kwargs): ) self.form = FilterForm( User.objects.filter(kind=UserKind.JOB_SEEKER).filter(pk__in=self.job_seekers_ids), - self.request.GET or None, + self.request.GET or {}, request_user=request.user, ) + self.form.full_clean() # We do not use is_valid to validate each field independently + self.order = JobSeekerOrder(self.form.cleaned_data.get("order") or JobSeekerOrder.FULL_NAME_ASC) def test_func(self): return self.request.user.is_prescriber @@ -160,6 +160,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["back_url"] = get_safe_url(self.request, "back_url") context["filters_form"] = self.form + context["order"] = self.order page_obj = context["page_obj"] if page_obj is not None: for job_seeker in page_obj: @@ -198,9 +199,13 @@ def get_queryset(self): valid_eligibility_diagnosis=subquery_diagnosis, ) - if self.form.is_valid() and (job_seeker_pk := self.form.cleaned_data["job_seeker"]): + if job_seeker_pk := self.form.cleaned_data["job_seeker"]: query = query.filter(pk=job_seeker_pk) - + order_args = { + JobSeekerOrder.FULL_NAME_ASC: ("first_name", "last_name"), + JobSeekerOrder.FULL_NAME_DESC: ("-first_name", "-last_name"), + }.get(self.order, (str(self.order),)) + query = query.order_by(*order_args) return query diff --git a/tests/www/job_seekers_views/__snapshots__/test_list.ambr b/tests/www/job_seekers_views/__snapshots__/test_list.ambr index f5cf2a12f3..23361c6b6f 100644 --- a/tests/www/job_seekers_views/__snapshots__/test_list.ambr +++ b/tests/www/job_seekers_views/__snapshots__/test_list.ambr @@ -56,11 +56,12 @@

-
+
+
@@ -499,14 +500,29 @@ # --- # name: test_multiple[job seekers list table] ''' -
Liste des candidats
Prénom NOMStatut du PASS IAENombre de candidaturesDernière mise à jour de candidature
+
- + + - - + + + + diff --git a/tests/www/job_seekers_views/test_enums.py b/tests/www/job_seekers_views/test_enums.py new file mode 100644 index 0000000000..7e84d5be68 --- /dev/null +++ b/tests/www/job_seekers_views/test_enums.py @@ -0,0 +1,10 @@ +import pytest + +from itou.www.job_seekers_views.enums import JobSeekerOrder + + +@pytest.mark.parametrize("order", JobSeekerOrder) +@pytest.mark.no_django_db +def test_opposite(order): + assert order.opposite != order + assert order.opposite.opposite == order diff --git a/tests/www/job_seekers_views/test_list.py b/tests/www/job_seekers_views/test_list.py index 3c621079e0..7338f94282 100644 --- a/tests/www/job_seekers_views/test_list.py +++ b/tests/www/job_seekers_views/test_list.py @@ -479,3 +479,48 @@ def test_filtered_by_job_seeker_for_unauthorized_prescriber(client): (c_d_job_seeker.pk, "C… D…"), (created_job_seeker.pk, "Zorro MARTIN"), ] + + +def test_job_seekers_order(client, subtests): + prescriber = PrescriberFactory() + c_d_job_seeker = JobApplicationFactory( + sender=prescriber, + job_seeker__created_by=prescriber, + job_seeker__last_login=timezone.now(), + job_seeker__first_name="Charles", + job_seeker__last_name="Deux candidatures", + ).job_seeker + JobApplicationFactory(sender=prescriber, job_seeker=c_d_job_seeker) + created_job_seeker = JobSeekerFactory( + created_by=prescriber, + first_name="Zorro", + last_name="Martin", + ) + a_b_job_seeker = JobApplicationFactory( + sender=prescriber, job_seeker__first_name="Alice", job_seeker__last_name="Berger" + ).job_seeker + + client.force_login(prescriber) + url = reverse("job_seekers_views:list") + + expected_order = { + "full_name": [a_b_job_seeker, c_d_job_seeker, created_job_seeker], + "job_applications_nb": [created_job_seeker, a_b_job_seeker, c_d_job_seeker], + "last_updated_at": [c_d_job_seeker, a_b_job_seeker, created_job_seeker], + } + + with subtests.test(order=""): + response = client.get(url) + assert response.context["page_obj"].object_list == expected_order["full_name"] + + with subtests.test(order=""): + response = client.get(url, {"order": "invalid_value"}) + assert response.context["page_obj"].object_list == expected_order["full_name"] + + for order, job_seekers in expected_order.items(): + with subtests.test(order=order): + response = client.get(url, {"order": order}) + assert response.context["page_obj"].object_list == job_seekers + + response = client.get(url, {"order": f"-{order}"}) + assert response.context["page_obj"].object_list == list(reversed(job_seekers))
Liste des candidats
Prénom NOM + + Statut du PASS IAENombre de candidaturesDernière mise à jour de candidature + + + +