Skip to content

Commit

Permalink
feat: update approvals list filters ui
Browse files Browse the repository at this point in the history
The approval expiry field value for ALL changed from "0" to "". That
allows the JS for `has-selected-item` to identify the “empty” option. It
also allowed simplifying the form.
  • Loading branch information
hellodeloo committed May 28, 2024
1 parent 18bd521 commit 48b94a3
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 73 deletions.
10 changes: 10 additions & 0 deletions itou/static/js/htmx_dropdown_filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
htmx.onLoad((target) => {
function toggleHasSelectedItem() {
const dropdown = this.closest('.dropdown');
this.classList.toggle('has-selected-item', dropdown.querySelector('input:checked:not([value=""])'));
}
target.querySelectorAll('.btn-dropdown-filter.dropdown-toggle').forEach((dropdownFilter) => {
dropdownFilter.addEventListener('hide.bs.dropdown', toggleHasSelectedItem);
toggleHasSelectedItem.call(dropdownFilter);
});
});
12 changes: 12 additions & 0 deletions itou/templates/approvals/includes/approvals_filters/reset.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% load str_filters %}

<div class="ms-md-auto" id="approvals-list-filter-counter"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
{% if filters_counter > 0 %}
<a href="{% url 'approvals:list' %}"
class="btn btn-ico btn-dropdown-filter"
aria-label="Réinitialiser le{{ filters_counter|pluralizefr }} filtre{{ filters_counter|pluralizefr }} actif{{ filters_counter|pluralizefr }}">
<i class="ri-eraser-line font-weight-bold" aria-hidden="true"></i>
<span>Effacer tout</span>
</a>
{% endif %}
</div>
18 changes: 10 additions & 8 deletions itou/templates/approvals/includes/approvals_filters/status.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{% load django_bootstrap5 %}

<fieldset>
<legend>Statut du PASS IAE</legend>
<ul class="list-unstyled">
<li>{% bootstrap_field filters_form.status_valid wrapper_class="" %}</li>
<li>{% bootstrap_field filters_form.status_suspended wrapper_class="" %}</li>
<li>{% bootstrap_field filters_form.status_future wrapper_class="" %}</li>
<li>{% bootstrap_field filters_form.status_expired wrapper_class="" %}</li>
<div class="dropdown">
<button type="button" class="btn btn-dropdown-filter dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
Statut
</button>
<ul class="dropdown-menu">
<li>{% bootstrap_field filters_form.status_valid wrapper_class="dropdown-item" %}</li>
<li>{% bootstrap_field filters_form.status_suspended wrapper_class="dropdown-item" %}</li>
<li>{% bootstrap_field filters_form.status_future wrapper_class="dropdown-item" %}</li>
<li>{% bootstrap_field filters_form.status_expired wrapper_class="dropdown-item" %}</li>
</ul>
</fieldset>
</div>
23 changes: 17 additions & 6 deletions itou/templates/approvals/includes/approvals_filters/users.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
{% load django_bootstrap5 %}

<fieldset>
<legend>Nom du salarié</legend>
{% bootstrap_field filters_form.users layout="inline" %}
</fieldset>
<div class="dropdown">
<button type="button" class="btn btn-dropdown-filter dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
Fin du parcours en IAE
</button>
<ul class="dropdown-menu">
{% for choice in filters_form.expiry %}
<li>
<div class="dropdown-item">
<div class="form-check">
<input class="form-check-input" type="radio" name="{{ choice.data.name }}" id="{{ choice.id_for_label }}" value="{{ choice.data.value }}"{% if choice.data.selected %} checked="checked"{% endif %}>
<label class="form-check-label" for="{{ choice.id_for_label }}">{{ choice.data.label }}</label>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
5 changes: 5 additions & 0 deletions itou/templates/approvals/includes/list_counter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% load str_filters %}

<h3 class="h4 mb-0" id="approvals-list-count"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
{% with paginator.count as counter %}<strong>{{ counter }} résultat{{ counter|pluralizefr }}</strong>{% endwith %}
</h3>
12 changes: 12 additions & 0 deletions itou/templates/approvals/includes/list_reset_filters.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% load str_filters %}

<div class="ms-md-auto" id="approvals-list-filter-counter"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
{% if filters_counter > 0 %}
<a href="{% url 'approvals:list' %}"
class="btn btn-ico btn-dropdown-filter"
aria-label="Réinitialiser le{{ filters_counter|pluralizefr }} filtre{{ filters_counter|pluralizefr }} actif{{ filters_counter|pluralizefr }}">
<i class="ri-eraser-line font-weight-bold" aria-hidden="true"></i>
<span>Effacer tout</span>
</a>
{% endif %}
</div>
20 changes: 7 additions & 13 deletions itou/templates/approvals/includes/list_results.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
{% load str_filters %}
{% load django_bootstrap5 %}

<div id="approvals-list">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-md-between mb-3 mb-md-4">
<h3 class="h4 mb-0">
{% with paginator.count as counter %}<strong>{{ counter }} résultat{{ counter|pluralizefr }}</strong>{% endwith %}
</h3>
{% if filters_counter > 0 %}
<div class="flex-column flex-md-row btn-group btn-group-sm btn-group-action" role="group" aria-label="Actions sur les filtres de PASS IAE">
<a href="{% url 'approvals:list' %}" class="btn btn-secondary btn-ico">
<i class="ri-arrow-go-back-line" aria-hidden="true"></i>
<span>Réinitialiser les filtres ({{ filters_counter }})</span>
</a>
</div>
{% endif %}
</div>
{% if not approval_list %}
<div class="c-box c-box--results my-3 my-md-4">
<div class="c-box--results__body">
Expand All @@ -26,3 +15,8 @@ <h3 class="h4 mb-0">
{% include "includes/pagination.html" with page=page_obj boost=True boost_target="#approvals-list" boost_indicator="#approvals-list" %}
{% endif %}
</div>

{% if request.htmx %}
{% include "approvals/includes/list_counter.html" %}
{% include "approvals/includes/approvals_filters/reset.html" %}
{% endif %}
44 changes: 23 additions & 21 deletions itou/templates/approvals/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,30 @@
<section class="s-section">
<div class="s-section__container container">
<div class="s-section__row row">
<div class="col-12 col-md-4 mb-3 mb-md-5">
<aside class="c-aside-filters">
<button class="c-aside-filters__btn__collapse" data-bs-toggle="collapse" data-bs-target="#asideFiltersCollapse" aria-expanded="true" aria-controls="asideFiltersCollapse">
<i class="ri-filter-line" aria-hidden="true"></i>
<span>Filtrer les résultats</span>
</button>
<div class="c-aside-filters__card collapse show" id="asideFiltersCollapse">
<form hx-get="{% url 'approvals:list' %}" hx-trigger="change delay:.5s" hx-indicator="#approvals-list" hx-target="#approvals-list" hx-swap="outerHTML" hx-push-url="true">
<div class="c-aside-filters__card__body">
{% include "approvals/includes/approvals_filters/users.html" %}
<hr>
{% include "approvals/includes/approvals_filters/status.html" %}
<hr>
<fieldset>
<legend>Fin du parcours en IAE</legend>
{% bootstrap_field filters_form.expiry layout="inline" %}
</fieldset>
</div>
</form>
<div class="col-12">
<form hx-get="{% url 'approvals:list' %}"
hx-trigger="change delay:.5s, change from:#id_users"
hx-indicator="#approvals-list"
hx-target="#approvals-list"
hx-swap="outerHTML"
hx-push-url="true"
hx-include="#id_users">
<div class="btn-dropdown-filter-group mb-3 mb-md-4">
{% include "approvals/includes/approvals_filters/status.html" %}
{% include "approvals/includes/approvals_filters/users.html" %}
{% include "approvals/includes/approvals_filters/reset.html" %}
</div>
</aside>
</form>
</div>
</div>
<div class="s-section__row row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-md-between mb-3 mb-md-4">
{% include "approvals/includes/list_counter.html" %}
<div class="flex-column flex-md-row mt-3 mt-md-0">{% bootstrap_field filters_form.users layout="inline" %}</div>
</div>
{% include "approvals/includes/list_results.html" %}
</div>
<div class="col-12 col-md-8">{% include "approvals/includes/list_results.html" %}</div>
</div>
</div>
</section>
Expand All @@ -45,6 +46,7 @@
{% block script %}
{{ block.super }}
<script src='{% static "js/htmx_compat.js" %}'></script>
<script src='{% static "js/htmx_dropdown_filter.js" %}'></script>
<!-- Needed to use the Select2MultipleWidget JS widget. -->
{{ filters_form.media.js }}
{% endblock %}
4 changes: 3 additions & 1 deletion itou/templates/employee_record/includes/list_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,7 @@ <h3 class="h4 m-0">{{ counter }} résultat{{ counter|pluralizefr }}</h3>
</div>
{% if request.htmx %}
{% include "employee_record/includes/list_status_help.html" with request=request status=form.status.value only %}
{% include "employee_record/includes/list_form_fields.html" %}
{% include "employee_record/includes/list_counter.html" %}
{% include "employee_record/includes/employee_record_filters/status.html" %}
{% include "employee_record/includes/employee_record_filters/reset.html" %}
{% endif %}
17 changes: 16 additions & 1 deletion itou/templates/employee_record/list.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% extends "layout/base.html" %}
{% load django_bootstrap5 %}
{% load static %}

{% block title %}Fiches salarié ASP - {{ request.current_organization.display_name }} {{ block.super }}{% endblock %}
Expand Down Expand Up @@ -69,7 +70,21 @@ <h2 class="h3">Nous transférons vos fiches salarié à l'ASP afin de vous faire
{{ form.order.as_hidden }}
</form>
</div>
</aside>
{# Filled via jQuery. Does not need reloading with HTMX, its content is static. #}
{{ form.order.as_hidden }}
</form>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="d-flex flex-column flex-md-row align-items-md-center mb-3 mb-md-4">
{% include "employee_record/includes/list_counter.html" %}
{% include "employee_record/includes/list_sort.html" %}
<div class="flex-column flex-md-row mt-3 mt-md-0 ms-md-3">
{% bootstrap_field filters_form.job_seekers layout="inline" %}
</div>
</div>
{% include "employee_record/includes/list_results.html" %}
</div>
<div class="col-12 col-md-8">{% include "employee_record/includes/list_results.html" %}</div>
</div>
Expand Down
17 changes: 13 additions & 4 deletions itou/www/approvals_views/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,19 @@ class ApprovalExpiry(TextChoices):
LESS_THAN_1_MONTH = "1", "Moins d'1 mois"
LESS_THAN_3_MONTHS = "3", "Moins de 3 mois"
LESS_THAN_7_MONTHS = "7", "Moins de 7 mois"
ALL = "0", "Tous"
ALL = "", "Tous"


class ApprovalForm(forms.Form):
users = forms.MultipleChoiceField(required=False, label="Nom", widget=Select2MultipleWidget)
users = forms.MultipleChoiceField(
required=False,
label="Nom",
widget=Select2MultipleWidget(
attrs={
"data-placeholder": "Nom du candidat",
}
),
)
status_valid = forms.BooleanField(label="PASS IAE valide", required=False)
status_suspended = forms.BooleanField(label="PASS IAE valide (suspendu)", required=False)
status_future = forms.BooleanField(label="PASS IAE valide (non démarré)", required=False)
Expand All @@ -54,6 +62,7 @@ class ApprovalForm(forms.Form):
choices=ApprovalExpiry.choices,
widget=forms.RadioSelect,
initial=ApprovalExpiry.ALL,
required=False,
)

def __init__(self, siae_pk, data, *args, **kwargs):
Expand Down Expand Up @@ -102,8 +111,8 @@ def get_qs_filters(self):
status_filters_list.append(Q(end_at__lt=now))
qs_filters_list.append(Q(reduce(operator.or_, status_filters_list, Q())))

if expiry := int(data.get("expiry")):
qs_filters_list.append(Q(end_at__lt=now + relativedelta(months=expiry), end_at__gte=now))
if expiry := data.get("expiry", ApprovalExpiry.ALL):
qs_filters_list.append(Q(end_at__lt=now + relativedelta(months=int(expiry)), end_at__gte=now))

return qs_filters_list

Expand Down
8 changes: 1 addition & 7 deletions itou/www/approvals_views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
from itou.utils.urls import add_url_params, get_safe_url
from itou.www.apply.forms import JobSeekerExistsForm
from itou.www.approvals_views.forms import (
ApprovalExpiry,
ApprovalForm,
PoleEmploiApprovalSearchForm,
ProlongationRequestDenyInformationProposedActionsForm,
Expand Down Expand Up @@ -172,12 +171,7 @@ def __init__(self):
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
if self.siae:
form_data = self.request.GET or None
if form_data and "expiry" not in form_data:
# Use this as default to handle the case where page=XX is provided
# disabling the initial values of the form
form_data |= {"expiry": ApprovalExpiry.ALL}
self.form = ApprovalForm(self.siae.pk, form_data)
self.form = ApprovalForm(self.siae.pk, self.request.GET or None)

def get_template_names(self):
return ["approvals/includes/list_results.html" if self.request.htmx else "approvals/list.html"]
Expand Down
25 changes: 13 additions & 12 deletions tests/www/approvals_views/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ def test_users_filters(self, client):
(approval_same_company.user_id, "Seb Tambre"),
]

url = f"{reverse('approvals:list')}?users={approval.user_id}&expiry=0"
url = f"{reverse('approvals:list')}?users={approval.user_id}&expiry="
response = client.get(url)
assertContains(response, "1 résultat")
assertContains(response, reverse("approvals:detail", kwargs={"pk": approval.pk}))
assertNotContains(response, reverse("approvals:detail", kwargs={"pk": approval_same_company.pk}))
assertNotContains(response, reverse("approvals:detail", kwargs={"pk": approval_other_company.pk}))

url = f"{reverse('approvals:list')}?users={approval.user_id}&users={approval_same_company.user_id}&expiry=0"
url = f"{reverse('approvals:list')}?users={approval.user_id}&users={approval_same_company.user_id}&expiry="
response = client.get(url)
assertContains(response, "2 résultats")
assertContains(response, reverse("approvals:detail", kwargs={"pk": approval.pk}))
Expand Down Expand Up @@ -177,27 +177,28 @@ def test_approval_state_filters(self, client):
client.force_login(employer)
list_url = reverse("approvals:list")

url = f"{list_url}?status_valid=on&expiry=0"
url = f"{list_url}?status_valid=on&expiry="
response = client.get(url)
print(response.content.decode())
assertContains(response, "1 résultat")
assertContains(response, reverse("approvals:detail", kwargs={"pk": valid_approval.pk}))

url = f"{list_url}?status_suspended=on&expiry=0"
url = f"{list_url}?status_suspended=on&expiry="
response = client.get(url)
assertContains(response, "1 résultat")
assertContains(response, reverse("approvals:detail", kwargs={"pk": suspended_approval.pk}))

url = f"{list_url}?status_future=on&expiry=0"
url = f"{list_url}?status_future=on&expiry="
response = client.get(url)
assertContains(response, "1 résultat")
assertContains(response, reverse("approvals:detail", kwargs={"pk": future_approval.pk}))

url = f"{list_url}?status_expired=on&expiry=0"
url = f"{list_url}?status_expired=on&expiry="
response = client.get(url)
assertContains(response, "1 résultat")
assertContains(response, reverse("approvals:detail", kwargs={"pk": expired_approval.pk}))

url = f"{list_url}?status_expired=on&status_suspended=on&status_future=on&status_valid=on&expiry=0"
url = f"{list_url}?status_expired=on&status_suspended=on&status_future=on&status_valid=on&expiry="
response = client.get(url)
assertContains(response, "4 résultats")
assertContains(response, reverse("approvals:detail", kwargs={"pk": valid_approval.pk}))
Expand Down Expand Up @@ -319,12 +320,12 @@ def test_approval_expiry_filter_default(self, client):
list_url = reverse("approvals:list")
response = client.get(list_url)
# Check that the default "Fin du parcours en IAE" value "Tous" is selected
expiry_all_input = parse_response_to_soup(response, "input[name='expiry'][value='0']")
assert expiry_all_input.get("checked")
expiry_all_input = parse_response_to_soup(response, "input[name='expiry'][value='']")
assert expiry_all_input.has_attr("checked")
response = client.get(f"{list_url}?page=2")
# Check that the default "Fin du parcours en IAE" value "Tous" is selected
expiry_all_input = parse_response_to_soup(response, "input[name='expiry'][value='0']")
assert expiry_all_input.get("checked")
expiry_all_input = parse_response_to_soup(response, "input[name='expiry'][value='']")
assert expiry_all_input.has_attr("checked")

def test_update_with_htmx(self, client):
now = timezone.localdate()
Expand Down Expand Up @@ -357,7 +358,7 @@ def test_update_with_htmx(self, client):
[less_than_3_months] = simulated_page.find_all("input", attrs={"name": "expiry", "value": "3"})
del less_than_3_months["checked"]
[less_than_1_month] = simulated_page.find_all("input", attrs={"name": "expiry", "value": "1"})
less_than_1_month["checked"] = "checked"
less_than_1_month["checked"] = ""

response = client.get(url, {"expiry": "1"}, headers={"HX-Request": "true"})
update_page_with_htmx(simulated_page, f"form[hx-get='{url}']", response)
Expand Down

0 comments on commit 48b94a3

Please sign in to comment.