Repository: gip-inclusion/les-emplois
Pull request #6039: Candidature: Travail préparatoire en vue de refondre la session 3c: paramètres d'url et gestion des sessions
Author: login: tonial
Labels: name: no-changelog
description: Ne doit pas figurer dans le journal des changements.
Created: 2025-04-26T07:58:57Z | Merged: 2025-05-12T14:07:29Z

## :thinking: Pourquoi ?

Encore du travail préparatoire...
Normalement après ça il ne reste qu'à passer les sessions en uuid :crossed_fingers:  

~~Désolé pour le grand nombre de commits.
Les changements sont normalement les plus petits possibles pour chaque commit.~~

Découpé en 3, les 2 premières parties sont à présent ici : 
https://github.com/gip-inclusion/les-emplois/pull/6083
https://github.com/gip-inclusion/les-emplois/pull/6086

Ici ça reste un peu du mélange :
- j'introduis des méthodes qui permettront de passer les paramètres d'urls (`company_pk` ou `session_uuid`) en fonction de l'url le temps de la bascule (utilisé dans la PR #6065)
Ces méthodes seront supprimées quand on aura fait la bascule.
- je saute l'étape d'eligibilité dans le parcours `for_hire` (comme pour le parcours d'eligibilté dans le parcours normal)
- je nettoie la session lorsque le candidat est embauché (parcours `for_hire`)
- Je refonds le code de récupération des sessions dans les tests pour préparer au passage en session uuid dans #6065

## :cake: Comment ? <!-- optionnel -->

> _Décrivez en quelques mots la solution retenue et mise en oeuvre, les difficultés ou problèmes rencontrés. Attirez l'attention sur les décisions d'architecture ou de conception importantes._

## :rotating_light: À vérifier

- [ ] Mettre à jour le CHANGELOG_breaking_changes.md ?
- [ ] Ajouter l'étiquette « Bug » ?

## :desert_island: Comment tester ?

> _Les instructions pour reproduire le problème, les profils de test, le parcours spécifique à utiliser, etc. Si vous disposez d'une recette jetable, mettre l'URL pour tester dans cette partie._

## :computer: Captures d'écran <!-- optionnel -->

<!--
# Catégories changelog

 +-------------------------|--------------------------+
| API                      | Interface                |
| Accessibilité            | Notifications            |
| Admin                    | Page d’accueil           |
| Annexes financières      | PASS IAE                 |
| Candidature              | Performances             |
| Connexion                | Pilotage                 |
| Contrôle a posteriori    | Prescripteur             |
| Demandes de prolongation | Profil salarié           |
| Demandeur d’emploi       | Recherche employeur      |
| Employeur                | Recherche fiche de poste |
| Fiche de poste           | Recherche prescripteur   |
| Fiche entreprise         | Stabilité                |
| Fiches salarié           | Statistiques             |
| GEIQ                     | Tableau de bord          |
| Inscription              | Vie privée               |
+--------------------------|--------------------------+
-->



Diff:
diff --git a/itou/www/apply/views/common.py b/itou/www/apply/views/common.py
index 25481474501..827da17a94b 100644
--- a/itou/www/apply/views/common.py
+++ b/itou/www/apply/views/common.py
@@ -32,7 +32,17 @@
 from itou.www.geiq_eligibility_views.forms import GEIQAdministrativeCriteriaForGEIQForm
 
 
-def _accept(request, company, job_seeker, error_url, back_url, template_name, extra_context, job_application=None):
+def _accept(
+    request,
+    company,
+    job_seeker,
+    error_url,
+    back_url,
+    template_name,
+    extra_context,
+    job_application=None,
+    session=None,
+):
     forms = []
 
     # Ask the SIAE to verify the job seeker's Pôle emploi status.
@@ -163,6 +173,9 @@ def _accept(request, company, job_seeker, error_url, back_url, template_name, ex
         else:
             final_url = reverse("apply:details_for_company", kwargs={"job_application_id": job_application.pk})
 
+        if session:
+            session.delete()
+
         return HttpResponseClientRedirect(final_url)
 
     if request.htmx:
diff --git a/itou/www/apply/views/process_views.py b/itou/www/apply/views/process_views.py
index 102a83c2b15..136f954b977 100644
--- a/itou/www/apply/views/process_views.py
+++ b/itou/www/apply/views/process_views.py
@@ -680,10 +680,7 @@ def get_initial(self):
     def get_next_url(self):
         base_url = reverse(
             "apply:job_application_external_transfer_step_3",
-            kwargs={
-                "job_application_id": self.job_application.pk,
-                "company_pk": self.company.pk,
-            },
+            kwargs=self.get_base_kwargs() | {"job_application_id": self.job_application.pk},
         )
         return f"{base_url}?back_url={quote(self.request.get_full_path())}"
 
@@ -709,7 +706,7 @@ def dispatch(self, request, *args, **kwargs):
             return HttpResponseRedirect(
                 reverse(
                     "apply:job_application_external_transfer_step_2",
-                    kwargs={"job_application_id": self.job_application.pk, "company_pk": self.company.pk},
+                    kwargs=self.get_base_kwargs() | {"job_application_id": self.job_application.pk},
                 )
             )
         return super().dispatch(request, *args, **kwargs)
diff --git a/itou/www/apply/views/submit_views.py b/itou/www/apply/views/submit_views.py
index e65f4d2a0d0..b19a609812b 100644
--- a/itou/www/apply/views/submit_views.py
+++ b/itou/www/apply/views/submit_views.py
@@ -251,6 +251,9 @@ def get_context_data(self, **kwargs):
             "reset_url": self.get_reset_url(),
         }
 
+    def get_base_kwargs(self):
+        return {"company_pk": self.company.pk}
+
 
 class ApplyStepBaseView(RequireApplySessionMixin, ApplicationPermissionMixin, TemplateView):
     def __init__(self):
@@ -316,7 +319,7 @@ def get_eligibility_step_url(self):
         if self.company.kind == CompanyKind.GEIQ and self.request.from_authorized_prescriber:
             return reverse(
                 "apply:application_geiq_eligibility",
-                kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
             )
         if self.company.kind != CompanyKind.GEIQ:
             bypass_eligibility_conditions = [
@@ -330,7 +333,30 @@ def get_eligibility_step_url(self):
             if not any(bypass_eligibility_conditions):
                 return reverse(
                     "apply:application_eligibility",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+        return None
+
+    def get_eligibility_for_hire_step_url(self):
+        if self.company.kind == CompanyKind.GEIQ:
+            if not GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(self.job_seeker, self.company).exists():
+                return reverse(
+                    "apply:geiq_eligibility_for_hire",
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+        else:
+            bypass_eligibility_conditions = [
+                # Don't perform an eligibility diagnosis if the SIAE doesn't need it,
+                not self.company.is_subject_to_eligibility_rules,
+                # No need for eligibility diagnosis if the job seeker already has a PASS IAE
+                self.job_seeker.has_valid_approval,
+                # Job seeker must not have a diagnosis
+                self.job_seeker.has_valid_diagnosis(for_siae=self.company),
+            ]
+            if not any(bypass_eligibility_conditions):
+                return reverse(
+                    "apply:eligibility_for_hire",
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
                 )
         return None
 
@@ -374,7 +400,7 @@ def redirect_to_check_infos(self, job_seeker_public_id):
             else "job_seekers_views:check_job_seeker_info"
         )
         return HttpResponseRedirect(
-            reverse(view_name, kwargs={"company_pk": self.company.pk, "job_seeker_public_id": job_seeker_public_id})
+            reverse(view_name, kwargs=self.get_base_kwargs() | {"job_seeker_public_id": job_seeker_public_id})
         )
 
 
@@ -404,16 +430,13 @@ class CheckPreviousApplications(ApplicationBaseView):
 
     def get_next_url(self):
         if self.hire_process:
-            view_name = (
-                "apply:geiq_eligibility_for_hire"
-                if self.company.kind == CompanyKind.GEIQ
-                else "apply:eligibility_for_hire"
+            return self.get_eligibility_for_hire_step_url() or reverse(
+                "apply:hire_confirmation",
+                kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
             )
         else:
             view_name = "apply:application_jobs"
-        return reverse(
-            view_name, kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id}
-        )
+        return reverse(view_name, kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id})
 
     def get(self, request, *args, **kwargs):
         if not self.get_previous_applications_queryset().exists():
@@ -456,7 +479,7 @@ def setup(self, request, *args, **kwargs):
     def get_next_url(self):
         return self.get_eligibility_step_url() or reverse(
             "apply:application_resume",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def post(self, request, *args, **kwargs):
@@ -480,10 +503,7 @@ class CheckApplySessionMixin:
     def get_redirect_url(self):
         return reverse(
             "apply:application_jobs",
-            kwargs={
-                "company_pk": self.company.pk,
-                "job_seeker_public_id": self.job_seeker.public_id,
-            },
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def dispatch(self, request, *args, **kwargs):
@@ -535,7 +555,7 @@ def setup(self, request, *args, **kwargs):
     def get_next_url(self):
         return reverse(
             "apply:application_resume",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def dispatch(self, request, *args, **kwargs):
@@ -570,7 +590,7 @@ def get_context_data(self, **kwargs):
         context["job_seeker"] = self.job_seeker
         context["back_url"] = reverse(
             "apply:application_jobs",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
         context["full_content_width"] = True
         if self.eligibility_diagnosis:
@@ -600,7 +620,7 @@ def setup(self, request, *args, **kwargs):
             ),
             form_url=reverse(
                 "apply:application_geiq_eligibility",
-                kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
             ),
             data=request.POST or None,
         )
@@ -608,7 +628,7 @@ def setup(self, request, *args, **kwargs):
     def get_next_url(self):
         return reverse(
             "apply:application_resume",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def dispatch(self, request, *args, **kwargs):
@@ -621,7 +641,7 @@ def get_context_data(self, **kwargs):
         return super().get_context_data(**kwargs) | {
             "back_url": reverse(
                 "apply:application_jobs",
-                kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
             ),
             "form": self.form,
             "full_content_width": True,
@@ -739,7 +759,7 @@ def post(self, request, *args, **kwargs):
     def get_back_url(self):
         return self.get_eligibility_step_url() or reverse(
             "apply:application_jobs",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def get_context_data(self, **kwargs):
@@ -800,26 +820,20 @@ class IAEEligibilityForHireView(ApplicationBaseView, common_views.BaseIAEEligibi
     template_name = "apply/submit/eligibility_for_hire.html"
 
     def dispatch(self, request, *args, **kwargs):
-        bypass_eligibility_conditions = [
-            # Don't perform an eligibility diagnosis if the SIAE doesn't need it,
-            not self.company.is_subject_to_eligibility_rules,
-            # No need for eligibility diagnosis if the job seeker already has a PASS IAE
-            self.job_seeker.has_valid_approval,
-        ]
-        if any(bypass_eligibility_conditions) or self.job_seeker.has_valid_diagnosis(for_siae=self.company):
+        if self.get_eligibility_for_hire_step_url() is None:
             return HttpResponseRedirect(self.get_success_url())
         return super().dispatch(request, *args, **kwargs)
 
     def get_success_url(self):
         return reverse(
             "apply:hire_confirmation",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def get_cancel_url(self):
         return reverse(
             "job_seekers_views:check_job_seeker_info_for_hire",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def get_context_data(self, **kwargs):
@@ -836,24 +850,24 @@ def setup(self, request, *args, **kwargs):
 
         self.geiq_eligibility_criteria_url = reverse(
             "apply:geiq_eligibility_criteria_for_hire",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def dispatch(self, request, *args, **kwargs):
-        if GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(self.job_seeker, self.company).exists():
+        if self.get_eligibility_for_hire_step_url() is None:
             return HttpResponseRedirect(self.get_next_url())
         return super().dispatch(request, *args, **kwargs)
 
     def get_next_url(self):
         return reverse(
             "apply:hire_confirmation",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def get_back_url(self):
         return reverse(
             "job_seekers_views:check_job_seeker_info_for_hire",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def get_context_data(self, **kwargs):
@@ -874,6 +888,11 @@ def hire_confirmation(
     job_seeker_public_id,
     template_name="apply/submit/hire_confirmation.html",
 ):
+    # RequireApplySessionMixin
+    apply_session = SessionNamespace(request.session, APPLY_SESSION_KIND, f"job_application-{company_pk}")
+    if not apply_session.exists():
+        raise Http404
+
     company = get_object_or_404(
         Company.objects.filter(pk__in={org.pk for org in request.organizations}), pk=company_pk
     )
@@ -907,9 +926,8 @@ def hire_confirmation(
         request,
         company,
         job_seeker,
-        error_url=reverse(
-            "apply:hire_confirmation", kwargs={"company_pk": company_pk, "job_seeker_public_id": job_seeker_public_id}
-        ),
+        error_url=request.get_full_path(),
+        # FIXME(alaurent) I doubt we should go back there...
         back_url=reverse(
             "job_seekers_views:check_job_seeker_info_for_hire",
             kwargs={"company_pk": company.pk, "job_seeker_public_id": job_seeker.public_id},
@@ -923,6 +941,7 @@ def hire_confirmation(
             "expired_eligibility_diagnosis": None,  # XXX: should we search for an expired diagnosis here ?
             "is_subject_to_geiq_eligibility_rules": company.kind == CompanyKind.GEIQ,
         },
+        session=apply_session,
     )
 
 
diff --git a/itou/www/job_seekers_views/views.py b/itou/www/job_seekers_views/views.py
index cbec807c84a..4a0301130cb 100644
--- a/itou/www/job_seekers_views/views.py
+++ b/itou/www/job_seekers_views/views.py
@@ -302,6 +302,9 @@ def setup(self, request, *args, hire_process=False, **kwargs):
             and self.company == request.current_organization
         )
 
+    def get_apply_kwargs(self):
+        return self.job_seeker_session.get("apply", {})
+
     def get_exit_url(self, job_seeker, created=False):
         if self.is_gps:
             return reverse("gps:group_list")
@@ -314,7 +317,7 @@ def get_exit_url(self, job_seeker, created=False):
         if self.standalone_creation:
             return reverse("job_seekers_views:details", kwargs={"public_id": job_seeker.public_id})
 
-        kwargs = {"company_pk": self.company.pk, "job_seeker_public_id": job_seeker.public_id}
+        kwargs = self.get_apply_kwargs() | {"job_seeker_public_id": job_seeker.public_id}
         if created and self.hire_process:
             # The job seeker was just created, we don't need to check info if we are hiring
             if self.company.kind == CompanyKind.GEIQ:
@@ -388,19 +391,22 @@ def dispatch(self, request, *args, **kwargs):
     def get(self, request, *args, **kwargs):
         # The NIR already exists, go to next step
         if self.job_seeker.jobseeker_profile.nir:
+            next_url = reverse(
+                "job_seekers_views:check_job_seeker_info",
+                kwargs=self.get_apply_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+            )  # get_apply_kwargs requires the job_seeker_session
             # TODO(ewen): check_job_seeker_info doesn't use the session yet,
             # so we delete the session here.
             self.job_seeker_session.delete()
-            return HttpResponseRedirect(
-                reverse(
-                    "job_seekers_views:check_job_seeker_info",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
-                )
-            )
+            return HttpResponseRedirect(next_url)
 
         return super().get(request, *args, **kwargs)
 
     def post(self, request, *args, **kwargs):
+        next_url = reverse(
+            "job_seekers_views:check_job_seeker_info",
+            kwargs=self.get_apply_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+        )
         if self.form.is_valid():
             self.job_seeker.jobseeker_profile.nir = self.form.cleaned_data["nir"]
             self.job_seeker.jobseeker_profile.lack_of_nir_reason = ""
@@ -408,17 +414,8 @@ def post(self, request, *args, **kwargs):
             # TODO(ewen): check_job_seeker_info doesn't use the session yet,
             # so we delete the session here.
             self.job_seeker_session.delete()
-            return HttpResponseRedirect(
-                reverse(
-                    "job_seekers_views:check_job_seeker_info",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
-                )
-            )
+            return HttpResponseRedirect(next_url)
         else:
-            next_url = reverse(
-                "job_seekers_views:check_job_seeker_info",
-                kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
-            )
             kwargs["temporary_nir_url"] = next_url
 
         return self.render_to_response(self.get_context_data(**kwargs))
@@ -839,8 +836,8 @@ def post(self, request, *args, **kwargs):
             else:
                 user.save()
 
-            self.job_seeker_session.delete()
             url = self.get_exit_url(self.profile.user, created=True)
+            self.job_seeker_session.delete()
 
             if self.is_gps:
                 notify_duplicate = (
@@ -1195,7 +1192,7 @@ def setup(self, request, *args, **kwargs):
     def get_redirect_url(self):
         return reverse(
             "apply:step_check_prev_applications",
-            kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
         )
 
     def get(self, request, *args, **kwargs):
diff --git a/tests/gps/test_views.py b/tests/gps/test_views.py
index 2cb882b87ed..39c5825f3d7 100644
--- a/tests/gps/test_views.py
+++ b/tests/gps/test_views.py
@@ -19,6 +19,7 @@
 from itou.utils.templatetags.str_filters import mask_unless
 from itou.utils.urls import get_absolute_url
 from itou.www.gps.enums import EndReason
+from itou.www.job_seekers_views.enums import JobSeekerSessionKinds
 from tests.cities.factories import create_city_geispolsheim
 from tests.companies.factories import CompanyFactory, CompanyMembershipFactory
 from tests.gps.factories import FollowUpGroupFactory, FollowUpGroupMembershipFactory
@@ -30,7 +31,7 @@
     PrescriberFactory,
 )
 from tests.utils.htmx.test import assertSoupEqual, update_page_with_htmx
-from tests.utils.test import assertSnapshotQueries, parse_response_to_soup, session_data_without_known_keys
+from tests.utils.test import assertSnapshotQueries, get_session_name, parse_response_to_soup
 
 
 def assert_new_beneficiary_toast(response, job_seeker, can_view_personal_info=True):
@@ -1190,7 +1191,7 @@ def test_view(self, client, snapshot):
         # unknown NIR :
         dummy_job_seeker = JobSeekerFactory.build(for_snapshot=True)
         response = client.post(self.URL, data={"nir": dummy_job_seeker.jobseeker_profile.nir, "preview": "1"})
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse(
             "job_seekers_views:search_by_email_for_sender",
             kwargs={"session_uuid": job_seeker_session_name},
@@ -1239,7 +1240,7 @@ def test_unknown_nir_known_email_with_no_nir(self, client, snapshot):
         # ----------------------------------------------------------------------
         response = client.post(self.URL, data={"nir": nir, "preview": "1"})
 
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse(
             "job_seekers_views:search_by_email_for_sender",
             kwargs={"session_uuid": job_seeker_session_name},
@@ -1298,7 +1299,7 @@ def test_unknown_nir_known_email_with_another_nir(self, client, snapshot):
         # ----------------------------------------------------------------------
         response = client.post(self.URL, data={"nir": nir, "preview": "1"})
 
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse(
             "job_seekers_views:search_by_email_for_sender",
             kwargs={"session_uuid": job_seeker_session_name},
@@ -1356,7 +1357,7 @@ def test_unknown_nir_and_unknown_email(self, client, settings, mocker):
         # ----------------------------------------------------------------------
         response = client.post(self.URL, data={"nir": dummy_job_seeker.jobseeker_profile.nir, "preview": "1"})
 
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse(
             "job_seekers_views:search_by_email_for_sender",
             kwargs={"session_uuid": job_seeker_session_name},
@@ -1558,7 +1559,7 @@ def test_unknown_email(self, client, settings, mocker, known_name):
             "preview": "1",
         }
         response = client.post(self.URL, data=post_data)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse(
             "job_seekers_views:create_job_seeker_step_1_for_sender",
             kwargs={"session_uuid": job_seeker_session_name},
diff --git a/tests/utils/test.py b/tests/utils/test.py
index 611221f4608..615149e6201 100644
--- a/tests/utils/test.py
+++ b/tests/utils/test.py
@@ -15,6 +15,7 @@
 from django.conf import Path, settings
 from django.db import DEFAULT_DB_ALIAS, connections
 from django.db.backends.utils import CursorDebugWrapper
+from django.http import Http404
 from django.template import Template
 from django.template.base import Node
 from django.template.loader import render_to_string
@@ -23,6 +24,7 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
@@ -30,14 +32,34 @@
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
-    for k in list(data):
+def session_names_without_known_keys(session):
+    session_names = []
+    for k in session.keys():
+        if k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
+            continue
         if k.endswith("_session_kind"):
-            del data[k]
-    return data
+            continue
+        session_names.append(k)
+    return session_names
+
+
+def get_session_name(session, session_kind, ignore=()):
+    data = session_names_without_known_keys(session)
+    found_names = []
+    for session_name in data:
+        if session_name in ignore:
+            continue
+        try:
+            named_session = SessionNamespace(session, session_kind, session_name)
+            named_session.verify_kind()
+            found_names.append(session_name)
+        except Http404:
+            pass
+    if len(found_names) > 1:
+        raise ValueError("Too many sessions with this kind")
+    if len(found_names) == 1:
+        return found_names[0]
+    return None
 
 
 def pprint_html(response, **selectors):
diff --git a/tests/www/apply/test_batch_views.py b/tests/www/apply/test_batch_views.py
index b02b74369d5..404e33c4d41 100644
--- a/tests/www/apply/test_batch_views.py
+++ b/tests/www/apply/test_batch_views.py
@@ -11,6 +11,7 @@
 from itou.job_applications import enums as job_applications_enums
 from itou.job_applications.enums import JobApplicationState, SenderKind
 from itou.utils.urls import add_url_params
+from itou.www.apply.views.batch_views import RefuseWizardView
 from tests.companies.factories import CompanyFactory, CompanyMembershipFactory
 from tests.job_applications.factories import (
     JobApplicationFactory,
@@ -19,7 +20,7 @@
     JobSeekerFactory,
     LaborInspectorFactory,
 )
-from tests.utils.test import session_data_without_known_keys
+from tests.utils.test import get_session_name
 
 
 class TestBatchArchive:
@@ -658,7 +659,7 @@ def test_single_app_from_orienter(self, client):
             add_url_params(reverse("apply:batch_refuse"), {"next_url": next_url}),
             data={"application_ids": [refusable_app.pk]},
         )
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         expected_session = {
             "config": {
                 "tunnel": "batch",
@@ -756,7 +757,7 @@ def test_multiple_apps_from_authorized_prescribers(self, client):
             add_url_params(reverse("apply:batch_refuse"), {"next_url": next_url}),
             data={"application_ids": [refusable_app.pk for refusable_app in refusable_apps]},
         )
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         expected_session = {
             "config": {
                 "tunnel": "batch",
@@ -848,7 +849,7 @@ def test_multiple_apps_from_same_job_seeker(self, client):
             add_url_params(reverse("apply:batch_refuse"), {"next_url": next_url}),
             data={"application_ids": [refusable_app.pk for refusable_app in refusable_apps]},
         )
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         expected_session = {
             "config": {
                 "tunnel": "batch",
@@ -933,7 +934,7 @@ def test_refuse_step_bypass(self, client, reason_is_optional):
             add_url_params(reverse("apply:batch_refuse"), {"next_url": next_url}),
             data={"application_ids": [refusable_app.pk]},
         )
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         expected_session = {
             "config": {
                 "tunnel": "batch",
@@ -991,7 +992,7 @@ def test_single_app_transferred_concurrently(self, client):
             add_url_params(reverse("apply:batch_refuse"), {"next_url": next_url}),
             data={"application_ids": [refusable_app.pk]},
         )
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         expected_session = {
             "config": {
                 "tunnel": "batch",
@@ -1058,7 +1059,7 @@ def test_multiple_apps_deleted_concurrently(self, client):
             add_url_params(reverse("apply:batch_refuse"), {"next_url": next_url}),
             data={"application_ids": [refusable_app.pk for refusable_app in refusable_apps]},
         )
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         expected_session = {
             "config": {
                 "tunnel": "batch",
diff --git a/tests/www/apply/test_process.py b/tests/www/apply/test_process.py
index f1f811e7717..86b1c44f4e7 100644
--- a/tests/www/apply/test_process.py
+++ b/tests/www/apply/test_process.py
@@ -49,6 +49,7 @@
 from itou.utils.urls import add_url_params
 from itou.utils.widgets import DuetDatePickerWidget
 from itou.www.apply.forms import AcceptForm
+from itou.www.apply.views.batch_views import RefuseWizardView
 from itou.www.apply.views.process_views import job_application_sender_left_org
 from tests.approvals.factories import ApprovalFactory, SuspensionFactory
 from tests.cities.factories import create_test_cities
@@ -74,8 +75,8 @@
 from tests.utils.test import (
     assert_previous_step,
     assertSnapshotQueries,
+    get_session_name,
     parse_response_to_soup,
-    session_data_without_known_keys,
 )
 
 
@@ -917,7 +918,7 @@ def test_refuse_session_prefix(self, client):
         url = reverse("apply:refuse", kwargs={"job_application_id": job_application.pk})
         response = client.get(url)
 
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         assert client.session[refuse_session_name] == {
             "config": {
                 "tunnel": "single",
@@ -951,9 +952,10 @@ def test_refuse_session_prefix(self, client):
         job_application_2 = JobApplicationFactory(to_company=job_application.to_company)
         url = reverse("apply:refuse", kwargs={"job_application_id": job_application_2.pk})
         response = client.get(url)
-        [refuse_session_name_2] = [
-            k for k in session_data_without_known_keys(client.session) if k != refuse_session_name
-        ]
+
+        refuse_session_name_2 = get_session_name(
+            client.session, RefuseWizardView.expected_session_kind, ignore=[refuse_session_name]
+        )
         refusal_reason_url_2 = reverse(
             "apply:batch_refuse_steps", kwargs={"session_uuid": refuse_session_name_2, "step": "reason"}
         )
@@ -987,7 +989,7 @@ def test_refuse_from_prescriber(self, client):
         client.force_login(employer)
 
         response = client.get(reverse("apply:refuse", kwargs={"job_application_id": job_application.pk}), follow=True)
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         refusal_reason_url = reverse(
             "apply:batch_refuse_steps", kwargs={"session_uuid": refuse_session_name, "step": "reason"}
         )
@@ -1049,7 +1051,7 @@ def test_refuse_from_job_seeker(self, client):
 
         refusal_reason_url = reverse("apply:refuse", kwargs={"job_application_id": job_application.pk})
         response = client.get(refusal_reason_url, follow=True)
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         refusal_reason_url = reverse(
             "apply:batch_refuse_steps", kwargs={"session_uuid": refuse_session_name, "step": "reason"}
         )
@@ -1102,7 +1104,7 @@ def test_refuse_labels_for_prescriber_or_orienteur(self, client):
 
         response = client.get(reverse("apply:refuse", kwargs={"job_application_id": job_application.pk}))
 
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         refusal_reason_url = reverse(
             "apply:batch_refuse_steps", kwargs={"session_uuid": refuse_session_name, "step": "reason"}
         )
@@ -1251,7 +1253,7 @@ def test_refuse_step_bypass(self, client):
 
         refusal_reason_url = reverse("apply:refuse", kwargs={"job_application_id": job_application.pk})
         response = client.get(refusal_reason_url, follow=True)
-        [refuse_session_name] = session_data_without_known_keys(client.session)
+        refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
         refusal_reason_url = reverse(
             "apply:batch_refuse_steps", kwargs={"session_uuid": refuse_session_name, "step": "reason"}
         )
@@ -3404,7 +3406,7 @@ def test_refuse_jobapplication_geiq_reasons(client, reason):
 
     url = reverse("apply:refuse", kwargs={"job_application_id": job_application.pk})
     response = client.get(url)
-    [refuse_session_name] = session_data_without_known_keys(client.session)
+    refuse_session_name = get_session_name(client.session, RefuseWizardView.expected_session_kind)
     assert client.session[refuse_session_name] == {
         "config": {
             "tunnel": "single",
diff --git a/tests/www/apply/test_submit.py b/tests/www/apply/test_submit.py
index 2878066ebc1..ba0412cf0b7 100644
--- a/tests/www/apply/test_submit.py
+++ b/tests/www/apply/test_submit.py
@@ -46,6 +46,7 @@
 from itou.utils.widgets import DuetDatePickerWidget
 from itou.www.apply.views import constants as apply_view_constants
 from itou.www.apply.views.submit_views import initialize_apply_session
+from itou.www.job_seekers_views.enums import JobSeekerSessionKinds
 from tests.approvals.factories import ApprovalFactory, PoleEmploiApprovalFactory
 from tests.cities.factories import create_city_geispolsheim, create_city_in_zrr, create_test_cities
 from tests.companies.factories import (
@@ -68,7 +69,7 @@
     PrescriberFactory,
 )
 from tests.users.test_models import user_with_approval_in_waiting_period
-from tests.utils.test import assertSnapshotQueries, parse_response_to_soup, session_data_without_known_keys
+from tests.utils.test import assertSnapshotQueries, get_session_name, parse_response_to_soup
 
 
 BACK_BUTTON_ARIA_LABEL = "Retourner à l’étape précédente"
@@ -84,6 +85,7 @@ def fake_session_initialization(client, company, data):
     # The first argument is supposed to be a request, but we only need it to have a session attribute so client works
     session = initialize_apply_session(client, company, data)
     session.save()
+    return session
 
 
 def assert_contains_apply_nir_modal(response, job_seeker, with_personal_information=True):
@@ -537,9 +539,7 @@ def test_check_nir_job_seeker_with_lack_of_nir_reason(client):
 
     response = client.get(reverse("apply:start", kwargs={"company_pk": company.pk}))
 
-    [job_seeker_session_name] = [
-        k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-    ]
+    job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.CHECK_NIR_JOB_SEEKER)
     next_url = reverse("job_seekers_views:check_nir_for_job_seeker", kwargs={"session_uuid": job_seeker_session_name})
     assertRedirects(response, next_url)
 
@@ -572,9 +572,7 @@ def test_apply_as_job_seeker_with_suspension_sanction(self, client):
         client.force_login(user)
 
         response = client.get(reverse("apply:start", kwargs={"company_pk": company.pk}))
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.CHECK_NIR_JOB_SEEKER)
         # The suspension does not prevent access to the process
         assertRedirects(
             response,
@@ -600,9 +598,7 @@ def test_apply_as_jobseeker(self, client, pdf_file):
             {"back_url": reset_url_company},
         )
 
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.CHECK_NIR_JOB_SEEKER)
         next_url = reverse(
             "job_seekers_views:check_nir_for_job_seeker", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -744,9 +740,7 @@ def test_apply_as_job_seeker_temporary_nir(self, client):
 
         # Follow all redirections until NIR.
         # ----------------------------------------------------------------------
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.CHECK_NIR_JOB_SEEKER)
         next_url = reverse(
             "job_seekers_views:check_nir_for_job_seeker", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -794,9 +788,7 @@ def test_apply_as_job_seeker_from_job_description(self, client):
             reverse("apply:start", kwargs={"company_pk": company.pk}),
             {"job_description_id": job_description.pk, "back_url": reset_url_job_description},
         )
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.CHECK_NIR_JOB_SEEKER)
         next_url = reverse(
             "job_seekers_views:check_nir_for_job_seeker", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -1040,9 +1032,7 @@ def test_apply_as_prescriber_with_pending_authorization(self, client, pdf_file):
         )
 
         response = client.get(next_url)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
         assertRedirects(response, next_url)
 
@@ -1322,9 +1312,7 @@ def test_apply_as_authorized_prescriber(self, client, pdf_file, snapshot):
         assertRedirects(response, next_url, target_status_code=302, fetch_redirect_response=False)
 
         response = client.get(next_url)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
         assertRedirects(response, next_url)
 
@@ -1648,9 +1636,7 @@ def test_cannot_create_job_seeker_with_pole_emploi_email(self, client):
         # Init session
         start_url = reverse("apply:start", kwargs={"company_pk": company.pk})
         client.get(start_url, follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         nir_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
         response = client.get(nir_url)
         assert response.status_code == 200
@@ -1715,9 +1701,7 @@ def test_apply_with_temporary_nir(self, client):
         assertRedirects(response, next_url, target_status_code=302, fetch_redirect_response=False)
 
         response = client.get(next_url)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         assertRedirects(
             response,
             reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name}),
@@ -1767,9 +1751,7 @@ def test_apply_as_prescriber_with_suspension_sanction(self, client):
         client.force_login(user)
 
         response = client.get(reverse("apply:start", kwargs={"company_pk": company.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
 
         # The suspension does not prevent the access to the process
         assertRedirects(
@@ -1812,9 +1794,7 @@ def test_apply_as_prescriber(self, client, pdf_file):
         assertRedirects(response, next_url, target_status_code=302, fetch_redirect_response=False)
 
         response = client.get(next_url)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
         assertRedirects(response, next_url)
 
@@ -2197,9 +2177,7 @@ def test_one_account_no_nir(self, client):
         # …until a job seeker has to be determined.
         assert response.status_code == 200
         last_url = response.redirect_chain[-1][0]
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         assert last_url == reverse(
             "job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -2284,9 +2262,7 @@ def test_one_account_lack_of_nir_reason(self, client):
         response = client.get(
             reverse("apply:start", kwargs={"company_pk": siae.pk}), {"back_url": reset_url_company}, follow=True
         )
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
 
         # …until a job seeker has to be determined.
         assert response.status_code == 200
@@ -2361,9 +2337,7 @@ def test_perms_for_company(self, client):
         client.force_login(user)
 
         response = client.get(reverse("apply:start", kwargs={"company_pk": company_2.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         assertRedirects(
             response,
             expected_url=reverse(
@@ -2413,9 +2387,7 @@ def _test_apply_as_company(self, client, user, company, dummy_job_seeker, pdf_fi
         assertRedirects(response, next_url, target_status_code=302, fetch_redirect_response=False)
 
         response = client.get(next_url)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
         assertRedirects(response, next_url)
 
@@ -2771,9 +2743,7 @@ def test_cannot_create_job_seeker_with_pole_emploi_email(self, client):
         # Init session
         start_url = reverse("apply:start", kwargs={"company_pk": company.pk})
         client.get(start_url, follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         nir_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
         response = client.get(nir_url)
         assert response.status_code == 200
@@ -2867,9 +2837,7 @@ def test_hire_as_company(self, client):
 
         # Init session
         response = client.get(reverse("apply:start_hire", kwargs={"company_pk": company.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         check_nir_url = reverse(
             "job_seekers_views:check_nir_for_hire", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -3187,9 +3155,7 @@ def test_hire_as_geiq(self, client):
 
         # Init session
         response = client.get(reverse("apply:start_hire", kwargs={"company_pk": company.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         check_nir_url = reverse(
             "job_seekers_views:check_nir_for_hire", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -3441,9 +3407,7 @@ def test_application_start_with_invalid_job_description_id(self, client):
         response = client.get(
             reverse("apply:start", kwargs={"company_pk": company.pk}), {"job_description_id": "invalid"}, follow=True
         )
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         assertRedirects(
             response,
             reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name}),
@@ -3807,10 +3771,7 @@ def setup_method(self, settings, mocker):
         self.start_url = add_url_params(reverse("job_seekers_views:update_job_seeker_start"), params)
 
     def get_job_seeker_session_key(self, client):
-        [job_seeker_session_key] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ] or [None]
-        return job_seeker_session_key
+        return get_session_name(client.session, JobSeekerSessionKinds.UPDATE)
 
     def get_step_url(self, step, client):
         viewname = f"job_seekers_views:update_job_seeker_step_{step}"
@@ -4407,9 +4368,7 @@ def test_job_seeker_with_profile_has_check_boxes_ticked_in_step3(self, client):
         assert response.status_code == 302
 
         # Go straight to STEP 3
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.UPDATE)
         response = client.get(
             reverse(
                 "job_seekers_views:update_job_seeker_step_3",
@@ -4457,9 +4416,7 @@ def test_detect_existing_job_seeker(client):
     # ----------------------------------------------------------------------
 
     response = client.get(next_url)
-    [job_seeker_session_name] = [
-        k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-    ]
+    job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
     next_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
 
     response = client.get(next_url)
@@ -5047,9 +5004,7 @@ def get_check_nir_url(self, client):
         # Init session
         start_url = reverse("apply:start_hire", kwargs={"company_pk": self.company.pk})
         client.get(start_url, follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         return reverse("job_seekers_views:check_nir_for_hire", kwargs={"session_uuid": job_seeker_session_name})
 
     def test_job_seeker_found_with_nir(self, client):
@@ -5089,9 +5044,7 @@ def test_job_seeker_found_with_email(self, client):
 
         response = client.post(check_nir_url, data={"nir": INVALID_NIR, "preview": 1})
         assertContains(response, "Le numéro de sécurité sociale est trop court")
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         search_by_email_url = reverse(
             "job_seekers_views:search_by_email_for_hire",
             kwargs={"session_uuid": job_seeker_session_name},
@@ -5271,49 +5224,66 @@ def _reverse(self, view_name):
             view_name, kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id}
         )
 
-    def test_no_previous_as_employer(self, client):
+    def test_iae_employer(self, client):
         self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_employer(self, client):
-        self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
-        client.force_login(self.company.members.first())
-        fake_session_initialization(client, self.company, {})
-
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
+        response = client.get(url)
         assertTemplateNotUsed(response, "approvals/includes/box.html")
         assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
         response = client.post(
             self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
         )
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        assertRedirects(response, next_url)
 
-    def test_no_previous_as_geiq_staff(self, client):
+    def test_geiq_employer(self, client):
         self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:geiq_eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:geiq_eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_geiq_staff(self, client):
-        self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
+        response = client.get(url)
+        assertTemplateNotUsed(response, "approvals/includes/box.html")
+        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
+        response = client.post(
+            self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
+        )
+        assertRedirects(response, next_url)
+
+    def test_other_employer(self, client):
+        # not IAE or GEIQ
+        self.company = CompanyFactory(kind=CompanyKind.EA, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:hire_confirmation")
+        response = client.get(url)
+        assertRedirects(response, next_url)
+
+        # with previous job application
+        JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
+        response = client.get(url)
         assertTemplateNotUsed(response, "approvals/includes/box.html")
+        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
         response = client.post(
             self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
         )
-        assertRedirects(response, self._reverse("apply:geiq_eligibility_for_hire"))
+        assertRedirects(response, next_url)
 
 
 @pytest.mark.ignore_unknown_variable_template_error("is_subject_to_eligibility_rules")
@@ -5328,7 +5298,7 @@ def _reverse(self, view_name):
         )
 
     def test_not_subject_to_eligibility(self, client):
-        self.company = CompanyFactory(not_subject_to_eligibility=True, with_membership=True)
+        self.company = CompanyFactory(kind=CompanyKind.EA, with_membership=True)  # We don't want a GEIQ here
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {"selected_jobs": []})
         response = client.get(self._reverse("apply:eligibility_for_hire"))
@@ -5465,6 +5435,7 @@ def test_as_company(self, client, snapshot):
         self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
         IAEEligibilityDiagnosisFactory(from_prescriber=True, job_seeker=self.job_seeker)
         client.force_login(self.company.members.first())
+        apply_session = fake_session_initialization(client, self.company, {"selected_jobs": []})
 
         with assertSnapshotQueries(snapshot(name="view queries")):
             response = client.get(self._reverse("apply:hire_confirmation"))
@@ -5520,9 +5491,12 @@ def test_as_company(self, client, snapshot):
         assert job_application.resume_link == ""
         assert job_application.resume is None
 
+        assert apply_session.name not in client.session
+
     def test_cannot_hire_start_date_after_approval_expires(self, client):
         self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
         client.force_login(self.company.members.first())
+        apply_session = fake_session_initialization(client, self.company, {"selected_jobs": []})
 
         today = timezone.localdate()
         approval = ApprovalFactory(end_at=today + datetime.timedelta(days=1))
@@ -5554,12 +5528,14 @@ def test_cannot_hire_start_date_after_approval_expires(self, client):
             "hiring_start_at",
             JobApplication.ERROR_HIRES_AFTER_APPROVAL_EXPIRES,
         )
+        assert apply_session.name in client.session
 
     def test_as_company_elibility_diagnosis_from_another_company(self, client, snapshot):
         eligibility_diagnosis = IAEEligibilityDiagnosisFactory(from_employer=True, job_seeker=self.job_seeker)
         ApprovalFactory(eligibility_diagnosis=eligibility_diagnosis, user=self.job_seeker)
         self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
         client.force_login(self.company.members.get())
+        apply_session = fake_session_initialization(client, self.company, {"selected_jobs": []})
 
         response = client.get(self._reverse("apply:hire_confirmation"))
         assertContains(response, "Déclarer l’embauche de Clara SION")
@@ -5607,11 +5583,15 @@ def test_as_company_elibility_diagnosis_from_another_company(self, client, snaps
         assert job_application.resume_link == ""
         assert job_application.resume is None
 
+        assert apply_session.name not in client.session
+
     def test_as_geiq(self, client):
         diagnosis = GEIQEligibilityDiagnosisFactory(job_seeker=self.job_seeker, from_employer=True)
         diagnosis.administrative_criteria.add(GEIQAdministrativeCriteria.objects.get(pk=19))
         self.company = diagnosis.author_geiq
         client.force_login(self.company.members.first())
+        apply_session = fake_session_initialization(client, self.company, {"selected_jobs": []})
+
         response = client.get(self._reverse("apply:hire_confirmation"))
         assertContains(response, "Déclarer l’embauche de Clara SION")
         assertContains(response, "Éligibilité GEIQ confirmée")
@@ -5663,6 +5643,8 @@ def test_as_geiq(self, client):
         assert job_application.resume_link == ""
         assert job_application.resume is None
 
+        assert apply_session.name not in client.session
+
 
 class TestNewHireProcessInfo:
     GEIQ_APPLY_PROCESS_INFO = "Cet espace vous permet d’enregistrer une candidature à traiter plus tard"
@@ -5700,9 +5682,7 @@ def test_as_employer(self, client):
 
         # Init session
         response = client.get(reverse("apply:start_hire", kwargs={"company_pk": self.company.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         response = client.get(
             reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
         )
@@ -5718,9 +5698,9 @@ def test_as_employer(self, client):
 
         # Init session
         response = client.get(reverse("apply:start_hire", kwargs={"company_pk": self.geiq.pk}), follow=True)
-        [job_seeker_session_name_geiq] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name_geiq = get_session_name(
+            client.session, JobSeekerSessionKinds.GET_OR_CREATE, ignore=[job_seeker_session_name]
+        )
         response = client.get(
             reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name_geiq})
         )
diff --git a/tests/www/companies_views/test_job_description_views.py b/tests/www/companies_views/test_job_description_views.py
index 1b38ab76aad..d1cac5e0bef 100644
--- a/tests/www/companies_views/test_job_description_views.py
+++ b/tests/www/companies_views/test_job_description_views.py
@@ -20,7 +20,7 @@
 from tests.jobs.factories import create_test_romes_and_appellations
 from tests.prescribers.factories import PrescriberOrganizationWithMembershipFactory
 from tests.users.factories import EmployerFactory, JobSeekerFactory, PrescriberFactory
-from tests.utils.test import assertSnapshotQueries, parse_response_to_soup, session_data_without_known_keys
+from tests.utils.test import assertSnapshotQueries, parse_response_to_soup
 
 
 POSTULER = "Postuler"
@@ -491,7 +491,6 @@ def test_edit_job_description_company_edit_url_preview_fallback(self, client):
         response = client.post(self.edit_preview_url)
         assertRedirects(response, self.list_url)
         assert self.company.job_description_through.count() == 5
-        assert session_data_without_known_keys(client.session) == {}
         assert ITOU_SESSION_JOB_DESCRIPTION_KEY not in client.session
 
     @freeze_time("2025-04-09 15:16:17.18")
@@ -513,8 +512,6 @@ def test_edit_job_description_opcs(self, client):
 
         # Step 1: edit job description
         response = client.get(self.edit_url)
-        assert session_data_without_known_keys(client.session) == {}
-
         post_data = {
             "appellation": "11076",  # Must be a non existing one for the company
             "market_context_description": "Whatever market description",
diff --git a/tests/www/employee_record_views/test_add.py b/tests/www/employee_record_views/test_add.py
index 03f1f4f81b1..e3f1eed0700 100644
--- a/tests/www/employee_record_views/test_add.py
+++ b/tests/www/employee_record_views/test_add.py
@@ -4,11 +4,12 @@
 
 from itou.employee_record.enums import Status
 from itou.utils.urls import add_url_params
+from itou.www.employee_record_views.views import AddView
 from tests.approvals.factories import ApprovalFactory
 from tests.companies.factories import CompanyFactory
 from tests.employee_record.factories import EmployeeRecordFactory
 from tests.job_applications.factories import JobApplicationFactory
-from tests.utils.test import assertSnapshotQueries, parse_response_to_soup, session_data_without_known_keys
+from tests.utils.test import assertSnapshotQueries, get_session_name, parse_response_to_soup
 
 
 def test_wizard(snapshot, client):
@@ -28,7 +29,7 @@ def test_wizard(snapshot, client):
     with assertSnapshotQueries(snapshot(name="start-queries")):
         response = client.get(add_url_params(reverse("employee_record_views:add"), {"reset_url": reset_url}))
 
-    [wizard_session_name] = session_data_without_known_keys(client.session)
+    wizard_session_name = get_session_name(client.session, AddView.expected_session_kind)
     expected_session = {"config": {"reset_url": reset_url}}
     assert client.session[wizard_session_name] == expected_session
     choose_employee_url = reverse(
@@ -137,7 +138,7 @@ def test_employee_list(client):
     reset_url = reverse("employee_record_views:list")
     response = client.get(add_url_params(reverse("employee_record_views:add"), {"reset_url": reset_url}))
 
-    [wizard_session_name] = session_data_without_known_keys(client.session)
+    wizard_session_name = get_session_name(client.session, AddView.expected_session_kind)
     choose_employee_url = reverse(
         "employee_record_views:add", kwargs={"session_uuid": wizard_session_name, "step": "choose-employee"}
     )
@@ -165,7 +166,7 @@ def test_choose_employee_step_with_a_bad_choice(client):
     reset_url = reverse("employee_record_views:list")
     response = client.get(add_url_params(reverse("employee_record_views:add"), {"reset_url": reset_url}))
 
-    [wizard_session_name] = session_data_without_known_keys(client.session)
+    wizard_session_name = get_session_name(client.session, AddView.expected_session_kind)
     choose_employee_url = reverse(
         "employee_record_views:add", kwargs={"session_uuid": wizard_session_name, "step": "choose-employee"}
     )
diff --git a/tests/www/job_seekers_views/test_create_or_update.py b/tests/www/job_seekers_views/test_create_or_update.py
index 851c1837691..0c20b0829c1 100644
--- a/tests/www/job_seekers_views/test_create_or_update.py
+++ b/tests/www/job_seekers_views/test_create_or_update.py
@@ -28,7 +28,7 @@
 )
 from tests.users import constants as users_test_constants
 from tests.users.factories import ItouStaffFactory, JobSeekerFactory
-from tests.utils.test import parse_response_to_soup, session_data_without_known_keys
+from tests.utils.test import get_session_name, parse_response_to_soup
 from tests.www.apply.test_submit import CONFIRM_RESET_MARKUP, LINK_RESET_MARKUP
 
 
@@ -138,9 +138,7 @@ def test_check_nir_with_session(self, client):
         # Init session
         start_url = reverse("apply:start", kwargs={"company_pk": company.pk})
         client.get(start_url, {"back_url": reset_url})
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.CHECK_NIR_JOB_SEEKER)
 
         response = client.get(
             reverse("job_seekers_views:check_nir_for_job_seeker", kwargs={"session_uuid": job_seeker_session_name})
@@ -168,9 +166,7 @@ def test_cannot_check_nir_if_already_set(self, client):
         # Init session
         start_url = reverse("apply:start", kwargs={"company_pk": company.pk})
         client.get(start_url)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.CHECK_NIR_JOB_SEEKER)
 
         response = client.get(
             reverse("job_seekers_views:check_nir_for_job_seeker", kwargs={"session_uuid": job_seeker_session_name})
@@ -254,7 +250,7 @@ def test_start_get_or_create_sender(
         assert response.status_code == expected_status_code
 
         if expected_status_code == 302:
-            [job_seeker_session_name] = session_data_without_known_keys(client.session)
+            job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
             view_name = (
                 "job_seekers_views:check_nir_for_hire"
                 if tunnel == "hire"
@@ -310,9 +306,7 @@ def test_check_nir_with_session(self, client):
         # Init session
         start_url = reverse("apply:start", kwargs={"company_pk": company.pk})
         client.get(start_url, follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
 
         response = client.get(
             reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
@@ -329,9 +323,7 @@ def test_create_step_1(self, born_in_france, client):
 
         # Init session
         client.get(reverse("apply:start", kwargs={"company_pk": company.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
 
         birthdate = datetime.date(1911, 11, 1)
         response = client.post(
@@ -369,9 +361,7 @@ def test_birth_country_not_france_and_birthplace(self, client):
 
         # Init session
         client.get(reverse("apply:start", kwargs={"company_pk": company.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
 
         birthdate = datetime.date(1911, 11, 1)
         response = client.post(
@@ -410,9 +400,7 @@ def test_birth_country_france_and_no_birthplace(self, client):
 
         # Init session
         client.get(reverse("apply:start", kwargs={"company_pk": company.pk}), follow=True)
-        [job_seeker_session_name] = [
-            k for k in session_data_without_known_keys(client.session) if not k.startswith("job_application")
-        ]
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
 
         response = client.post(
             reverse(
@@ -518,7 +506,7 @@ def test_standalone_creation_as_prescriber_existing_nir(self, client, snapshot,
         params = {"tunnel": "standalone", "from_url": from_url}
         start_url = add_url_params(reverse("job_seekers_views:get_or_create_start"), params)
         client.get(start_url)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         check_nir_url = reverse(
             "job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -596,7 +584,7 @@ def test_standalone_creation_as_prescriber_existing_email(self, client, snapshot
         params = {"tunnel": "standalone", "from_url": from_url}
         start_url = add_url_params(reverse("job_seekers_views:get_or_create_start"), params)
         client.get(start_url)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         search_by_email_url = reverse(
             "job_seekers_views:search_by_email_for_sender", kwargs={"session_uuid": job_seeker_session_name}
         )
@@ -631,7 +619,7 @@ def test_standalone_creation_as_prescriber(self, client):
         params = {"tunnel": "standalone", "from_url": from_url}
         start_url = add_url_params(reverse("job_seekers_views:get_or_create_start"), params)
         client.get(start_url)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.GET_OR_CREATE)
         next_url = reverse("job_seekers_views:check_nir_for_sender", kwargs={"session_uuid": job_seeker_session_name})
 
         # Step determine the job seeker with a NIR.
@@ -948,7 +936,7 @@ def test_start_update(self, job_seeker_value, from_url_value, expected_status_co
         assert response.status_code == expected_status_code
 
         if expected_status_code == 302:
-            [job_seeker_session_name] = session_data_without_known_keys(client.session)
+            job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.UPDATE)
             step_1_url = reverse(
                 "job_seekers_views:update_job_seeker_step_1",
                 kwargs={"session_uuid": job_seeker_session_name},
@@ -1014,7 +1002,7 @@ def test_update_step_1(self, born_in_france, client):
         }
         start_url = add_url_params(reverse("job_seekers_views:update_job_seeker_start"), params)
         client.get(start_url)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.UPDATE)
 
         url = reverse("job_seekers_views:update_job_seeker_step_1", kwargs={"session_uuid": job_seeker_session_name})
         response = client.get(url)
@@ -1061,7 +1049,7 @@ def test_birth_country_not_france_and_birthplace(self, client):
         }
         start_url = add_url_params(reverse("job_seekers_views:update_job_seeker_start"), params)
         client.get(start_url)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.UPDATE)
 
         birthdate = datetime.date(1911, 11, 1)
         response = client.post(
@@ -1109,7 +1097,7 @@ def test_birth_country_france_and_no_birthplace(self, client):
         }
         start_url = add_url_params(reverse("job_seekers_views:update_job_seeker_start"), params)
         client.get(start_url)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.UPDATE)
 
         response = client.post(
             reverse(
@@ -1164,7 +1152,7 @@ def test_fields_readonly_with_certified_criteria(self, client):
             ),
         }
         response = client.get(reverse("job_seekers_views:update_job_seeker_start"), params)
-        [job_seeker_session_name] = session_data_without_known_keys(client.session)
+        job_seeker_session_name = get_session_name(client.session, JobSeekerSessionKinds.UPDATE)
         new_birth_date = datetime.date(1978, 12, 1)
         url = reverse(
             "job_seekers_views:update_job_seeker_step_1",


Reviews:
user: login: EwenKorr
body: Je n'ai relevé que des coquilles, et j'applaudis le changement d'URL sans que ça pète a priori, vu qu'elle n'est pas affichée sur une page.

Je serais plus à l'aise s'il y avait une autre relecture, je ne valide pas tout de suite.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T12:32:20Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T05:35:12Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T05:46:44Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T05:55:12Z
user: login: francoisfreitag
body: La PR est grosse et assez fourre-tout. Plusieurs PRs rendraient le travail plus clair, digeste et encourageraient une relecture plus approfondie.
Arrivé à [apply: Refactor geiq eligibility view](https://github.com/gip-inclusion/les-emplois/pull/6039/commits/117278c6ccc001af7756806d3324e19def840ba9), mon cerveau a repris la route des vacances.


Au global, j’ai l’impression que tu as organisé les checks dans les bonnes cases, ce qui rend le suivi du code plus harmonieux. :+1: 


Le commit `apply: Check apply_session in hire_confirmation` devrait être reword. Il supprime la session dans `hire_confirmation`, c’est un check violent.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T14:04:35Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:42:08Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:44:37Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:48:52Z
user: login: francoisfreitag
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T15:09:25Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:10:49Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:17:36Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:20:13Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:22:05Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:22:48Z
user: login: francoisfreitag
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T15:25:27Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:27:46Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:32:11Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T19:53:20Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:01:03Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T05:08:51Z
user: login: francoisfreitag
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T13:14:20Z
user: login: tonial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:41:09Z

Review comments:
diff_hunk: @@ -314,7 +317,28 @@ def get_eligibility_step_url(self):
             if not any(bypass_eligibility_conditions):
                 return reverse(
                     "apply:application_eligibility",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+
+    def get_eligibility_for_hire_step_url(self):
+        if self.company.kind == CompanyKind.GEIQ:
+            return reverse(
+                "apply:geiq_eligibility_for_hire",
+                kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+            )
+        else:
+            bypass_eligibility_conditions = [
+                # Don't perform an eligibility diagnosis is the SIAE doesn't need it,
path: itou/www/apply/views/submit_views.py
user: login: EwenKorr
body: ```suggestion
                # Don't perform an eligibility diagnosis if the SIAE doesn't need it,
```
created_at: 2025-04-28T09:58:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -220,45 +220,61 @@ def get_context_data(self, **kwargs):
         }
 
 
-def _geiq_eligibility(
-    request, company, job_seeker, back_url, next_url, geiq_eligibility_criteria_url, template_name, extra_context
-):
-    # Check GEIQ eligibility during job application process
-    # Pass get_full_path to keep back/next_url query params
-    form = CheckJobSeekerGEIQEligibilityForm(hx_post_url=request.get_full_path(), data=request.POST or None)
-    geiq_criteria_form_url = add_url_params(
-        geiq_eligibility_criteria_url,
-        {
-            "back_url": back_url,
-            "next_url": next_url,
-        },
-    )
+class GEIQEligibilityView(FormView):
+    template_name = None
+    form_class = CheckJobSeekerGEIQEligibilityForm
 
-    if request.method == "POST" and form.is_valid():
+    def setup(self, request, *args, **kwargs):
+        # Don't use UserPassesTestMixin as we want the test to be performed before raising a 404 in the setup
+        if not request.user.is_employer:
+            raise PermissionDenied()
+        return super().setup(request, *args, **kwargs)
+
+    def dispatch(self, request, *args, **kwargs):
+        if self.company.kind != CompanyKind.GEIQ:
+            raise Http404()
+        return super().dispatch(request, *args, **kwargs)
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs["hx_post_url"] = self.request.get_full_path()
+        return kwargs
+
+    def get_next_url(self):
+        raise NotImplementedError
+
+    def get_back_url(self):
+        raise NotImplementedError
+
+    def get_success_url(self):
+        return add_url_params(
+            self.geiq_eligibility_criteria_url, {"back_url": self.get_back_url(), "next_url": self.get_next_url()}
+        )
+
+    def form_valid(self, form):
+        # if self.request.htmx:
path: itou/www/apply/views/common.py
user: login: EwenKorr
body: :eyes: 
created_at: 2025-04-28T10:08:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -821,65 +818,78 @@ def send_diagoriente_invite(request, job_application_id):
     return HttpResponseRedirect(redirect_url)
 
 
-@check_user(lambda user: user.is_employer)
-def eligibility(request, job_application_id, template_name="apply/process_eligibility.html"):
-    """
-    Check eligibility (as an SIAE).
-    """
+class IAEEligibilityView(common_views.IAEEligibilityView):
+    template_name = "apply/process_eligibility.html"
 
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    job_application = get_object_or_404(
-        queryset,
-        id=job_application_id,
-        state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
-    )
-    return common_views._eligibility(
-        request,
-        job_application.to_company,
-        job_application.job_seeker,
-        cancel_url=reverse("apply:details_for_company", kwargs={"job_application_id": job_application.id}),
-        next_url=reverse("apply:accept", kwargs={"job_application_id": job_application.id}),
-        template_name=template_name,
-        extra_context={"job_application": job_application},
-    )
+    def setup(self, request, job_application_id, *args, **kwargs):
+        super().setup(request, *args, **kwargs)
 
+        queryset = JobApplication.objects.is_active_company_member(request.user)
+        self.job_application = get_object_or_404(
+            queryset,
+            id=job_application_id,
+            state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
+        )
+        self.company = self.job_application.to_company
+        self.job_seeker = self.job_application.job_seeker
 
-@check_user(lambda user: user.is_employer)
-def geiq_eligibility(request, job_application_id, template_name="apply/process_geiq_eligibility.html"):
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    # Check GEIQ eligibility during job application process
-    job_application = get_object_or_404(queryset, pk=job_application_id)
-    back_url = get_safe_url(
-        request,
-        "back_url",
-        fallback_url=reverse("apply:details_for_company", kwargs={"job_application_id": job_application.pk}),
-    )
-    next_url = get_safe_url(request, "next_url")
-    return common_views._geiq_eligibility(
-        request,
-        job_application.to_company,
-        job_application.job_seeker,
-        back_url=back_url,
-        next_url=next_url,
-        geiq_eligibility_criteria_url=reverse(
-            "apply:geiq_eligibility_criteria", kwargs={"job_application_id": job_application.pk}
-        ),
-        template_name=template_name,
-        extra_context={},
-    )
+    def get_next_url(self):
+        return reverse("apply:accept", kwargs={"job_application_id": self.job_application.id})
 
+    def get_cancel_url(self):
+        return (reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.id}),)
 
-@check_user(lambda user: user.is_employer)
-def geiq_eligibility_criteria(
-    request,
-    job_application_id,
-    template_name="apply/includes/geiq/check_geiq_eligibility_form.html",
-):
-    """Dynamic GEIQ eligibility criteria form (HTMX)"""
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(**kwargs) | {
+            "job_application": self.job_application,
+        }
 
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    job_application = get_object_or_404(queryset, pk=job_application_id)
-    return common_views._geiq_eligibility_criteria(request, job_application.to_company, job_application.job_seeker)
+
+class GEIQEligibilitView(common_views.GEIQEligibilityView):
path: itou/www/apply/views/process_views.py
user: login: EwenKorr
body: ```suggestion
class GEIQEligibilityView(common_views.GEIQEligibilityView):
```
created_at: 2025-04-28T10:10:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -811,99 +826,84 @@ def get_context_data(self, **kwargs):
             "can_view_personal_information": can_view_personal_information(
                 self.request, self.job_application.job_seeker
             ),
-            "reset_url": reverse(
-                "apply:application_end",
-                kwargs={"company_pk": self.company.pk, "application_pk": self.job_application.pk},
-            ),
+            "reset_url": reverse("apply:application_end", kwargs={"application_pk": self.job_application.pk}),
             "page_title": "Auto-prescription enregistrée" if self.auto_prescription_process else "Candidature envoyée",
         }
 
 
-@check_user(lambda user: user.is_employer)
-def eligibility_for_hire(
-    request,
-    company_pk,
-    job_seeker_public_id,
-    template_name="apply/submit/eligibility_for_hire.html",
-):
-    company = get_object_or_404(
-        Company.objects.filter(pk__in={org.pk for org in request.organizations}), pk=company_pk
-    )
-    job_seeker = get_object_or_404(User.objects.filter(kind=UserKind.JOB_SEEKER), public_id=job_seeker_public_id)
-    _check_job_seeker_approval(request, job_seeker, company)
-    next_url = reverse(
-        "apply:hire_confirmation", kwargs={"company_pk": company.pk, "job_seeker_public_id": job_seeker.public_id}
-    )
-    bypass_eligibility_conditions = [
-        # Don't perform an eligibility diagnosis is the SIAE doesn't need it,
-        not company.is_subject_to_eligibility_rules,
-        # No need for eligibility diagnosis if the job seeker already has a PASS IAE
-        job_seeker.has_valid_approval,
-    ]
-    if any(bypass_eligibility_conditions) or job_seeker.has_valid_diagnosis(for_siae=company):
-        return HttpResponseRedirect(next_url)
-    return common_views._eligibility(
-        request,
-        company,
-        job_seeker,
-        cancel_url=reverse(
-            "job_seekers_views:check_job_seeker_info_for_hire",
-            kwargs={"company_pk": company.pk, "job_seeker_public_id": job_seeker.public_id},
-        ),
-        next_url=next_url,
-        template_name=template_name,
-        extra_context={"hire_process": True},
-    )
+class IAEEligibilityForHireView(ApplicationBaseView, common_views.IAEEligibilityView):
+    template_name = "apply/submit/eligibility_for_hire.html"
 
+    def dispatch(self, request, *args, **kwargs):
+        bypass_eligibility_conditions = [
+            # Don't perform an eligibility diagnosis is the SIAE doesn't need it,
+            not self.company.is_subject_to_eligibility_rules,
+            # No need for eligibility diagnosis if the job seeker already has a PASS IAE
+            self.job_seeker.has_valid_approval,
+            # Job seeker must not have a diagnosis
+            self.job_seeker.has_valid_diagnosis(for_siae=self.company),
+        ]
+        if any(bypass_eligibility_conditions):
+            return HttpResponseRedirect(self.get_next_url())
+        return super().dispatch(request, *args, **kwargs)
 
-@check_user(lambda user: user.is_employer)
-def geiq_eligibility_for_hire(
-    request,
-    company_pk,
-    job_seeker_public_id,
-    template_name="apply/submit/geiq_eligibility_for_hire.html",
-):
-    company = get_object_or_404(
-        Company.objects.filter(pk__in={org.pk for org in request.organizations}, kind=CompanyKind.GEIQ), pk=company_pk
-    )
-    job_seeker = get_object_or_404(User.objects.filter(kind=UserKind.JOB_SEEKER), public_id=job_seeker_public_id)
-    next_url = reverse(
-        "apply:hire_confirmation", kwargs={"company_pk": company.pk, "job_seeker_public_id": job_seeker.public_id}
-    )
-    if GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(job_seeker, company).exists():
-        return HttpResponseRedirect(next_url)
-    return common_views._geiq_eligibility(
-        request,
-        company,
-        job_seeker,
-        back_url=reverse(
-            "job_seekers_views:check_job_seeker_info_for_hire",
-            kwargs={"company_pk": company.pk, "job_seeker_public_id": job_seeker.public_id},
-        ),
-        next_url=next_url,
-        geiq_eligibility_criteria_url=reverse(
+    def get_next_url(self):
+        return reverse(
+            "apply:hire_confirmation",
+            kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+        )
+
+    def get_cancel_url(self):
+        return (
+            reverse(
+                "job_seekers_views:check_job_seeker_info_for_hire",
+                kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+            ),
+        )
+
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(**kwargs) | {
+            "hire_process": True,
+        }
+
+
+class GEIQEigibilityForHireView(ApplicationBaseView, common_views.GEIQEligibilityView):
path: itou/www/apply/views/submit_views.py
user: login: EwenKorr
body: ```suggestion
class GEIQEligibilityForHireView(ApplicationBaseView, common_views.GEIQEligibilityView):
```

created_at: 2025-04-28T10:13:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -293,6 +293,7 @@ def _geiq_eligibility_criteria(
         data=request.POST or None,
     )
     next_url = get_safe_url(request, "next_url")
+    back_url = get_safe_url(request, "back_url")
path: itou/www/apply/views/common.py
user: login: EwenKorr
body: Je commente ici mais ça concerne le message de commit : il y a une coquille dans `common`
created_at: 2025-04-28T10:16:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -882,17 +882,8 @@ def get_context_data(self, **kwargs):
         }
 
 
-@check_user(lambda user: user.is_employer)
-def geiq_eligibility_criteria_for_hire(request, company_pk, job_seeker_public_id):
-    company = get_object_or_404(
-        Company.objects.filter(pk__in={org.pk for org in request.organizations}, kind=CompanyKind.GEIQ), pk=company_pk
-    )
-    job_seeker = get_object_or_404(User.objects.filter(kind=UserKind.JOB_SEEKER), public_id=job_seeker_public_id)
-    return common_views._geiq_eligibility_criteria(
-        request,
-        company,
-        job_seeker,
-    )
+class GEIQEligiblityCriteriaForHireView(ApplicationBaseView, common_views.GEIQEligibilityCriteriaHtmxView):
+    pass
path: itou/www/apply/views/submit_views.py
user: login: EwenKorr
body: Nice.
created_at: 2025-04-28T10:22:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -821,65 +818,78 @@ def send_diagoriente_invite(request, job_application_id):
     return HttpResponseRedirect(redirect_url)
 
 
-@check_user(lambda user: user.is_employer)
-def eligibility(request, job_application_id, template_name="apply/process_eligibility.html"):
-    """
-    Check eligibility (as an SIAE).
-    """
+class IAEEligibilityView(common_views.IAEEligibilityView):
+    template_name = "apply/process_eligibility.html"
 
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    job_application = get_object_or_404(
-        queryset,
-        id=job_application_id,
-        state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
-    )
-    return common_views._eligibility(
-        request,
-        job_application.to_company,
-        job_application.job_seeker,
-        cancel_url=reverse("apply:details_for_company", kwargs={"job_application_id": job_application.id}),
-        next_url=reverse("apply:accept", kwargs={"job_application_id": job_application.id}),
-        template_name=template_name,
-        extra_context={"job_application": job_application},
-    )
+    def setup(self, request, job_application_id, *args, **kwargs):
+        super().setup(request, *args, **kwargs)
 
+        queryset = JobApplication.objects.is_active_company_member(request.user)
+        self.job_application = get_object_or_404(
+            queryset,
+            id=job_application_id,
+            state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
+        )
+        self.company = self.job_application.to_company
+        self.job_seeker = self.job_application.job_seeker
 
-@check_user(lambda user: user.is_employer)
-def geiq_eligibility(request, job_application_id, template_name="apply/process_geiq_eligibility.html"):
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    # Check GEIQ eligibility during job application process
-    job_application = get_object_or_404(queryset, pk=job_application_id)
-    back_url = get_safe_url(
-        request,
-        "back_url",
-        fallback_url=reverse("apply:details_for_company", kwargs={"job_application_id": job_application.pk}),
-    )
-    next_url = get_safe_url(request, "next_url")
-    return common_views._geiq_eligibility(
-        request,
-        job_application.to_company,
-        job_application.job_seeker,
-        back_url=back_url,
-        next_url=next_url,
-        geiq_eligibility_criteria_url=reverse(
-            "apply:geiq_eligibility_criteria", kwargs={"job_application_id": job_application.pk}
-        ),
-        template_name=template_name,
-        extra_context={},
-    )
+    def get_next_url(self):
+        return reverse("apply:accept", kwargs={"job_application_id": self.job_application.id})
 
+    def get_cancel_url(self):
+        return (reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.id}),)
 
-@check_user(lambda user: user.is_employer)
-def geiq_eligibility_criteria(
-    request,
-    job_application_id,
-    template_name="apply/includes/geiq/check_geiq_eligibility_form.html",
-):
-    """Dynamic GEIQ eligibility criteria form (HTMX)"""
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(**kwargs) | {
+            "job_application": self.job_application,
+        }
 
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    job_application = get_object_or_404(queryset, pk=job_application_id)
-    return common_views._geiq_eligibility_criteria(request, job_application.to_company, job_application.job_seeker)
+
+class GEIQEligibilitView(common_views.GEIQEligibilityView):
+    template_name = "apply/process_geiq_eligibility.html"
+
+    def setup(self, request, job_application_id, *args, **kwargs):
+        super().setup(request, *args, **kwargs)
+
+        queryset = JobApplication.objects.is_active_company_member(request.user)
+        self.job_application = get_object_or_404(queryset, pk=job_application_id)
+        self.company = self.job_application.to_company
+        self.job_seeker = self.job_application.job_seeker
+
+        self.geiq_eligibility_criteria_url = reverse(
+            "apply:geiq_eligibility_criteria", kwargs={"job_application_id": self.job_application.pk}
+        )
+
+    def get_next_url(self):
+        return get_safe_url(self.request, "next_url")
+
+    def get_back_url(self):
+        return get_safe_url(
+            self.request,
+            "back_url",
+            fallback_url=reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}),
+        )
+
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(**kwargs) | {
+            "hire_process": True,
+            "is_subject_to_eligibility_rules": False,
+        }
+
+
+class GEIQEligiblityCriteriaView(common_views.GEIQEligibilityCriteriaHtmxView):
+    def setup(self, request, job_application_id, *args, **kwargs):
+        # Don't use UserPassesTestMixin as we want the test to be performed before raising a 404 in the setup
+        # We also nee to do it before querying the job application as the queryset would be empty for a non employer
path: itou/www/apply/views/process_views.py
user: login: EwenKorr
body: ```suggestion
        # We also need to do it before querying the job application as the queryset would be empty for a non employer
```
created_at: 2025-04-28T10:24:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -811,99 +826,84 @@ def get_context_data(self, **kwargs):
             "can_view_personal_information": can_view_personal_information(
                 self.request, self.job_application.job_seeker
             ),
-            "reset_url": reverse(
-                "apply:application_end",
-                kwargs={"company_pk": self.company.pk, "application_pk": self.job_application.pk},
-            ),
+            "reset_url": reverse("apply:application_end", kwargs={"application_pk": self.job_application.pk}),
             "page_title": "Auto-prescription enregistrée" if self.auto_prescription_process else "Candidature envoyée",
         }
 
 
-@check_user(lambda user: user.is_employer)
-def eligibility_for_hire(
-    request,
-    company_pk,
-    job_seeker_public_id,
-    template_name="apply/submit/eligibility_for_hire.html",
-):
-    company = get_object_or_404(
-        Company.objects.filter(pk__in={org.pk for org in request.organizations}), pk=company_pk
-    )
-    job_seeker = get_object_or_404(User.objects.filter(kind=UserKind.JOB_SEEKER), public_id=job_seeker_public_id)
-    _check_job_seeker_approval(request, job_seeker, company)
-    next_url = reverse(
-        "apply:hire_confirmation", kwargs={"company_pk": company.pk, "job_seeker_public_id": job_seeker.public_id}
-    )
-    bypass_eligibility_conditions = [
-        # Don't perform an eligibility diagnosis is the SIAE doesn't need it,
-        not company.is_subject_to_eligibility_rules,
-        # No need for eligibility diagnosis if the job seeker already has a PASS IAE
-        job_seeker.has_valid_approval,
-    ]
-    if any(bypass_eligibility_conditions) or job_seeker.has_valid_diagnosis(for_siae=company):
-        return HttpResponseRedirect(next_url)
-    return common_views._eligibility(
-        request,
-        company,
-        job_seeker,
-        cancel_url=reverse(
-            "job_seekers_views:check_job_seeker_info_for_hire",
-            kwargs={"company_pk": company.pk, "job_seeker_public_id": job_seeker.public_id},
-        ),
-        next_url=next_url,
-        template_name=template_name,
-        extra_context={"hire_process": True},
-    )
+class IAEEligibilityForHireView(ApplicationBaseView, common_views.IAEEligibilityView):
+    template_name = "apply/submit/eligibility_for_hire.html"
 
+    def dispatch(self, request, *args, **kwargs):
+        bypass_eligibility_conditions = [
+            # Don't perform an eligibility diagnosis is the SIAE doesn't need it,
path: itou/www/apply/views/submit_views.py
user: login: EwenKorr
body: ```suggestion
            # Don't perform an eligibility diagnosis if the SIAE doesn't need it,
```
created_at: 2025-04-28T11:52:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,7 +317,28 @@ def get_eligibility_step_url(self):
             if not any(bypass_eligibility_conditions):
                 return reverse(
                     "apply:application_eligibility",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+
+    def get_eligibility_for_hire_step_url(self):
path: itou/www/apply/views/submit_views.py
user: login: EwenKorr
body: Il y aurait peut-être moyen de mutualiser avec `ApplicationBaseView.get_eligibility_step_url` ?
created_at: 2025-04-28T11:58:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,7 +317,28 @@ def get_eligibility_step_url(self):
             if not any(bypass_eligibility_conditions):
                 return reverse(
                     "apply:application_eligibility",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+
+    def get_eligibility_for_hire_step_url(self):
path: itou/www/apply/views/submit_views.py
user: login: EwenKorr
body: Je dis ça parce qu'il y a une bonne partie de code en commun, mais c'est peut-être pas si simple.
created_at: 2025-04-28T12:05:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5302,49 +5219,66 @@ def _reverse(self, view_name):
             view_name, kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id}
         )
 
-    def test_no_previous_as_employer(self, client):
+    def test_iae_employer(self, client):
         self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_employer(self, client):
-        self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
-        client.force_login(self.company.members.first())
-        fake_session_initialization(client, self.company, {})
-
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
+        response = client.get(url)
         assertTemplateNotUsed(response, "approvals/includes/box.html")
         assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
         response = client.post(
             self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
         )
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        assertRedirects(response, next_url)
 
-    def test_no_previous_as_geiq_staff(self, client):
+    def test_geiq_employer(self, client):
         self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:geiq_eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:geiq_eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_geiq_staff(self, client):
-        self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
+        response = client.get(url)
+        assertTemplateNotUsed(response, "approvals/includes/box.html")
+        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
+        response = client.post(
+            self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
+        )
+        assertRedirects(response, next_url)
+
+    def test_other_employer(self, client):
+        # not IEA or GEIQ
path: tests/www/apply/test_submit.py
user: login: EwenKorr
body: ```suggestion
        # not IAE or GEIQ
```
created_at: 2025-04-28T12:10:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -220,45 +220,61 @@ def get_context_data(self, **kwargs):
         }
 
 
-def _geiq_eligibility(
-    request, company, job_seeker, back_url, next_url, geiq_eligibility_criteria_url, template_name, extra_context
-):
-    # Check GEIQ eligibility during job application process
-    # Pass get_full_path to keep back/next_url query params
-    form = CheckJobSeekerGEIQEligibilityForm(hx_post_url=request.get_full_path(), data=request.POST or None)
-    geiq_criteria_form_url = add_url_params(
-        geiq_eligibility_criteria_url,
-        {
-            "back_url": back_url,
-            "next_url": next_url,
-        },
-    )
+class GEIQEligibilityView(FormView):
+    template_name = None
+    form_class = CheckJobSeekerGEIQEligibilityForm
 
-    if request.method == "POST" and form.is_valid():
+    def setup(self, request, *args, **kwargs):
+        # Don't use UserPassesTestMixin as we want the test to be performed before raising a 404 in the setup
+        if not request.user.is_employer:
+            raise PermissionDenied()
+        return super().setup(request, *args, **kwargs)
+
+    def dispatch(self, request, *args, **kwargs):
+        if self.company.kind != CompanyKind.GEIQ:
+            raise Http404()
+        return super().dispatch(request, *args, **kwargs)
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs["hx_post_url"] = self.request.get_full_path()
+        return kwargs
+
+    def get_next_url(self):
+        raise NotImplementedError
+
+    def get_back_url(self):
+        raise NotImplementedError
+
+    def get_success_url(self):
+        return add_url_params(
+            self.geiq_eligibility_criteria_url, {"back_url": self.get_back_url(), "next_url": self.get_next_url()}
+        )
+
+    def form_valid(self, form):
+        # if self.request.htmx:
path: itou/www/apply/views/common.py
user: login: tonial
body: en effet, je l'ai géré dans le commit précédent (en le retirant) mais j'ai oublié de retirer le commentaire ici
created_at: 2025-04-29T05:35:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,7 +317,28 @@ def get_eligibility_step_url(self):
             if not any(bypass_eligibility_conditions):
                 return reverse(
                     "apply:application_eligibility",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+
+    def get_eligibility_for_hire_step_url(self):
path: itou/www/apply/views/submit_views.py
user: login: tonial
body: Vu qu'en plus des urls, les conditions ne sont pas les mêmes pour l'IAE, ça ne simplifie pas grand chose

created_at: 2025-04-29T05:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,7 +317,28 @@ def get_eligibility_step_url(self):
             if not any(bypass_eligibility_conditions):
                 return reverse(
                     "apply:application_eligibility",
-                    kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id},
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+
+    def get_eligibility_for_hire_step_url(self):
path: itou/www/apply/views/submit_views.py
user: login: tonial
body: par contre, j'ai remarqué que je peux appeler les `get_eligibility_(*)step_url()` dans le dispatch des vues d'éligibilité pour éviter de définir deux fois les conditions de bypass.
J'ai donc modifié le commit    `apply: Allow to skip eligibility step in hire_process` et j'ai ajouté un autre commit pour modifier les vues `submit - common` (pas `hire process`)

created_at: 2025-04-29T05:55:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,7 +48,7 @@
         name="application_resume",
     ),
     path(
-        "<int:company_pk>/application/<uuid:application_pk>/end",
path: itou/www/apply/urls.py
user: login: francoisfreitag
body: Et la compat’ avec les pages déjà servies ? Il faut un URL qui accepte le company_pk pour la transition.
created_at: 2025-05-05T12:47:07Z
author_association: MEMBER
diff_hunk: @@ -782,11 +779,11 @@ class ApplicationEndView(TemplateView):
     def setup(self, request, *args, **kwargs):
         super().setup(request, *args, **kwargs)
 
-        self.company = get_object_or_404(Company.objects.with_has_active_members(), pk=kwargs["company_pk"])
path: itou/www/apply/views/submit_views.py
user: login: francoisfreitag
body: Pour moi, ce check permet d’empêcher une candidature si l’entreprise n’a plus d’admin. Donc il me semble toujours utile ?
created_at: 2025-05-05T12:49:36Z
author_association: MEMBER
diff_hunk: @@ -821,27 +821,31 @@ def send_diagoriente_invite(request, job_application_id):
     return HttpResponseRedirect(redirect_url)
 
 
-@check_user(lambda user: user.is_employer)
-def eligibility(request, job_application_id, template_name="apply/process_eligibility.html"):
-    """
-    Check eligibility (as an SIAE).
-    """
+class IAEEligibilityView(common_views.IAEEligibilityView):
+    template_name = "apply/process_eligibility.html"
 
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    job_application = get_object_or_404(
-        queryset,
-        id=job_application_id,
-        state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
-    )
-    return common_views._eligibility(
-        request,
-        job_application.to_company,
-        job_application.job_seeker,
-        cancel_url=reverse("apply:details_for_company", kwargs={"job_application_id": job_application.id}),
-        next_url=reverse("apply:accept", kwargs={"job_application_id": job_application.id}),
-        template_name=template_name,
-        extra_context={"job_application": job_application},
-    )
+    def setup(self, request, job_application_id, *args, **kwargs):
+        super().setup(request, *args, **kwargs)
+
+        queryset = JobApplication.objects.is_active_company_member(request.user)
+        self.job_application = get_object_or_404(
+            queryset,
+            id=job_application_id,
+            state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
+        )
+        self.company = self.job_application.to_company
+        self.job_seeker = self.job_application.job_seeker
+
+    def get_next_url(self):
+        return reverse("apply:accept", kwargs={"job_application_id": self.job_application.id})
+
+    def get_cancel_url(self):
+        return (reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.id}),)
path: itou/www/apply/views/process_views.py
user: login: francoisfreitag
body: Tuple par erreur ?
created_at: 2025-05-05T12:53:42Z
author_association: MEMBER
diff_hunk: @@ -169,37 +171,53 @@ def _accept(request, company, job_seeker, error_url, back_url, template_name, ex
     return render(request, template_name, {**context, "has_form_error": any(form.errors for form in forms)})
 
 
-def _eligibility(request, siae, job_seeker, cancel_url, next_url, template_name, extra_context):
-    if not siae.is_subject_to_eligibility_rules:
-        raise Http404()
+class IAEEligibilityView(UserPassesTestMixin, FormView):
path: itou/www/apply/views/common.py
user: login: francoisfreitag
body: `BaseIAEEligibilityView` pour distinguer de la classe concrète du même nom ?
created_at: 2025-05-05T12:55:39Z
author_association: MEMBER
diff_hunk: @@ -5324,20 +5325,23 @@ def _reverse(self, view_name):
     def test_not_subject_to_eligibility(self, client):
         self.company = CompanyFactory(not_subject_to_eligibility=True, with_membership=True)
         client.force_login(self.company.members.first())
+        fake_session_initialization(client, self.company, {"selected_jobs": []})
path: tests/www/apply/test_submit.py
user: login: francoisfreitag
body: Nécessaire pour utiliser `ApplicationBaseView` je présume ?
created_at: 2025-05-05T12:59:51Z
author_association: MEMBER
diff_hunk: @@ -220,45 +220,60 @@ def get_context_data(self, **kwargs):
         }
 
 
-def _geiq_eligibility(
-    request, company, job_seeker, back_url, next_url, geiq_eligibility_criteria_url, template_name, extra_context
-):
-    # Check GEIQ eligibility during job application process
-    # Pass get_full_path to keep back/next_url query params
-    form = CheckJobSeekerGEIQEligibilityForm(hx_post_url=request.get_full_path(), data=request.POST or None)
-    geiq_criteria_form_url = add_url_params(
-        geiq_eligibility_criteria_url,
-        {
-            "back_url": back_url,
-            "next_url": next_url,
-        },
-    )
+class GEIQEligibilityView(FormView):
+    template_name = None
+    form_class = CheckJobSeekerGEIQEligibilityForm
 
-    if request.method == "POST" and form.is_valid():
+    def setup(self, request, *args, **kwargs):
+        # Don't use UserPassesTestMixin as we want the test to be performed before raising a 404 in the setup
path: itou/www/apply/views/common.py
user: login: francoisfreitag
body: Pour moi, une 404 à la place d’une 403 n’est pas un souci. D’autres sites répondent systématiquement 404 lorsqu’un utilisateur n’a pas accès à une ressource pour ne pas en divulguer l’existence.
created_at: 2025-05-05T13:12:07Z
author_association: MEMBER
diff_hunk: @@ -310,10 +311,7 @@ def _geiq_eligibility_criteria(
         "form": form,
         "allowance_amount": allowance_amount,
         "progress": 66,
-        "back_url": reverse(
-            "job_seekers_views:check_job_seeker_info_for_hire",
-            kwargs={"job_seeker_public_id": job_seeker.public_id, "company_pk": company.pk},
path: itou/www/apply/views/common.py
user: login: francoisfreitag
body: Est-ce qu’on n’utiliserait pas cet URL en fallback de `get_safe_url`?
created_at: 2025-05-05T13:27:39Z
author_association: MEMBER
diff_hunk: @@ -276,47 +276,60 @@ def get_context_data(self, **kwargs):
         }
 
 
-# HTMX fragments
+class GEIQEligibilityCriteriaHtmxView(FormView):
+    """Dynamic GEIQ eligibility criteria form (HTMX)"""
 
+    template_name = "apply/includes/geiq/check_geiq_eligibility_form.html"
+    form_class = GEIQAdministrativeCriteriaForGEIQForm
 
-def _geiq_eligibility_criteria(
-    request, company, job_seeker, template_name="apply/includes/geiq/check_geiq_eligibility_form.html"
-):
-    """Dynamic GEIQ eligibility criteria form (HTMX)"""
+    def setup(self, request, *args, **kwargs):
+        # Don't use UserPassesTestMixin as we want the test to be performed before raising a 404 in the setup
+        if not request.user.is_employer:
+            raise PermissionDenied()
 
-    diagnosis = GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(job_seeker, company).first()
-    form = GEIQAdministrativeCriteriaForGEIQForm(
-        company,
-        diagnosis.administrative_criteria.all() if diagnosis else [],
-        request.path,
-        data=request.POST or None,
-    )
-    next_url = get_safe_url(request, "next_url")
-    back_url = get_safe_url(request, "back_url")
-    allowance_amount = None
+        super().setup(request, *args, **kwargs)
+
+        self.diagnosis = GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(self.job_seeker, self.company).first()
+        self.next_url = get_safe_url(request, "next_url")
+        self.back_url = get_safe_url(request, "back_url")
+        self.allowance_amount = None
 
-    if request.method == "POST" and form.is_valid():
+    def dispatch(self, request, *args, **kwargs):
+        if self.company.kind != CompanyKind.GEIQ:
+            raise Http404()
+        return super().dispatch(request, *args, **kwargs)
+
+    def get_form_kwargs(self):
+        return super().get_form_kwargs() | {
+            "company": self.company,
+            "administrative_criteria": self.diagnosis.administrative_criteria.all() if self.diagnosis else [],
+            "form_url": self.request.path,
+        }
+
+    def form_valid(self, form):
         criteria = form.cleaned_data
-        if request.htmx:
-            allowance_amount = geiq_allowance_amount(request.from_authorized_prescriber, criteria)
+        if self.request.htmx:
+            self.allowance_amount = geiq_allowance_amount(self.request.from_authorized_prescriber, criteria)
+            return self.render_to_response(self.get_context_data(form=form))
         else:
-            if diagnosis:
-                GEIQEligibilityDiagnosis.update_eligibility_diagnosis(diagnosis, request.user, criteria)
+            if self.diagnosis:
+                GEIQEligibilityDiagnosis.update_eligibility_diagnosis(self.diagnosis, self.request.user, criteria)
             else:
-                GEIQEligibilityDiagnosis.create_eligibility_diagnosis(job_seeker, request.user, company, criteria)
-
-            return HttpResponseRedirect(next_url)
+                GEIQEligibilityDiagnosis.create_eligibility_diagnosis(
+                    self.job_seeker, self.request.user, self.company, criteria
+                )
 
-    context = {
-        "form": form,
-        "allowance_amount": allowance_amount,
-        "progress": 66,
-        "back_url": back_url,
-    }
+            return HttpResponseRedirect(self.next_url)
 
-    geo_criteria_detected = job_seeker.address_in_qpv or job_seeker.zrr_city_name
-    context["geo_criteria_detected"] = geo_criteria_detected
-    if geo_criteria_detected:
-        context["job_seeker"] = job_seeker
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs) | {
+            "allowance_amount": self.allowance_amount,
+            "progress": 66,
+            "back_url": self.back_url,
+        }
path: itou/www/apply/views/common.py
user: login: francoisfreitag
body: Plutôt que des update, il est plus clair et performant d’assigner directement.

D’autant plus ici, où on continue avec des assignements directs.
created_at: 2025-05-05T13:31:22Z
author_association: MEMBER
diff_hunk: @@ -314,7 +317,7 @@ def get_exit_url(self, job_seeker, created=False):
         if self.standalone_creation:
             return reverse("job_seekers_views:details", kwargs={"public_id": job_seeker.public_id})
 
-        kwargs = {"company_pk": self.company.pk, "job_seeker_public_id": job_seeker.public_id}
+        kwargs = self.get_apply_kwargs() | {"job_seeker_public_id": job_seeker.public_id}
path: itou/www/job_seekers_views/views.py
user: login: francoisfreitag
body: On perd `company_pk` ici, non ?
created_at: 2025-05-05T13:35:03Z
author_association: MEMBER
diff_hunk: @@ -390,11 +393,12 @@ def get(self, request, *args, **kwargs):
         if self.job_seeker.jobseeker_profile.nir:
             # TODO(ewen): check_job_seeker_info doesn't use the session yet,
             # so we delete the session here.
+            apply_kwargs = self.get_apply_kwargs()
path: itou/www/job_seekers_views/views.py
user: login: francoisfreitag
body: J’imagine qu’on pourrait faire sans la variable intermédiaire ici ?
Sinon, il faudrait déplacer le commentaire ci-dessus pour qu’il porte bien sur `self.job_seeker_session.delete()`.
created_at: 2025-05-05T13:36:32Z
author_association: MEMBER
diff_hunk: @@ -407,17 +411,18 @@ def post(self, request, *args, **kwargs):
             self.job_seeker.jobseeker_profile.save(update_fields=("nir", "lack_of_nir_reason"))
             # TODO(ewen): check_job_seeker_info doesn't use the session yet,
             # so we delete the session here.
+            apply_kwargs = self.get_apply_kwargs()
path: itou/www/job_seekers_views/views.py
user: login: francoisfreitag
body: Idem, soit supprimer cette ligne soit la déplacer au dessus du TODO.
Voir éventuellement la mutualiser pour les deux branches du `if`.
created_at: 2025-05-05T13:37:31Z
author_association: MEMBER
diff_hunk: @@ -839,8 +844,8 @@ def post(self, request, *args, **kwargs):
             else:
                 user.save()
 
-            self.job_seeker_session.delete()
             url = self.get_exit_url(self.profile.user, created=True)
+            self.job_seeker_session.delete()
path: itou/www/job_seekers_views/views.py
user: login: francoisfreitag
body: Ce changement n’est probablement pas nécessaire ?
created_at: 2025-05-05T13:38:11Z
author_association: MEMBER
diff_hunk: @@ -5266,49 +5266,66 @@ def _reverse(self, view_name):
             view_name, kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id}
         )
 
-    def test_no_previous_as_employer(self, client):
+    def test_iae_employer(self, client):
         self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_employer(self, client):
-        self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
-        client.force_login(self.company.members.first())
-        fake_session_initialization(client, self.company, {})
-
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
+        response = client.get(url)
         assertTemplateNotUsed(response, "approvals/includes/box.html")
         assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
         response = client.post(
             self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
         )
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        assertRedirects(response, next_url)
 
-    def test_no_previous_as_geiq_staff(self, client):
+    def test_geiq_employer(self, client):
         self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:geiq_eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:geiq_eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_geiq_staff(self, client):
-        self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
+        response = client.get(url)
+        assertTemplateNotUsed(response, "approvals/includes/box.html")
+        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
+        response = client.post(
+            self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
+        )
+        assertRedirects(response, next_url)
+
+    def test_other_employer(self, client):
+        # not IAE or GEIQ
+        self.company = CompanyFactory(kind=CompanyKind.EA, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url, fetch_redirect_response=False)
path: tests/www/apply/test_submit.py
user: login: francoisfreitag
body: :raised_eyebrow: Pourquoi ignorer la réponse ?
created_at: 2025-05-05T13:46:23Z
author_association: MEMBER
diff_hunk: @@ -320,6 +320,28 @@ def get_eligibility_step_url(self):
                     kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
                 )
 
+    def get_eligibility_for_hire_step_url(self):
+        if self.company.kind == CompanyKind.GEIQ:
+            if not GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(self.job_seeker, self.company).exists():
+                return reverse(
+                    "apply:geiq_eligibility_for_hire",
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+        else:
+            bypass_eligibility_conditions = [
+                # Don't perform an eligibility diagnosis if the SIAE doesn't need it,
+                not self.company.is_subject_to_eligibility_rules,
+                # No need for eligibility diagnosis if the job seeker already has a PASS IAE
+                self.job_seeker.has_valid_approval,
+                # Job seeker must not have a diagnosis
+                self.job_seeker.has_valid_diagnosis(for_siae=self.company),
+            ]
+            if not any(bypass_eligibility_conditions):
+                return reverse(
+                    "apply:eligibility_for_hire",
+                    kwargs=self.get_base_kwargs() | {"job_seeker_public_id": self.job_seeker.public_id},
+                )
+
path: itou/www/apply/views/submit_views.py
user: login: francoisfreitag
body: Peut-être explicitement `return None` pour préciser que c’est attendu.
created_at: 2025-05-05T13:50:54Z
author_association: MEMBER
diff_hunk: @@ -5313,9 +5313,9 @@ def test_other_employer(self, client):
         fake_session_initialization(client, self.company, {})
 
         url = self._reverse("apply:check_prev_applications_for_hire")
-        next_url = self._reverse("apply:eligibility_for_hire")
+        next_url = self._reverse("apply:hire_confirmation")
         response = client.get(url)
-        assertRedirects(response, next_url, fetch_redirect_response=False)
+        assertRedirects(response, next_url)
path: tests/www/apply/test_submit.py
user: login: francoisfreitag
body: On doit pouvoir intégrer avec le changement précédent, pour éviter l’état intermédiaire des tests où on ne fetch pas la réponse.
created_at: 2025-05-05T13:52:38Z
author_association: MEMBER
diff_hunk: @@ -23,21 +24,37 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
 BASE_NUM_QUERIES = 2
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
+def session_names_without_known_keys(session):
+    data = session.keys()
+    session_names = []
     for k in list(data):
+        if k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
+            continue
         if k.endswith("_session_kind"):
-            del data[k]
-    return data
+            continue
+        session_names.append(k)
+    return session_names
+
+
+def get_session_name(session, session_kind, ignore=None):
path: tests/utils/test.py
user: login: francoisfreitag
body: Trick: `ignore=()`
created_at: 2025-05-05T13:59:52Z
author_association: MEMBER
diff_hunk: @@ -23,21 +24,37 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
 BASE_NUM_QUERIES = 2
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
+def session_names_without_known_keys(session):
+    data = session.keys()
+    session_names = []
     for k in list(data):
path: tests/utils/test.py
user: login: francoisfreitag
body: Plus besoin de `list()`.
created_at: 2025-05-05T14:01:00Z
author_association: MEMBER
diff_hunk: @@ -23,21 +24,37 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
 BASE_NUM_QUERIES = 2
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
+def session_names_without_known_keys(session):
+    data = session.keys()
+    session_names = []
     for k in list(data):
+        if k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
+            continue
         if k.endswith("_session_kind"):
-            del data[k]
-    return data
+            continue
+        session_names.append(k)
+    return session_names
+
+
+def get_session_name(session, session_kind, ignore=None):
+    data = session_names_without_known_keys(session)
+    for session_name in data:
+        if session_name in (ignore or []):
+            continue
+        try:
+            named_session = SessionNamespace(session, session_kind, session_name)
+            named_session.verify_kind()
+            return session_name
+        except Http404:
+            pass
 
path: tests/utils/test.py
user: login: francoisfreitag
body: `return None` ?
created_at: 2025-05-05T14:03:32Z
author_association: MEMBER
diff_hunk: @@ -48,7 +48,7 @@
         name="application_resume",
     ),
     path(
-        "<int:company_pk>/application/<uuid:application_pk>/end",
path: itou/www/apply/urls.py
user: login: tonial
body: :facepalm: 
created_at: 2025-05-05T14:42:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,11 +779,11 @@ class ApplicationEndView(TemplateView):
     def setup(self, request, *args, **kwargs):
         super().setup(request, *args, **kwargs)
 
-        self.company = get_object_or_404(Company.objects.with_has_active_members(), pk=kwargs["company_pk"])
path: itou/www/apply/views/submit_views.py
user: login: tonial
body: cf le message de commit : 
La candidature a été créée à cette étape, et les étapes précédentes vérifiaient `with_has_active_members` 
- Il est donc extrêmement peu probable que ça ai changé entre le post de la vue précédente, et le get de celle-ci 
- Quand bien même ce serait le cas, la candidature existe et est accessible via la liste des candidatures, autant ne pas raiser ici.
created_at: 2025-05-05T14:44:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -821,27 +821,31 @@ def send_diagoriente_invite(request, job_application_id):
     return HttpResponseRedirect(redirect_url)
 
 
-@check_user(lambda user: user.is_employer)
-def eligibility(request, job_application_id, template_name="apply/process_eligibility.html"):
-    """
-    Check eligibility (as an SIAE).
-    """
+class IAEEligibilityView(common_views.IAEEligibilityView):
+    template_name = "apply/process_eligibility.html"
 
-    queryset = JobApplication.objects.is_active_company_member(request.user)
-    job_application = get_object_or_404(
-        queryset,
-        id=job_application_id,
-        state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
-    )
-    return common_views._eligibility(
-        request,
-        job_application.to_company,
-        job_application.job_seeker,
-        cancel_url=reverse("apply:details_for_company", kwargs={"job_application_id": job_application.id}),
-        next_url=reverse("apply:accept", kwargs={"job_application_id": job_application.id}),
-        template_name=template_name,
-        extra_context={"job_application": job_application},
-    )
+    def setup(self, request, job_application_id, *args, **kwargs):
+        super().setup(request, *args, **kwargs)
+
+        queryset = JobApplication.objects.is_active_company_member(request.user)
+        self.job_application = get_object_or_404(
+            queryset,
+            id=job_application_id,
+            state__in=JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES,
+        )
+        self.company = self.job_application.to_company
+        self.job_seeker = self.job_application.job_seeker
+
+    def get_next_url(self):
+        return reverse("apply:accept", kwargs={"job_application_id": self.job_application.id})
+
+    def get_cancel_url(self):
+        return (reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.id}),)
path: itou/www/apply/views/process_views.py
user: login: tonial
body: Yes, c'est corrigé
created_at: 2025-05-05T14:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -782,11 +779,11 @@ class ApplicationEndView(TemplateView):
     def setup(self, request, *args, **kwargs):
         super().setup(request, *args, **kwargs)
 
-        self.company = get_object_or_404(Company.objects.with_has_active_members(), pk=kwargs["company_pk"])
path: itou/www/apply/views/submit_views.py
user: login: francoisfreitag
body: Ah oui, `ApplicationEndView` n’est pas la vue qui crée la candidature :upside_down_face:. Merci pour les explications, c’est plus clair :+1: 
created_at: 2025-05-05T15:09:25Z
author_association: MEMBER
diff_hunk: @@ -314,7 +317,7 @@ def get_exit_url(self, job_seeker, created=False):
         if self.standalone_creation:
             return reverse("job_seekers_views:details", kwargs={"public_id": job_seeker.public_id})
 
-        kwargs = {"company_pk": self.company.pk, "job_seeker_public_id": job_seeker.public_id}
+        kwargs = self.get_apply_kwargs() | {"job_seeker_public_id": job_seeker.public_id}
path: itou/www/job_seekers_views/views.py
user: login: tonial
body: pour que self.company soit défini il faut qu'on ai `self.job_seeker_session.get("apply", {}).get("company_pk")` (voir dans le `setup()` de `JobSeekerBaseView`)
`get_apply_kwargs()` retourne `self.job_seeker_session.get("apply", {})` qui vaut `{"company_pk": self.company.pk}`
Donc tout va bien (si je ne me suis pas pris le pied dans un tapis bien caché)
created_at: 2025-05-05T15:10:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5266,49 +5266,66 @@ def _reverse(self, view_name):
             view_name, kwargs={"company_pk": self.company.pk, "job_seeker_public_id": self.job_seeker.public_id}
         )
 
-    def test_no_previous_as_employer(self, client):
+    def test_iae_employer(self, client):
         self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_employer(self, client):
-        self.company = CompanyFactory(subject_to_eligibility=True, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
-        client.force_login(self.company.members.first())
-        fake_session_initialization(client, self.company, {})
-
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
+        response = client.get(url)
         assertTemplateNotUsed(response, "approvals/includes/box.html")
         assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
         response = client.post(
             self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
         )
-        assertRedirects(response, self._reverse("apply:eligibility_for_hire"))
+        assertRedirects(response, next_url)
 
-    def test_no_previous_as_geiq_staff(self, client):
+    def test_geiq_employer(self, client):
         self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertRedirects(response, self._reverse("apply:geiq_eligibility_for_hire"))
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:geiq_eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url)
 
-    def test_with_previous_as_geiq_staff(self, client):
-        self.company = CompanyFactory(kind=CompanyKind.GEIQ, with_membership=True)
+        # with previous job application
         JobApplicationFactory(job_seeker=self.job_seeker, to_company=self.company, eligibility_diagnosis=None)
+        response = client.get(url)
+        assertTemplateNotUsed(response, "approvals/includes/box.html")
+        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
+        response = client.post(
+            self._reverse("apply:check_prev_applications_for_hire"), data={"force_new_application": "force"}
+        )
+        assertRedirects(response, next_url)
+
+    def test_other_employer(self, client):
+        # not IAE or GEIQ
+        self.company = CompanyFactory(kind=CompanyKind.EA, with_membership=True)
         client.force_login(self.company.members.first())
         fake_session_initialization(client, self.company, {})
 
-        response = client.get(self._reverse("apply:check_prev_applications_for_hire"))
-        assertContains(response, "Le candidat a déjà postulé chez cet employeur le")
+        url = self._reverse("apply:check_prev_applications_for_hire")
+        next_url = self._reverse("apply:eligibility_for_hire")
+        response = client.get(url)
+        assertRedirects(response, next_url, fetch_redirect_response=False)
path: tests/www/apply/test_submit.py
user: login: tonial
body: parce que `apply:eligibility_for_hire` fait un redirect dans ce cas là, et on ne peut pas `assertRedirect` sur une vue qui retourne une 302 sans `apply:eligibility_for_hire`

EDIT: c'est fusionné avec le commit suivant
created_at: 2025-05-05T15:17:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5313,9 +5313,9 @@ def test_other_employer(self, client):
         fake_session_initialization(client, self.company, {})
 
         url = self._reverse("apply:check_prev_applications_for_hire")
-        next_url = self._reverse("apply:eligibility_for_hire")
+        next_url = self._reverse("apply:hire_confirmation")
         response = client.get(url)
-        assertRedirects(response, next_url, fetch_redirect_response=False)
+        assertRedirects(response, next_url)
path: tests/www/apply/test_submit.py
user: login: tonial
body: J'avais volontairement splité les 2 commits parce que le diff des tests que j'ai réorganisé rendait trop peu lisible l'impact du changement sur `submit_views.py`
Mais en fait ça n'a pas de sens parce que dans le commit de refonte des tests, on peut faire `assertRedirects(response, self._reverse("apply:hire_confirmation"))`
J'ai fusionné les 2 commits
created_at: 2025-05-05T15:20:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,21 +24,37 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
 BASE_NUM_QUERIES = 2
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
+def session_names_without_known_keys(session):
+    data = session.keys()
+    session_names = []
     for k in list(data):
+        if k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
+            continue
         if k.endswith("_session_kind"):
-            del data[k]
-    return data
+            continue
+        session_names.append(k)
+    return session_names
+
+
+def get_session_name(session, session_kind, ignore=None):
path: tests/utils/test.py
user: login: tonial
body: En effet, c'est bien mieux ! 
created_at: 2025-05-05T15:22:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,21 +24,37 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
 BASE_NUM_QUERIES = 2
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
+def session_names_without_known_keys(session):
+    data = session.keys()
+    session_names = []
     for k in list(data):
path: tests/utils/test.py
user: login: tonial
body: ni de variable intermédiaire :+1: 
created_at: 2025-05-05T15:22:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,7 +317,7 @@ def get_exit_url(self, job_seeker, created=False):
         if self.standalone_creation:
             return reverse("job_seekers_views:details", kwargs={"public_id": job_seeker.public_id})
 
-        kwargs = {"company_pk": self.company.pk, "job_seeker_public_id": job_seeker.public_id}
+        kwargs = self.get_apply_kwargs() | {"job_seeker_public_id": job_seeker.public_id}
path: itou/www/job_seekers_views/views.py
user: login: francoisfreitag
body: Ce changement me semble transformer un bout de code auparavant très clair en un mécanisme assez obscur.
La lecture de `get_apply_session()` indique qu’on peut récupérer un dict vide. La raison pour laquelle on sait qu’il n’est pas vide est à cause du contenu du setup, qui pourrait changer. La dépendance est bien cachée et me semble une belle source de bug.
created_at: 2025-05-05T15:25:27Z
author_association: MEMBER
diff_hunk: @@ -839,8 +844,8 @@ def post(self, request, *args, **kwargs):
             else:
                 user.save()
 
-            self.job_seeker_session.delete()
             url = self.get_exit_url(self.profile.user, created=True)
+            self.job_seeker_session.delete()
path: itou/www/job_seekers_views/views.py
user: login: tonial
body: Si parce qu'il faut avoir accès à `self.job_seeker_session` pour calculer `self.get_exit_url()`
created_at: 2025-05-05T15:27:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -314,7 +317,7 @@ def get_exit_url(self, job_seeker, created=False):
         if self.standalone_creation:
             return reverse("job_seekers_views:details", kwargs={"public_id": job_seeker.public_id})
 
-        kwargs = {"company_pk": self.company.pk, "job_seeker_public_id": job_seeker.public_id}
+        kwargs = self.get_apply_kwargs() | {"job_seeker_public_id": job_seeker.public_id}
path: itou/www/job_seekers_views/views.py
user: login: tonial
body: Ah, je comprends le problème.
C'est un changement temporaire le temps qu'on ai un `apply_session_uuid` à passer
Dans la PR qui passe la session en UUID il faut gérer les 2 : 
https://github.com/gip-inclusion/les-emplois/pull/6065/files#diff-19e83c2ac6caceb1ee14e581fa24e8231388793d7100a3b8bb4dc384b8ff96a8L306-R315

Ensuite je nettoierai en ne mettant plus que apply_session dans le dict (idem pour `get_base_kwargs` dans `submit_views.py` )

created_at: 2025-05-05T15:32:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -276,47 +276,60 @@ def get_context_data(self, **kwargs):
         }
 
 
-# HTMX fragments
+class GEIQEligibilityCriteriaHtmxView(FormView):
+    """Dynamic GEIQ eligibility criteria form (HTMX)"""
 
+    template_name = "apply/includes/geiq/check_geiq_eligibility_form.html"
+    form_class = GEIQAdministrativeCriteriaForGEIQForm
 
-def _geiq_eligibility_criteria(
-    request, company, job_seeker, template_name="apply/includes/geiq/check_geiq_eligibility_form.html"
-):
-    """Dynamic GEIQ eligibility criteria form (HTMX)"""
+    def setup(self, request, *args, **kwargs):
+        # Don't use UserPassesTestMixin as we want the test to be performed before raising a 404 in the setup
+        if not request.user.is_employer:
+            raise PermissionDenied()
 
-    diagnosis = GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(job_seeker, company).first()
-    form = GEIQAdministrativeCriteriaForGEIQForm(
-        company,
-        diagnosis.administrative_criteria.all() if diagnosis else [],
-        request.path,
-        data=request.POST or None,
-    )
-    next_url = get_safe_url(request, "next_url")
-    back_url = get_safe_url(request, "back_url")
-    allowance_amount = None
+        super().setup(request, *args, **kwargs)
+
+        self.diagnosis = GEIQEligibilityDiagnosis.objects.valid_diagnoses_for(self.job_seeker, self.company).first()
+        self.next_url = get_safe_url(request, "next_url")
+        self.back_url = get_safe_url(request, "back_url")
+        self.allowance_amount = None
 
-    if request.method == "POST" and form.is_valid():
+    def dispatch(self, request, *args, **kwargs):
+        if self.company.kind != CompanyKind.GEIQ:
+            raise Http404()
+        return super().dispatch(request, *args, **kwargs)
+
+    def get_form_kwargs(self):
+        return super().get_form_kwargs() | {
+            "company": self.company,
+            "administrative_criteria": self.diagnosis.administrative_criteria.all() if self.diagnosis else [],
+            "form_url": self.request.path,
+        }
+
+    def form_valid(self, form):
         criteria = form.cleaned_data
-        if request.htmx:
-            allowance_amount = geiq_allowance_amount(request.from_authorized_prescriber, criteria)
+        if self.request.htmx:
+            self.allowance_amount = geiq_allowance_amount(self.request.from_authorized_prescriber, criteria)
+            return self.render_to_response(self.get_context_data(form=form))
         else:
-            if diagnosis:
-                GEIQEligibilityDiagnosis.update_eligibility_diagnosis(diagnosis, request.user, criteria)
+            if self.diagnosis:
+                GEIQEligibilityDiagnosis.update_eligibility_diagnosis(self.diagnosis, self.request.user, criteria)
             else:
-                GEIQEligibilityDiagnosis.create_eligibility_diagnosis(job_seeker, request.user, company, criteria)
-
-            return HttpResponseRedirect(next_url)
+                GEIQEligibilityDiagnosis.create_eligibility_diagnosis(
+                    self.job_seeker, self.request.user, self.company, criteria
+                )
 
-    context = {
-        "form": form,
-        "allowance_amount": allowance_amount,
-        "progress": 66,
-        "back_url": back_url,
-    }
+            return HttpResponseRedirect(self.next_url)
 
-    geo_criteria_detected = job_seeker.address_in_qpv or job_seeker.zrr_city_name
-    context["geo_criteria_detected"] = geo_criteria_detected
-    if geo_criteria_detected:
-        context["job_seeker"] = job_seeker
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs) | {
+            "allowance_amount": self.allowance_amount,
+            "progress": 66,
+            "back_url": self.back_url,
+        }
path: itou/www/apply/views/common.py
user: login: tonial
body: je trouvais plus élégant le update, mais j'entends l'argument de la performance, j'ai modifié tous mes commits
created_at: 2025-05-05T19:53:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -310,10 +311,7 @@ def _geiq_eligibility_criteria(
         "form": form,
         "allowance_amount": allowance_amount,
         "progress": 66,
-        "back_url": reverse(
-            "job_seekers_views:check_job_seeker_info_for_hire",
-            kwargs={"job_seeker_public_id": job_seeker.public_id, "company_pk": company.pk},
path: itou/www/apply/views/common.py
user: login: tonial
body: J'ai modifié le message de commit : on envoie sur cette vue que via `BaseGEIQEligibilityView.get_success_url()`
et les 2 urls sont en réalité différentes, donc mettre un fallback ne me semble pas une bonne idée

created_at: 2025-05-05T20:01:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -390,11 +393,12 @@ def get(self, request, *args, **kwargs):
         if self.job_seeker.jobseeker_profile.nir:
             # TODO(ewen): check_job_seeker_info doesn't use the session yet,
             # so we delete the session here.
+            apply_kwargs = self.get_apply_kwargs()
path: itou/www/job_seekers_views/views.py
user: login: tonial
body: EDIT: La variable sert parce qu'il faut appeler `self.get_apply_kwargs()` avant de supprimer la session
J'ai donc défini next_url avant de supprimer la session (au lieu de juste mettre `self.get_apply_kwargs()` dans une variable)
created_at: 2025-05-06T05:08:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5328,7 +5345,7 @@ def _reverse(self, view_name):
         )
 
     def test_not_subject_to_eligibility(self, client):
-        self.company = CompanyFactory(not_subject_to_eligibility=True, with_membership=True)
+        self.company = CompanyFactory(kind=CompanyKind.EA, with_membership=True)  # We don't wand a GEIQ here
path: tests/www/apply/test_submit.py
user: login: francoisfreitag
body: want*
created_at: 2025-05-12T13:09:12Z
author_association: MEMBER
diff_hunk: @@ -921,9 +921,7 @@ def hire_confirmation(
         request,
         company,
         job_seeker,
-        error_url=reverse(
-            "apply:hire_confirmation", kwargs={"company_pk": company_pk, "job_seeker_public_id": job_seeker_public_id}
-        ),
+        error_url=request.path,
path: itou/www/apply/views/submit_views.py
user: login: francoisfreitag
body: `request.get_full_path()` pour préserver les query params?
created_at: 2025-05-12T13:10:01Z
author_association: MEMBER
diff_hunk: @@ -23,21 +24,37 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
 BASE_NUM_QUERIES = 2
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
-    for k in list(data):
+def session_names_without_known_keys(session):
+    session_names = []
+    for k in session.keys():
+        if k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
+            continue
         if k.endswith("_session_kind"):
-            del data[k]
-    return data
+            continue
+        session_names.append(k)
+    return session_names
+
+
+def get_session_name(session, session_kind, ignore=()):
+    data = session_names_without_known_keys(session)
+    for session_name in data:
+        if session_name in ignore:
+            continue
+        try:
+            named_session = SessionNamespace(session, session_kind, session_name)
+            named_session.verify_kind()
+            return session_name
+        except Http404:
+            pass
+    return None
path: tests/utils/test.py
user: login: francoisfreitag
body: Ça ne gère pas le cas où il y aurait plusieurs clés pour un `session_kind` dans la session. Je pense qu’au lieu de retourner immédiatement, on devrait finir d’itérer sur le contenu de la session et vérifier qu’on n’a trouvé qu’une seule clé correspondant à la spec.
created_at: 2025-05-12T13:12:28Z
author_association: MEMBER
diff_hunk: @@ -23,21 +24,37 @@
 from pytest_django.asserts import assertContains, assertNotContains
 
 from itou.common_apps.address.departments import DEPARTMENTS
+from itou.utils.session import SessionNamespace
 
 
 # SAVEPOINT + RELEASE from the ATOMIC_REQUESTS transaction
 BASE_NUM_QUERIES = 2
 
 
 # Used to find the session namespace by elimination
-def session_data_without_known_keys(session):
-    data = dict(session)
-    for k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
-        data.pop(k, None)
-    for k in list(data):
+def session_names_without_known_keys(session):
+    session_names = []
+    for k in session.keys():
+        if k in ["_auth_user_id", "_auth_user_backend", "_auth_user_hash", "current_organization", "_csrftoken"]:
+            continue
         if k.endswith("_session_kind"):
-            del data[k]
-    return data
+            continue
+        session_names.append(k)
+    return session_names
+
+
+def get_session_name(session, session_kind, ignore=()):
+    data = session_names_without_known_keys(session)
+    for session_name in data:
+        if session_name in ignore:
+            continue
+        try:
+            named_session = SessionNamespace(session, session_kind, session_name)
+            named_session.verify_kind()
+            return session_name
+        except Http404:
+            pass
+    return None
path: tests/utils/test.py
user: login: tonial
body: :+1: 

created_at: 2025-05-12T13:41:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: tonial
created_at: 2025-05-05T15:34:45Z
body: @francoisfreitag première sous PR: https://github.com/gip-inclusion/les-emplois/pull/6083 
author_association: CONTRIBUTOR
user: login: tonial
created_at: 2025-05-06T05:28:07Z
body: @francoisfreitag seconde sous PR: https://github.com/gip-inclusion/les-emplois/pull/6086 
author_association: CONTRIBUTOR
user: login: tonial
created_at: 2025-05-06T20:03:59Z
body: @francoisfreitag C'est bon, il n'y a plus que 5 commits ici :)
author_association: CONTRIBUTOR