Skip to content

Commit

Permalink
job_seekers_views: add list for prescribers
Browse files Browse the repository at this point in the history
  • Loading branch information
xavfernandez committed Sep 3, 2024
1 parent bf1f2fe commit 5247815
Show file tree
Hide file tree
Showing 12 changed files with 1,021 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<span class="h4 mb-0">Candidats</span>
</div>
<div class="px-3 px-lg-4">
<a href="{% url 'job_seekers_views:list' %}" class="btn btn-outline-primary btn-block btn-ico mb-3">
<i class="ri-user-line ri-lg font-weight-normal"></i>
<span>Liste de mes candidats</span>
</a>
<ul class="list-unstyled mb-lg-5">
<li class="d-flex justify-content-between align-items-center mb-3">
<a href="{% url 'approvals:prolongation_requests_list' %}?only_pending=on" class="btn-link btn-ico">
Expand Down
5 changes: 5 additions & 0 deletions itou/templates/job_seekers_views/includes/list_counter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% load str_filters %}

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

<section aria-labelledby="job-seekers-list-count" id="job-seekers-section">
{% if not page_obj %}
<div class="text-center my-3 my-md-4">
<img class="img-fluid" src="{% static 'img/illustration-siae-card-no-result.svg' %}" alt="" loading="lazy">
<p class="mb-1 mt-3">
<strong>Aucun candidat pour le moment</strong>
</p>
</div>
{% else %}
<div class="mt-3 mt-md-4">
<table class="table">
<caption class="visually-hidden">Liste des candidats</caption>
<thead>
<tr>
<th scope="col">Prénom NOM</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>
</tr>
</thead>
<tbody>
{% for job_seeker in page_obj %}
<tr>
<td>
<a href="{% url "job_seekers_views:details" public_id=job_seeker.public_id %}?back_url={{ request.get_full_path|urlencode }}" class="btn-link">{{ job_seeker.get_full_name|mask_unless:job_seeker.user_can_view_personal_information }}</a>
</td>
<td>
{% include "apply/includes/eligibility_badge.html" with job_seeker=job_seeker is_subject_to_eligibility_rules=True eligibility_diagnosis=job_seeker.valid_eligibility_diagnosis only %}
</td>
<td>{{ job_seeker.job_applications_nb }}</td>
<td>{{ job_seeker.last_updated_at|date:"d/m/Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

{% include "includes/pagination.html" with page=page_obj boost=True boost_target="#job-seekers-section" boost_indicator="#job-seekers-section" %}
{% endif %}
</section>

{% if request.htmx %}
{% include "job_seekers_views/includes/list_counter.html" with paginator=paginator request=request only %}
{% endif %}
49 changes: 49 additions & 0 deletions itou/templates/job_seekers_views/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{% extends "layout/base.html" %}
{% load django_bootstrap5 %}
{% load matomo %}
{% load static %}
{% load str_filters %}
{% load format_filters %}

{% block title %}Candidats {{ block.super }}{% endblock %}

{% block title_content %}
<div class="d-flex flex-column flex-md-row gap-3 justify-content-md-between">
<h1 class="m-0">Candidats</h1>
<div class="d-flex flex-column flex-md-row gap-3" role="group" aria-label="Actions sur les candidatures">
<a href="{% url 'search:employers_results' %}" class="btn btn-lg btn-primary btn-ico">
<i class="ri-draft-line font-weight-medium" aria-hidden="true"></i>
<span>Postuler pour un candidat</span>
</a>
</div>
</div>
{% endblock %}

{% block title_prevstep %}
{% include "layout/previous_step.html" with back_url=back_url only %}
{% endblock %}

{% block content %}
<section class="s-section">
<div class="s-section__container container">
<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 "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">
{% bootstrap_field filters_form.job_seeker wrapper_class="w-lg-400px" show_label=False %}
</form>
</div>
</div>
{% include "job_seekers_views/includes/list_results.html" with page_obj=page_obj request=request only %}
</div>
</div>
</div>
</section>
{% endblock %}

{% block script %}
{{ block.super }}
<script src='{% static "js/htmx_compat.js" %}'></script>
{% endblock %}
10 changes: 10 additions & 0 deletions itou/utils/templatetags/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ def __repr__(self):
matomo_event_option="mes-candidatures",
),
# Prescribers.
"prescriber-jobseekers": NavItem(
label="Candidats",
icon="ri-user-line",
target=reverse("job_seekers_views:list"),
active_view_names=["job_seekers_views:list"],
matomo_event_category="offcanvasNav",
matomo_event_name="clic",
matomo_event_option="candidats",
),
"prescriber-job-apps": NavItem(
label="Candidatures",
icon="ri-draft-line",
Expand Down Expand Up @@ -187,6 +196,7 @@ def nav(request):
menu_items.append(NAV_ENTRIES["job-seeker-job-apps"])
elif request.user.is_prescriber:
menu_items.append(NAV_ENTRIES["prescriber-job-apps"])
menu_items.append(NAV_ENTRIES["prescriber-jobseekers"])
if request.current_organization:
menu_items.append(
NavGroup(
Expand Down
22 changes: 22 additions & 0 deletions itou/www/job_seekers_views/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django import forms
from django_select2.forms import Select2Widget


class FilterForm(forms.Form):
job_seeker = forms.ChoiceField(
required=False,
label="Nom",
widget=Select2Widget(
attrs={
"data-placeholder": "Nom du salarié",
}
),
)

def __init__(self, job_seeker_qs, data, *args, **kwargs):
super().__init__(data, *args, **kwargs)
self.fields["job_seeker"].choices = [
(job_seeker.pk, job_seeker.get_full_name())
for job_seeker in job_seeker_qs.order_by("first_name", "last_name")
if job_seeker.get_full_name()
]
1 change: 1 addition & 0 deletions itou/www/job_seekers_views/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

urlpatterns = [
path("details/<uuid:public_id>", views.JobSeekerDetailView.as_view(), name="details"),
path("list", views.JobSeekerListView.as_view(), name="list"),
]
83 changes: 82 additions & 1 deletion itou/www/job_seekers_views/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import DetailView
from django.db.models import Count, DateTimeField, Exists, IntegerField, Max, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.views.generic import DetailView, ListView

from itou.companies.enums import CompanyKind
from itou.eligibility.models.geiq import GEIQEligibilityDiagnosis
from itou.eligibility.models.iae import EligibilityDiagnosis
from itou.job_applications.models import JobApplication
from itou.users.enums import UserKind
from itou.users.models import User
from itou.utils.pagination import ItouPaginator
from itou.utils.urls import get_safe_url

from .forms import FilterForm


class JobSeekerDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
model = User
Expand Down Expand Up @@ -63,3 +70,77 @@ def get_context_data(self, **kwargs):
"can_view_personal_information": self.request.user.can_view_personal_information(self.object),
"can_edit_personal_information": self.request.user.can_edit_personal_information(self.object),
}


class JobSeekerListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
model = User
queryset = (
User.objects.filter(kind=UserKind.JOB_SEEKER).order_by("first_name", "last_name").prefetch_related("approvals")
)
paginate_by = 10
paginator_class = ItouPaginator

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

def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
if self.test_func():
self.form = FilterForm(
User.objects.filter(kind=UserKind.JOB_SEEKER).filter(Exists(self._get_user_job_applications())),
self.request.GET or None,
)

def test_func(self):
return self.request.user.is_authenticated and self.request.user.is_prescriber

def get_template_names(self):
return ["job_seekers_views/includes/list_results.html" if self.request.htmx else "job_seekers_views/list.html"]

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
page_obj = context["page_obj"]
if page_obj is not None:
for job_seeker in page_obj:
job_seeker.user_can_view_personal_information = self.request.user.can_view_personal_information(
job_seeker
)
return context

def _get_user_job_applications(self):
return JobApplication.objects.prescriptions_of(self.request.user, self.request.current_organization).filter(
job_seeker=OuterRef("pk")
)

def get_queryset(self):
queryset = super().get_queryset()
user_applications = self._get_user_job_applications()
subquery_count = Subquery(
user_applications.values("job_seeker").annotate(count=Count("pk")).values("count"),
output_field=IntegerField(),
)
subquery_last_update = Subquery(
user_applications.values("job_seeker").annotate(last_update=Max("updated_at")).values("last_update"),
output_field=DateTimeField(),
)
subquery_diagnosis = Subquery(
(
EligibilityDiagnosis.objects.valid()
.for_job_seeker_and_siae(job_seeker=OuterRef("pk"), siae=None)
.values("id")[:1]
),
output_field=IntegerField(),
)
query = queryset.filter(Exists(user_applications)).annotate(
job_applications_nb=Coalesce(subquery_count, 0),
last_updated_at=subquery_last_update,
valid_eligibility_diagnosis=subquery_diagnosis,
)

if self.form.is_valid() and (job_seeker_pk := self.form.cleaned_data["job_seeker"]):
query = query.filter(pk=job_seeker_pk)

return query
3 changes: 2 additions & 1 deletion tests/job_applications/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
JobSeekerFactory,
PrescriberFactory,
)
from tests.utils.factory_boy import AutoNowOverrideMixin


class JobApplicationFactory(factory.django.DjangoModelFactory):
class JobApplicationFactory(AutoNowOverrideMixin, factory.django.DjangoModelFactory):
class Meta:
model = models.JobApplication
skip_postgeneration_save = True
Expand Down
8 changes: 8 additions & 0 deletions tests/www/dashboard/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ def test_dashboard_for_prescriber(self):
response = self.client.get(reverse("dashboard:index"))
self.assertContains(response, format_siret(prescriber_organization.siret))

def test_dashboard_for_authorized_prescriber(self):
prescriber_organization = prescribers_factories.PrescriberOrganizationWithMembershipFactory(authorized=True)
self.client.force_login(prescriber_organization.members.first())

response = self.client.get(reverse("dashboard:index"))
self.assertContains(response, format_siret(prescriber_organization.siret))
self.assertContains(response, "Liste de mes candidats")

def test_dashboard_displays_asp_badge(self):
company = CompanyFactory(kind=CompanyKind.EI, with_membership=True)
other_company = CompanyFactory(kind=CompanyKind.ETTI, with_membership=True)
Expand Down
Loading

0 comments on commit 5247815

Please sign in to comment.