Skip to content

Commit

Permalink
Use HTMX on employee records list
Browse files Browse the repository at this point in the history
Rewrote the sort button dropdown to use native events and not jQuery
events, so that HTMX can listen to its change events.
  • Loading branch information
francoisfreitag committed May 2, 2024
1 parent 414ac6a commit cc25520
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 118 deletions.
21 changes: 21 additions & 0 deletions itou/templates/employee_record/includes/list_form_fields.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% load django_bootstrap5 %}
{% load list_filters %}
<div id="employee-records-list-filters" class="c-aside-filters__card__body"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
<fieldset>
<legend>Statut</legend>
<div class="form-group">
{% for status, badge in form.status|zip:badges %}
<div class="d-flex mb-2">
<div class="flex-grow-1">{{ status }}</div>
<div>
<span class="badge rounded-pill badge-xs {{ badge.1 }}">{{ badge.0 }}</span>
</div>
</div>
{% endfor %}
</div>
</fieldset>
<fieldset>
<legend>Par candidat</legend>
{% bootstrap_field filters_form.job_seekers show_label=False %}
</fieldset>
</div>
48 changes: 48 additions & 0 deletions itou/templates/employee_record/includes/list_results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{% load str_filters %}
<div id="employee-records-container">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
{% with navigation_pages.paginator.count as counter %}
<h3 class="h4 m-0">{{ counter }} résultat{{ counter|pluralizefr }}</h3>
{% endwith %}
</div>
<div>
<span class="fs-sm">Trier par :</span>
<button type="button" class="btn btn-sm btn-link dropdown-toggle p-0" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ ordered_by_label }}
</button>
<div class="dropdown-menu dropdown-menu-end" id="order-form-group">
{% for order_value, order_label in form.order.field.choices %}
<button class="dropdown-item {% if order_value == form.order.value %}active{% endif %}" type="button" value="{{ order_value }}">
{{ order_label }}
</button>
{% endfor %}
</div>
</div>
</div>
{# "Real" employee records objects #}
<div class="employee-records-list">
{% if employee_records_list %}
{% for employee_record in navigation_pages %}
{% include "employee_record/includes/list_item.html" with employee_record=employee_record item=employee_record.job_application current_url=request.get_full_path only %}
{% endfor %}
{# New employee records i.e. job applications #}
{% else %}
{% for job_application in navigation_pages %}
{% include "employee_record/includes/list_item.html" with employee_record=None item=job_application only %}
{% endfor %}
{% endif %}
</div>
{% if not navigation_pages %}
<div class="c-box c-box--results my-3 my-md-4">
<div class="c-box--results__body">
<p class="mb-0">Aucune fiche salarié avec le statut selectionné.</p>
</div>
</div>
{% endif %}
{% include "includes/pagination.html" with page=navigation_pages boost=True boost_target="#employee-records-container" boost_indicator="#employee-records-container" %}
</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" %}
{% endif %}
41 changes: 41 additions & 0 deletions itou/templates/employee_record/includes/list_status_help.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<div id="status-help"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
{% if status == "NEW" %}
<p>
Vous trouverez ici les candidatures validées <b>à partir desquelles vous devez créer de nouvelles fiches salarié</b>.
</p>
{% elif status == "READY" %}
<p class="mb-0">
Vous trouverez ici les fiches salarié complétées
<b>en attente d’envoi à l’ASP, qui a lieu automatiquement à intervalles réguliers</b>.
</p>
<p>
À ce stade, seule la visualisation des informations de la fiche est
possible.
</p>
<p>Merci de votre patience.</p>
{% elif status == "SENT" %}
<p class="mb-0">Vous trouverez ici les fiches salarié complétées et envoyées à l'ASP.</p>
<p>
À ce stade, et en attendant un retour de l'ASP, seule la visualisation des informations de
la fiche est possible.
</p>
{% elif status == "REJECTED" %}
<p class="mb-0">
Vous trouverez ici les fiches salarié envoyées à l'ASP et retournées avec une
erreur.
</p>
<p>Vous pouvez modifier les fiches en erreur et les envoyer à nouveau.</p>
{% elif status == "PROCESSED" %}
<p class="mb-0">Vous trouverez ici les fiches salarié envoyées et validées par l'ASP.</p>
<p>
Aucune action ultérieure n'est possible à ce stade, mais vous pouvez consulter le détail de
la fiche salarié.
</p>
{% elif status == "DISABLED" %}
<p class="mb-0">Vous trouverez ici les fiches salarié que vous avez désactivées.</p>
<p>
En cas de besoin vous pouvez réactiver une fiche, elle sera transférée dans la catégorie
"Nouvelle".
</p>
{% endif %}
</div>
134 changes: 17 additions & 117 deletions itou/templates/employee_record/list.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{% extends "layout/base.html" %}
{% load static %}
{% load str_filters %}
{% load list_filters %}
{% load django_bootstrap5 %}

{% block title %}Fiches salarié ASP - {{ request.current_organization.display_name }} {{ block.super }}{% endblock %}

Expand Down Expand Up @@ -44,45 +41,7 @@ <h2 class="h3">Nous transférons vos fiches salarié à l'ASP afin de vous faire
</li>
<li>La visualisation dans l’Extranet IAE 2.0 interviendra dans les 2 heures suivant l’envoi.</li>
</ul>
{% if form.status.value == "NEW" %}
<p>
Vous trouverez ici les candidatures validées <b>à partir desquelles vous devez créer de nouvelles fiches salarié</b>.
</p>
{% elif form.status.value == "READY" %}
<p class="mb-0">
Vous trouverez ici les fiches salarié complétées
<b>en attente d’envoi à l’ASP, qui a lieu automatiquement à intervalles réguliers</b>.
</p>
<p>
À ce stade, seule la visualisation des informations de la fiche est
possible.
</p>
<p>Merci de votre patience.</p>
{% elif form.status.value == "SENT" %}
<p class="mb-0">Vous trouverez ici les fiches salarié complétées et envoyées à l'ASP.</p>
<p>
À ce stade, et en attendant un retour de l'ASP, seule la visualisation des informations de
la fiche est possible.
</p>
{% elif form.status.value == "REJECTED" %}
<p class="mb-0">
Vous trouverez ici les fiches salarié envoyées à l'ASP et retournées avec une
erreur.
</p>
<p>Vous pouvez modifier les fiches en erreur et les envoyer à nouveau.</p>
{% elif form.status.value == "PROCESSED" %}
<p class="mb-0">Vous trouverez ici les fiches salarié envoyées et validées par l'ASP.</p>
<p>
Aucune action ultérieure n'est possible à ce stade, mais vous pouvez consulter le détail de
la fiche salarié.
</p>
{% elif form.status.value == "DISABLED" %}
<p class="mb-0">Vous trouverez ici les fiches salarié que vous avez désactivées.</p>
<p>
En cas de besoin vous pouvez réactiver une fiche, elle sera transférée dans la catégorie
"Nouvelle".
</p>
{% endif %}
{% include "employee_record/includes/list_status_help.html" with request=request status=form.status.value only %}
{% endblock %}

{% block content %}
Expand All @@ -104,96 +63,37 @@ <h2 class="h3">Nous transférons vos fiches salarié à l'ASP afin de vous faire
<span>Filtres des fiches salarié</span>
</button>
<div class="c-aside-filters__card collapse show" id="asideFiltersCollapse">
<form method="get">
<div class="c-aside-filters__card__body">
<fieldset>
<legend>Statut</legend>
<div class="form-group">
{% for status, badge in form.status|zip:badges %}
<div class="d-flex mb-2">
<div class="flex-grow-1">{{ status }}</div>
<div>
<span class="badge rounded-pill badge-xs {{ badge.1 }}">{{ badge.0 }}</span>
</div>
</div>
{% endfor %}
</div>
</fieldset>
{# Job seeker filter #}
<fieldset>
<legend>Par candidat</legend>
{% bootstrap_field filters_form.job_seekers show_label=False %}
</fieldset>
</div>
{# Filled via jQuery #}
<form hx-get="{% url 'employee_record_views:list' %}" hx-trigger="change delay:.5s" hx-indicator="#employee-records-container" hx-target="#employee-records-container" hx-swap="outerHTML" hx-push-url="true">
{% include "employee_record/includes/list_form_fields.html" %}
{# Filled via jQuery. Does not need reloading with HTMX, its content is static. #}
{{ form.order.as_hidden }}
</form>
</div>
</aside>
</div>

<div class="col-12 col-md-8">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
{% with navigation_pages.paginator.count as counter %}
<h3 class="h4 m-0">{{ counter }} résultat{{ counter|pluralizefr }}</h3>
{% endwith %}
</div>
<div>
<span class="fs-sm">Trier par :</span>
<button type="button" class="btn btn-sm btn-link dropdown-toggle p-0" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ ordered_by_label }}
</button>
<div class="dropdown-menu dropdown-menu-end" id="order-form-group">
{% for order_value, order_label in form.order.field.choices %}
<button class="dropdown-item {% if order_value == form.order.value %}active{% endif %}" type="button" value="{{ order_value }}">
{{ order_label }}
</button>
{% endfor %}
</div>
</div>
</div>

{# "Real" employee records objects #}
<div class="employee-records-list">
{% if employee_records_list %}
{% for employee_record in navigation_pages %}
{% include "employee_record/includes/list_item.html" with employee_record=employee_record item=employee_record.job_application current_url=request.get_full_path only %}
{% endfor %}
{# New employee records i.e. job applications #}
{% else %}
{% for job_application in navigation_pages %}
{% include "employee_record/includes/list_item.html" with employee_record=None item=job_application only %}
{% endfor %}
{% endif %}
</div>

{% if not navigation_pages %}
<div class="c-box c-box--results my-3 my-md-4">
<div class="c-box--results__body">
<p class="mb-0">Aucune fiche salarié avec le statut selectionné.</p>
</div>
</div>
{% endif %}
{% include "includes/pagination.html" with page=navigation_pages %}
</div>
<div class="col-12 col-md-8">{% include "employee_record/includes/list_results.html" %}</div>
</div>
</div>
</section>
{% endblock %}

{% block script %}
{{ block.super }}
<script src='{% static "js/htmx_compat.js" %}'></script>
<!-- Needed to use Select2MultipleWidget. -->
{{ filters_form.media.js }}
<script nonce="{{ CSP_NONCE }}">
$("#asideFiltersCollapse :input").change(function() {
$("#asideFiltersCollapse form").submit();
});
$("#order-form-group :input").click(function(event) {
let input = $("#id_order")
input.val(event.target.value); // Fill the hidden order input of the form
input.change(); // Fire a change event to notify handlers
htmx.onLoad(function(target) {
const orderFormGroup = target.querySelector("#order-form-group");
if (orderFormGroup) {
orderFormGroup.addEventListener("click", function(event) {
const orderHidden = document.getElementById("id_order");
orderHidden.value = event.target.value;
orderHidden.dispatchEvent(new Event("change", {
bubbles: true
}));
});
}
});
</script>
{% endblock %}
2 changes: 1 addition & 1 deletion itou/www/employee_record_views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def list_employee_records(request, template_name="employee_record/list.html"):
"back_url": reverse("dashboard:index"),
}

return render(request, template_name, context)
return render(request, "employee_record/includes/list_results.html" if request.htmx else template_name, context)


@login_required
Expand Down
37 changes: 37 additions & 0 deletions tests/www/employee_record_views/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from tests.employee_record import factories as employee_record_factories
from tests.employee_record.factories import EmployeeRecordFactory
from tests.job_applications.factories import JobApplicationWithApprovalNotCancellableFactory
from tests.utils.htmx.test import assertSoupEqual, update_page_with_htmx
from tests.utils.test import BASE_NUM_QUERIES, TestCase, assert_previous_step, parse_response_to_soup


Expand Down Expand Up @@ -488,6 +489,42 @@ def test_display_result_count(self):
response = self.client.get(self.URL + "?status=READY")
self.assertContains(response, "0 résultat")

def test_htmx(self):
self.client.force_login(self.user)
response = self.client.get(self.URL, {"status": "NEW"})
simulated_page = parse_response_to_soup(response)

[new_status] = simulated_page.find_all("input", attrs={"name": "status", "value": "NEW"})
del new_status["checked"]
[ready_status] = simulated_page.find_all("input", attrs={"name": "status", "value": "READY"})
ready_status["checked"] = ""

response = self.client.get(self.URL, {"status": "READY"}, headers={"HX-Request": "true"})
update_page_with_htmx(simulated_page, f"form[hx-get='{self.URL}']", response)

response = self.client.get(self.URL + "?status=READY")
fresh_page = parse_response_to_soup(response)
assertSoupEqual(simulated_page, fresh_page)

def test_htmx_new_employee_record_updates_badge_count(self):
self.client.force_login(self.user)
response = self.client.get(self.URL, {"status": "NEW"})
simulated_page = parse_response_to_soup(response)
# This new application should update the counter badge on NEW.
JobApplicationWithApprovalNotCancellableFactory(to_company=self.company)

[new_status] = simulated_page.find_all("input", attrs={"name": "status", "value": "NEW"})
del new_status["checked"]
[ready_status] = simulated_page.find_all("input", attrs={"name": "status", "value": "READY"})
ready_status["checked"] = ""

response = self.client.get(self.URL, {"status": "READY"}, headers={"HX-Request": "true"})
update_page_with_htmx(simulated_page, f"form[hx-get='{self.URL}']", response)

response = self.client.get(self.URL + "?status=READY")
fresh_page = parse_response_to_soup(response)
assertSoupEqual(simulated_page, fresh_page)


def test_an_active_siae_without_convention_can_not_access_the_view(client):
siae = CompanyFactory(
Expand Down

0 comments on commit cc25520

Please sign in to comment.