Skip to content

Commit

Permalink
www.job_seekers: add sort on table headers
Browse files Browse the repository at this point in the history
  • Loading branch information
xavfernandez committed Feb 14, 2025
1 parent 899889b commit ce7bd08
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 24 deletions.
5 changes: 5 additions & 0 deletions itou/templates/common/tables/th_with_sort.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<th scope="col" aria-sort="{% if order == ascending_value %}ascending{% elif order == ascending_value.opposite %}descending{% else %}none{% endif %}">
<button type="button" data-setter-target="#id_order" data-setter-value="{% if order == ascending_value %}{{ ascending_value.opposite }}{% else %}{{ ascending_value }}{% endif %}">
{{ name }}
</button>
</th>
8 changes: 4 additions & 4 deletions itou/templates/job_seekers_views/includes/list_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
</div>
{% else %}
<div class="table-responsive mt-3 mt-md-4">
<table class="table table-hover">
<table class="table table-hover table-sortable">
<caption class="visually-hidden">Liste des candidats</caption>
<thead>
<tr>
<th scope="col">Prénom NOM</th>
{% include 'common/tables/th_with_sort.html' with order=order ascending_value=order.FULL_NAME_ASC name="Prénom NOM" only %}
<th scope="col">Statut du PASS IAE</th>
<th scope="col">Nombre de candidatures</th>
<th scope="col">Dernière mise à jour de candidature</th>
{% 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 %}
<th scope="col" class="text-end w-50px"></th>
</tr>
</thead>
Expand Down
10 changes: 8 additions & 2 deletions itou/templates/job_seekers_views/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ <h1 class="m-0">Candidats</h1>
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-md-between mb-3 mb-md-4">
{% include "job_seekers_views/includes/list_counter.html" with paginator=paginator request=request only %}
<div class="flex-column flex-md-row mt-3 mt-md-0">
<form hx-get="{% url 'job_seekers_views:list' %}" hx-trigger="change delay:.5s" hx-indicator="#job-seekers-section" hx-target="#job-seekers-section" hx-swap="outerHTML" hx-push-url="true">
<form hx-get="{% url 'job_seekers_views:list' %}"
hx-trigger="change from:#id_order, change delay:.5s"
hx-indicator="#job-seekers-section"
hx-target="#job-seekers-section"
hx-swap="outerHTML"
hx-push-url="true">
{% bootstrap_field filters_form.job_seeker wrapper_class="w-lg-400px" show_label=False %}
{% bootstrap_field filters_form.order %}
</form>
</div>
</div>
{% 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 %}
</div>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions itou/utils/staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 24 additions & 2 deletions itou/www/job_seekers_views/enums.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions itou/www/job_seekers_views/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 = [
Expand Down
21 changes: 13 additions & 8 deletions itou/www/job_seekers_views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -111,17 +111,15 @@ 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

def __init__(self):
super().__init__()
self.form = None
self.order = None

def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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


Expand Down
26 changes: 21 additions & 5 deletions tests/www/job_seekers_views/__snapshots__/test_list.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@
</p>

<div class="flex-column flex-md-row mt-3 mt-md-0">
<form hx-get="/job-seekers/list" hx-indicator="#job-seekers-section" hx-push-url="true" hx-swap="outerHTML" hx-target="#job-seekers-section" hx-trigger="change delay:.5s">
<form hx-get="/job-seekers/list" hx-indicator="#job-seekers-section" hx-push-url="true" hx-swap="outerHTML" hx-target="#job-seekers-section" hx-trigger="change from:#id_order, change delay:.5s">
<div class="w-lg-400px"><label class="visually-hidden" for="id_job_seeker">Nom</label><select class="form-select django-select2" data-allow-clear="true" data-minimum-input-length="0" data-placeholder="Nom du candidat" data-theme="bootstrap-5" id="id_job_seeker" lang="fr" name="job_seeker">
<option selected="" value=""></option>

</select></div>
<input id="id_order" name="order" type="hidden"/>
</form>
</div>
</div>
Expand Down Expand Up @@ -499,14 +500,29 @@
# ---
# name: test_multiple[job seekers list table]
'''
<table class="table table-hover">
<table class="table table-hover table-sortable">
<caption class="visually-hidden">Liste des candidats</caption>
<thead>
<tr>
<th scope="col">Prénom NOM</th>
<th aria-sort="ascending" scope="col">
<button data-setter-target="#id_order" data-setter-value="-full_name" type="button">
Prénom NOM
</button>
</th>

<th scope="col">Statut du PASS IAE</th>
<th scope="col">Nombre de candidatures</th>
<th scope="col">Dernière mise à jour de candidature</th>
<th aria-sort="none" scope="col">
<button data-setter-target="#id_order" data-setter-value="job_applications_nb" type="button">
Nombre de candidatures
</button>
</th>

<th aria-sort="none" scope="col">
<button data-setter-target="#id_order" data-setter-value="last_updated_at" type="button">
Nombre de candidatures
</button>
</th>

<th class="text-end w-50px" scope="col"></th>
</tr>
</thead>
Expand Down
10 changes: 10 additions & 0 deletions tests/www/job_seekers_views/test_enums.py
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions tests/www/job_seekers_views/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="<missing_value>"):
response = client.get(url)
assert response.context["page_obj"].object_list == expected_order["full_name"]

with subtests.test(order="<invalid_value>"):
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))

0 comments on commit ce7bd08

Please sign in to comment.