diff --git a/itou/templates/companies/card.html b/itou/templates/companies/card.html index ca88ffb687..c52b39a46b 100644 --- a/itou/templates/companies/card.html +++ b/itou/templates/companies/card.html @@ -5,6 +5,21 @@ {% block title %}{{ siae.display_name }} {{ block.super }}{% endblock %} +{% block global_messages %} + {{ block.super }} + {% if job_seeker %} +
+

+ Vous postulez actuellement pour {{ job_seeker.get_full_name }} +

+ +
+ {% endif %} +{% endblock %} + {% block title_prevstep %} {% include "layout/previous_step.html" with back_url=back_url only %} {% endblock %} @@ -31,7 +46,7 @@

Actions rapides

{% else %}
- diff --git a/itou/templates/companies/includes/_card_jobdescription.html b/itou/templates/companies/includes/_card_jobdescription.html index 75106e2de9..0618e807c7 100644 --- a/itou/templates/companies/includes/_card_jobdescription.html +++ b/itou/templates/companies/includes/_card_jobdescription.html @@ -10,7 +10,9 @@ {{ job_description.market_context_description | default:"Entreprise anonyme" }} {% else %} - + {{ job_description.company.kind }} - {{ job_description.company.display_name }} @@ -29,7 +31,7 @@
  • {% else %}
    - Voir la fiche de l'entreprise
    @@ -78,7 +78,7 @@

    {{ siae.display_name }}

    Cette structure vous intéresse ?

    @@ -154,7 +154,7 @@

    {{ siae.display_name }}

    Cette structure vous intéresse ?

    diff --git a/itou/templates/companies/includes/_list_siae_actives_jobs_row.html b/itou/templates/companies/includes/_list_siae_actives_jobs_row.html index 3439adda4b..442185c3b4 100644 --- a/itou/templates/companies/includes/_list_siae_actives_jobs_row.html +++ b/itou/templates/companies/includes/_list_siae_actives_jobs_row.html @@ -6,7 +6,7 @@ {{ job.display_name }} {% else %} - {{ job.display_name }} {% endif %} diff --git a/itou/templates/companies/job_description_card.html b/itou/templates/companies/job_description_card.html index dd8f01eae5..3e6bf567d3 100644 --- a/itou/templates/companies/job_description_card.html +++ b/itou/templates/companies/job_description_card.html @@ -6,6 +6,22 @@ {% block title %}{{ job.display_name }} - {{ siae.display_name }} {{ block.super }}{% endblock %} +{% block global_messages %} + {{ block.super }} + {% if job_seeker %} +
    +

    + Vous postulez actuellement pour {{ job_seeker.get_full_name }} +

    + +
    + {% endif %} +{% endblock %} + + {% block nb_columns %}8{% endblock %} {% block title_prevstep %} @@ -57,7 +73,7 @@

    Actions rapides

    {% else %}
    - diff --git a/itou/templates/search/includes/siaes_search_content.html b/itou/templates/search/includes/siaes_search_content.html index 08ebb95d28..eabdb2d9cf 100644 --- a/itou/templates/search/includes/siaes_search_content.html +++ b/itou/templates/search/includes/siaes_search_content.html @@ -7,7 +7,13 @@
    {% include "search/includes/siaes_search_subtitle.html" %}
    -
    +
    {% include "includes/btn_dropdown_filter/radio.html" with field=form.distance only %} {% include "includes/btn_dropdown_filter/checkboxes.html" with field=form.kinds only %} diff --git a/itou/templates/search/siaes_search_results.html b/itou/templates/search/siaes_search_results.html index ef76f68827..aaebfe1556 100644 --- a/itou/templates/search/siaes_search_results.html +++ b/itou/templates/search/siaes_search_results.html @@ -7,11 +7,28 @@ {{ block.super }} {% endblock %} +{% block global_messages %} + {{ block.super }} + {% if job_seeker %} +
    +

    + Vous postulez actuellement pour {{ job_seeker.get_full_name }} +

    + +
    + {% endif %} +{% endblock %} + + {% block title_content %}

    Rechercher un emploi inclusif

    {% endblock %} {% block title_extra %} {% include "search/includes/siaes_search_form.html" with form=form is_home=False only %} + {% if job_seeker %}{% endif %} {% include "search/includes/siaes_search_tabs.html" %} {% endblock %} diff --git a/itou/www/apply/views/submit_views.py b/itou/www/apply/views/submit_views.py index f3b84f00f9..2b5a3c6898 100644 --- a/itou/www/apply/views/submit_views.py +++ b/itou/www/apply/views/submit_views.py @@ -245,6 +245,24 @@ def get(self, request, *args, **kwargs): else: self.apply_session.init({"selected_jobs": [job_description.pk]}) + # Go directly to step ApplicationJobsView if we're carrying the job seeker public id with us. + if job_seeker_public_id := request.GET.get("job_seeker"): + try: + job_seeker = ( + User.objects.filter(kind=UserKind.JOB_SEEKER, public_id=job_seeker_public_id) + .select_related("jobseeker_profile") + .first() + ) + except (User.DoesNotExist, ValueError): + pass + else: + return HttpResponseRedirect( + reverse( + "apply:application_jobs", + kwargs={"company_pk": self.company.pk, "job_seeker_public_id": job_seeker.public_id}, + ) + ) + # Warn message if prescriber's authorization is pending if ( request.user.is_prescriber @@ -902,7 +920,6 @@ class ApplicationJobsView(ApplicationBaseView): def __init__(self): super().__init__() - self.form = None def get_initial(self): @@ -1753,3 +1770,28 @@ def hire_confirmation( "is_subject_to_geiq_eligibility_rules": company.kind == CompanyKind.GEIQ, }, ) + + +class TemporaryApplyForMixin: + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Create an empty variable to avoid unknown variable template errors in tests + context["job_seeker"] = None + if job_seeker_public_id := self.request.GET.get("job_seeker"): + try: + job_seeker = ( + User.objects.filter(kind=UserKind.JOB_SEEKER, public_id=job_seeker_public_id) + .select_related("jobseeker_profile") + .first() + ) + + # TODO(ewen): check that current user can manage this job seeker + context["job_seeker"] = job_seeker + return context + except ValidationError: + return context + else: + return context + + def get_job_seeker_query_string(self): + return {"job_seeker": self.request.GET.get("job_seeker")} if self.request.GET.get("job_seeker", None) else {} diff --git a/itou/www/companies_views/views.py b/itou/www/companies_views/views.py index 8d1e0d2db2..c437936d24 100644 --- a/itou/www/companies_views/views.py +++ b/itou/www/companies_views/views.py @@ -26,6 +26,7 @@ from itou.utils.pagination import pager from itou.utils.perms.company import get_current_company_or_404 from itou.utils.urls import add_url_params, get_absolute_url, get_safe_url +from itou.www.apply.views.submit_views import TemporaryApplyForMixin from itou.www.companies_views import forms as companies_forms @@ -108,7 +109,7 @@ def report_tally_url(user, company, job_description=None): ### Job description views -class JobDescriptionCardView(TemplateView): +class JobDescriptionCardView(TemporaryApplyForMixin, TemplateView): template_name = "companies/job_description_card.html" def setup(self, request, job_description_id, *args, **kwargs): @@ -441,7 +442,7 @@ def select_financial_annex(request, template_name="companies/select_financial_an ### Company CRUD views -class CompanyCardView(TemplateView): +class CompanyCardView(TemporaryApplyForMixin, TemplateView): template_name = "companies/card.html" def setup(self, request, siae_id, *args, **kwargs): diff --git a/itou/www/search/views.py b/itou/www/search/views.py index 9a275c080e..547600c480 100644 --- a/itou/www/search/views.py +++ b/itou/www/search/views.py @@ -14,6 +14,7 @@ from itou.prescribers.models import PrescriberOrganization from itou.utils.pagination import pager from itou.utils.urls import add_url_params +from itou.www.apply.views.submit_views import TemporaryApplyForMixin from itou.www.search.forms import JobDescriptionSearchForm, PrescriberSearchForm, SiaeSearchForm @@ -29,13 +30,17 @@ def employer_search_home(request, template_name="search/siaes_search_home.html") return render(request, template_name, context) -class EmployerSearchBaseView(FormView): +class EmployerSearchBaseView(TemporaryApplyForMixin, FormView): form_class = SiaeSearchForm initial = {"distance": SiaeSearchForm.DISTANCE_DEFAULT} def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs["data"] = self.request.GET or None + # an extra GET parameter is used: job_seeker. It is not part of the form + # so we pop it. + params_get = self.request.GET.copy() + params_get.pop("job_seeker", None) + kwargs["data"] = params_get or None return kwargs def get(self, request, *args, **kwargs): @@ -47,10 +52,9 @@ def get_context_data(self, **kwargs): context = { "back_url": reverse("search:employers_home"), "clear_filters_url": add_url_params( - self.request.path, - {"city": kwargs["form"].data.get("city")}, + self.request.path, {"city": kwargs["form"].data.get("city")} | self.get_job_seeker_query_string() ), - "filters_query_string": "", + "filters_query_string": urlencode(self.get_job_seeker_query_string()), "job_descriptions_count": 0, "siaes_count": 0, "results_page": [], @@ -153,7 +157,8 @@ def form_valid(self, form): "contract_types": contract_types, "departments": departments, "domains": domains, - }, + } + | self.get_job_seeker_query_string(), doseq=True, ), "results_page": results_and_counts.results_page, diff --git a/tests/www/apply/test_submit.py b/tests/www/apply/test_submit.py index 4000e59052..6554929673 100644 --- a/tests/www/apply/test_submit.py +++ b/tests/www/apply/test_submit.py @@ -9,6 +9,7 @@ from django.contrib import messages from django.contrib.messages.test import MessagesTestMixin from django.core.files.storage import storages +from django.template.defaultfilters import urlencode as urlencode_filter from django.test import override_settings from django.urls import resolve, reverse from django.utils import timezone @@ -36,8 +37,18 @@ from itou.utils.widgets import DuetDatePickerWidget from tests.approvals.factories import PoleEmploiApprovalFactory from tests.asp.factories import CommuneFactory, CountryFranceFactory -from tests.cities.factories import create_city_geispolsheim, create_city_in_zrr, create_test_cities -from tests.companies.factories import CompanyFactory, CompanyMembershipFactory, CompanyWithMembershipAndJobsFactory +from tests.cities.factories import ( + create_city_geispolsheim, + create_city_guerande, + create_city_in_zrr, + create_test_cities, +) +from tests.companies.factories import ( + CompanyFactory, + CompanyMembershipFactory, + CompanyWithMembershipAndJobsFactory, + JobDescriptionFactory, +) from tests.eligibility.factories import GEIQEligibilityDiagnosisFactory, IAEEligibilityDiagnosisFactory from tests.geo.factories import ZRRFactory from tests.institutions.factories import InstitutionWithMembershipFactory @@ -1767,6 +1778,173 @@ def test_check_info_as_prescriber_for_job_seeker_with_incomplete_info(self): ), ) + @pytest.mark.ignore_unknown_variable_template_error( + "confirmation_needed", "job_seeker", "readonly_form", "update_job_seeker" + ) + @override_settings(API_BAN_BASE_URL="http://ban-api") + @mock.patch( + "itou.utils.apis.geocoding.get_geocoding_data", + side_effect=mock_get_geocoding_data_by_ban_api_resolved, + ) + def test_apply_as_prescriber_from_job_seekers_list(self, _mock): + guerande = create_city_guerande() + guerande_company = CompanyWithMembershipAndJobsFactory( + romes=("N1101", "N1105"), + department="44", + coords=guerande.coords, + post_code="44350", + kind=CompanyKind.AI, + with_membership=True, + ) + JobDescriptionFactory(company=guerande_company, location=guerande) + + job_application = JobApplicationFactory( + job_seeker__first_name="Alain", + job_seeker__last_name="Zorro", + job_seeker__public_id="11111111-2222-3333-4444-555566667777", + ) + job_seeker_public_id = job_application.job_seeker.public_id + prescriber = job_application.sender + self.client.force_login(prescriber) + + # Entry point: job seekers list + # ---------------------------------------------------------------------- + + response = self.client.get(reverse("job_seekers_views:list")) + assert response.status_code == 200 + next_url = f"{reverse('search:employers_results')}?job_seeker={job_seeker_public_id}" + assertContains( + response, + ( + '
    ' + ), + ) + + # Step search company + # ---------------------------------------------------------------------- + + response = self.client.get( + reverse("search:employers_results"), {"city": guerande.slug, "job_seeker": job_seeker_public_id} + ) + self.assertContains(response, "Vous postulez actuellement pour Alain ZORRO") + + # Has link to company card with job_seeker public_id + company_url_with_job_seeker_id = ( + f"{guerande_company.get_card_url()}?job_seeker={job_seeker_public_id}" + f"&back_url={urlencode_filter(response.wsgi_request.get_full_path())}" + ) + assertContains( + response, + company_url_with_job_seeker_id, + ) + + # Step apply to company + # ---------------------------------------------------------------------- + + # apply_company_url = ( + # reverse("apply:start", kwargs={"company_pk": guerande_company.pk}) + + # f"?job_seeker={job_seeker_public_id}" + # ) + + # Step apply to job description + # ---------------------------------------------------------------------- + + # apply_job_description_url = ( + # reverse("apply:start", kwargs={"company_pk": guerande_company.pk}) + + # f"?job_seeker={job_seeker_public_id}" + # ) + + # # Remove that extra job seeker and proceed with "normal" flow + # # ---------------------------------------------------------------------- + # other_job_seeker.delete() + + # response = self.client.post(next_url) + # assert response.status_code == 302 + + # assert job_seeker_session_name not in self.client.session + # new_job_seeker = User.objects.get(email=dummy_job_seeker.email) + + # next_url = reverse( + # "apply:application_jobs", + # kwargs={"company_pk": company.pk, "job_seeker_public_id": new_job_seeker.public_id}, + # ) + # assert response.url == next_url + + # # Step application's jobs. + # # ---------------------------------------------------------------------- + + # response = self.client.get(next_url) + # assert response.status_code == 200 + + # selected_job = company.job_description_through.first() + # response = self.client.post(next_url, data={"selected_jobs": [selected_job.pk]}) + # assert response.status_code == 302 + + # assert self.client.session[f"job_application-{company.pk}"] == {"selected_jobs": [selected_job.pk]} + + # next_url = reverse( + # "apply:application_eligibility", + # kwargs={"company_pk": company.pk, "job_seeker_public_id": new_job_seeker.public_id}, + # ) + # assert response.url == next_url + + # # Step application's eligibility. + # # ---------------------------------------------------------------------- + # response = self.client.get(next_url) + # assert response.status_code == 302 + + # next_url = reverse( + # "apply:application_resume", + # kwargs={"company_pk": company.pk, "job_seeker_public_id": new_job_seeker.public_id}, + # ) + # assert response.url == next_url + + # # Step application's resume. + # # ---------------------------------------------------------------------- + # response = self.client.get(next_url) + # self.assertContains(response, "Postuler") + + # with mock.patch( + # "itou.www.apply.views.submit_views.uuid.uuid4", + # return_value=uuid.UUID("11111111-1111-1111-1111-111111111111"), + # ): + # response = self.client.post( + # next_url, + # data={ + # "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + # "resume": self.pdf_file, + # }, + # ) + # assert response.status_code == 302 + + # job_application = JobApplication.objects.get(sender=user, to_company=company) + # assert job_application.job_seeker == new_job_seeker + # assert job_application.sender_kind == SenderKind.PRESCRIBER + # assert job_application.sender_company is None + # assert job_application.sender_prescriber_organization is None + # assert job_application.state == JobApplicationState.NEW + # assert job_application.message == "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + # assert job_application.selected_jobs.get() == selected_job + + # assert job_application.resume_link == ( + # f"{settings.AWS_S3_ENDPOINT_URL}tests/{storages['public'].location}" + # "/resume/11111111-1111-1111-1111-111111111111.pdf" + # ) + + # assert f"job_application-{company.pk}" not in self.client.session + + # next_url = reverse( + # "apply:application_end", kwargs={"company_pk": company.pk, "application_pk": job_application.pk} + # ) + # assert response.url == next_url + + # # Step application's end. + # # ---------------------------------------------------------------------- + # response = self.client.get(next_url) + # assert response.status_code == 200 + @pytest.mark.ignore_unknown_variable_template_error("job_seeker") class ApplyAsPrescriberNirExceptionsTest(TestCase): diff --git a/tests/www/companies_views/test_card_views.py b/tests/www/companies_views/test_card_views.py index deb8fe4a95..2eb35c02df 100644 --- a/tests/www/companies_views/test_card_views.py +++ b/tests/www/companies_views/test_card_views.py @@ -7,11 +7,8 @@ from itou.jobs.models import Appellation from itou.utils.urls import add_url_params from tests.cities.factories import create_city_vannes -from tests.companies.factories import ( - CompanyFactory, - CompanyWithMembershipAndJobsFactory, - JobDescriptionFactory, -) +from tests.companies.factories import CompanyFactory, CompanyWithMembershipAndJobsFactory, JobDescriptionFactory +from tests.job_applications.factories import JobApplicationFactory from tests.jobs.factories import create_test_romes_and_appellations from tests.users.factories import JobSeekerFactory from tests.utils.test import BASE_NUM_QUERIES, TestCase, assert_previous_step, parse_response_to_soup @@ -225,6 +222,26 @@ def test_card_flow(self): company_card_url_other_formatting = f"{company_card_base_url}?back_url={urlencode(list_url)}" self.assertContains(response, company_card_url_other_formatting) + def test_card_with_job_seeker_public_id(self): + """ + When applying from "Mes candidats" + """ + company = CompanyFactory() + JobDescriptionFactory(company=company) + job_application = JobApplicationFactory( + job_seeker__first_name="Alain", + job_seeker__last_name="Zorro", + job_seeker__public_id="11111111-2222-3333-4444-555566667777", + ) + job_seeker_public_id = job_application.job_seeker.public_id + prescriber = job_application.sender + self.client.force_login(prescriber) + + url = reverse("companies_views:card", kwargs={"siae_id": company.pk}) + f"?job_seeker={job_seeker_public_id}" + response = self.client.get(url) + + self.assertContains(response, f"Vous postulez actuellement pour {job_application.job_seeker.get_full_name()}") + @pytest.mark.usefixtures("unittest_compatibility") class JobDescriptionCardViewTest(TestCase): diff --git a/tests/www/job_seekers_views/__snapshots__/test_list.ambr b/tests/www/job_seekers_views/__snapshots__/test_list.ambr index 14ddb00a54..1ee88b1a5d 100644 --- a/tests/www/job_seekers_views/__snapshots__/test_list.ambr +++ b/tests/www/job_seekers_views/__snapshots__/test_list.ambr @@ -640,6 +640,11 @@ 2 29/08/2024 + + + + + @@ -659,6 +664,11 @@ 1 30/08/2024 + + + + + @@ -678,6 +688,11 @@ 1 30/08/2024 + + + + + diff --git a/tests/www/search/tests.py b/tests/www/search/tests.py index 38640282a1..a5b60eea40 100644 --- a/tests/www/search/tests.py +++ b/tests/www/search/tests.py @@ -414,6 +414,61 @@ def distance_radio(distance): assertContains(response, guerande_opt, html=True, count=1) assertContains(response, vannes_opt, html=True, count=1) + def test_results_links_from_job_seeker_list(self, client): + """ + When applying from "Mes candidats" + """ + job_application = JobApplicationFactory( + job_seeker__first_name="Alain", + job_seeker__last_name="Zorro", + job_seeker__public_id="11111111-2222-3333-4444-555566667777", + ) + job_seeker_public_id = job_application.job_seeker.public_id + prescriber = job_application.sender + + create_test_romes_and_appellations(["N1101"], appellations_per_rome=1) + guerande = create_city_guerande() + COMPANY_GUERANDE = "Entreprise Guérande" + guerande_company = CompanyFactory( + name=COMPANY_GUERANDE, + department="44", + coords=guerande.coords, + post_code="44350", + kind=CompanyKind.AI, + with_membership=True, + ) + + job_description = JobDescriptionFactory(company=guerande_company, location=guerande) + client.force_login(prescriber) + + response = client.get( + self.URL, {"city": guerande.slug, "distance": 100, "job_seeker": job_application.job_seeker.public_id} + ) + assertContains(response, f"Vous postulez actuellement pour {job_application.job_seeker.get_full_name()}") + + # Has link to company card with job_seeker public_id + company_url_with_job_seeker_id = ( + f"{guerande_company.get_card_url()}?job_seeker={job_seeker_public_id}" + f"&back_url={urlencode_filter(response.wsgi_request.get_full_path())}" + ) + assertContains( + response, + company_url_with_job_seeker_id, + ) + + # Has link to job description with job_seeker public_id + job_description_url_with_job_seeker_id = ( + f"{job_description.get_absolute_url()}?job_seeker={job_seeker_public_id}" + f"&back_url={urlencode_filter(response.wsgi_request.get_full_path())}" + ) + assertContains(response, job_description_url_with_job_seeker_id) + + # Has link to apply to company with job_seeker public_id + apply_url_with_job_seeker_id = ( + f"{reverse('apply:start', kwargs={'company_pk':guerande_company.pk})}?job_seeker={job_seeker_public_id}" + ) + assertContains(response, apply_url_with_job_seeker_id) + class TestSearchPrescriber: def test_home(self, client): @@ -1168,3 +1223,52 @@ def distance_radio(distance): response = client.get(self.URL, {"city": guerande.slug, "distance": 100}) fresh_page = parse_response_to_soup(response) assertSoupEqual(simulated_page, fresh_page) + + def test_results_links_from_job_seeker_list(self, client): + """ + When applying from "Mes candidats" + """ + job_application = JobApplicationFactory( + job_seeker__first_name="Alain", + job_seeker__last_name="Zorro", + job_seeker__public_id="11111111-2222-3333-4444-555566667777", + ) + job_seeker_public_id = job_application.job_seeker.public_id + prescriber = job_application.sender + + create_test_romes_and_appellations(["N1101"], appellations_per_rome=1) + guerande = create_city_guerande() + COMPANY_GUERANDE = "Entreprise Guérande" + guerande_company = CompanyFactory( + name=COMPANY_GUERANDE, + department="44", + coords=guerande.coords, + post_code="44350", + kind=CompanyKind.AI, + with_membership=True, + ) + + job_description = JobDescriptionFactory(company=guerande_company, location=guerande) + client.force_login(prescriber) + + response = client.get( + self.URL, {"city": guerande.slug, "distance": 100, "job_seeker": job_application.job_seeker.public_id} + ) + assertContains(response, f"Vous postulez actuellement pour {job_application.job_seeker.get_full_name()}") + + # Has link to company card with job_seeker public_id + company_url_with_job_seeker_id = ( + f"{guerande_company.get_card_url()}?job_seeker={job_seeker_public_id}" + f"&back_url={urlencode_filter(response.wsgi_request.get_full_path())}" + ) + assertContains( + response, + company_url_with_job_seeker_id, + ) + + # Has link to job description with job_seeker public_id + job_description_url_with_job_seeker_id = ( + f"{job_description.get_absolute_url()}?job_seeker={job_seeker_public_id}" + f"&back_url={urlencode_filter(response.wsgi_request.get_full_path())}" + ) + assertContains(response, job_description_url_with_job_seeker_id)