From 0a3d5f442f761b00b29ef14a9e845b42a54f9faf Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Tue, 10 Sep 2024 16:17:32 +0200 Subject: [PATCH] apply: Keep job app list filters in back_url when performing an action --- itou/www/apply/views/process_views.py | 6 +- .../www/apply/__snapshots__/test_process.ambr | 926 +++++++++++++++++- tests/www/apply/test_process.py | 209 ++-- 3 files changed, 1068 insertions(+), 73 deletions(-) diff --git a/itou/www/apply/views/process_views.py b/itou/www/apply/views/process_views.py index 3a03920b6a0..ce83934e314 100644 --- a/itou/www/apply/views/process_views.py +++ b/itou/www/apply/views/process_views.py @@ -149,7 +149,11 @@ def details_for_company(request, job_application_id, template_name="apply/proces job_seeker=job_application.job_seeker, for_siae=job_application.to_company ) - back_url = get_safe_url(request, "back_url", fallback_url=reverse_lazy("apply:list_for_siae")) + # get back_url from GET params or session or fallback value + session_key = f"JOB_APP_DETAILS_FOR_COMPANY-BACK_URL-{job_application.pk}" + fallback_url = request.session.get(session_key, reverse_lazy("apply:list_for_siae")) + back_url = get_safe_url(request, "back_url", fallback_url=fallback_url) + request.session[session_key] = back_url geiq_eligibility_diagnosis = ( job_application.to_company.kind == CompanyKind.GEIQ diff --git a/tests/www/apply/__snapshots__/test_process.ambr b/tests/www/apply/__snapshots__/test_process.ambr index 359a8449b88..66f13534987 100644 --- a/tests/www/apply/__snapshots__/test_process.ambr +++ b/tests/www/apply/__snapshots__/test_process.ambr @@ -1,5 +1,929 @@ # serializer version: 1 -# name: ProcessViewsTest.test_details_for_company[job application detail for company] +# name: ProcessViewsTest.test_details_for_company_from_approval[job application detail for company] + dict({ + 'num_queries': 18, + 'queries': list([ + dict({ + 'origin': list([ + 'SessionStore._get_session_from_db[/django/contrib/sessions/backends/db.py]', + ]), + 'sql': ''' + SELECT "django_session"."session_key", + "django_session"."session_data", + "django_session"."expire_date" + FROM "django_session" + WHERE ("django_session"."expire_date" > %s + AND "django_session"."session_key" = %s) + LIMIT 21 + ''', + }), + dict({ + 'origin': list([ + 'ItouCurrentOrganizationMiddleware.__call__[utils/perms/middleware.py]', + ]), + 'sql': ''' + SELECT "users_user"."id", + "users_user"."password", + "users_user"."last_login", + "users_user"."is_superuser", + "users_user"."username", + "users_user"."first_name", + "users_user"."last_name", + "users_user"."is_staff", + "users_user"."is_active", + "users_user"."date_joined", + "users_user"."address_line_1", + "users_user"."address_line_2", + "users_user"."post_code", + "users_user"."city", + "users_user"."department", + "users_user"."coords", + "users_user"."geocoding_score", + "users_user"."geocoding_updated_at", + "users_user"."ban_api_resolved_address", + "users_user"."insee_city_id", + "users_user"."title", + "users_user"."email", + "users_user"."phone", + "users_user"."kind", + "users_user"."identity_provider", + "users_user"."has_completed_welcoming_tour", + "users_user"."created_by_id", + "users_user"."external_data_source_history", + "users_user"."last_checked_at", + "users_user"."public_id", + "users_user"."address_filled_at", + "users_user"."first_login" + FROM "users_user" + WHERE "users_user"."id" = %s + LIMIT 21 + ''', + }), + dict({ + 'origin': list([ + 'ItouCurrentOrganizationMiddleware.__call__[utils/perms/middleware.py]', + ]), + 'sql': ''' + SELECT "companies_companymembership"."id", + "companies_companymembership"."user_id", + "companies_companymembership"."joined_at", + "companies_companymembership"."is_admin", + "companies_companymembership"."is_active", + "companies_companymembership"."created_at", + "companies_companymembership"."updated_at", + "companies_companymembership"."company_id", + "companies_companymembership"."updated_by_id", + "companies_companymembership"."notifications" + FROM "companies_companymembership" + WHERE ("companies_companymembership"."user_id" = %s + AND "companies_companymembership"."is_active") + ORDER BY "companies_companymembership"."created_at" ASC + ''', + }), + dict({ + 'origin': list([ + 'ItouCurrentOrganizationMiddleware.__call__[utils/perms/middleware.py]', + ]), + 'sql': ''' + SELECT "companies_company"."id", + "companies_company"."address_line_1", + "companies_company"."address_line_2", + "companies_company"."post_code", + "companies_company"."city", + "companies_company"."department", + "companies_company"."coords", + "companies_company"."geocoding_score", + "companies_company"."geocoding_updated_at", + "companies_company"."ban_api_resolved_address", + "companies_company"."insee_city_id", + "companies_company"."name", + "companies_company"."created_at", + "companies_company"."updated_at", + "companies_company"."uid", + "companies_company"."active_members_email_reminder_last_sent_at", + "companies_company"."siret", + "companies_company"."naf", + "companies_company"."kind", + "companies_company"."brand", + "companies_company"."phone", + "companies_company"."email", + "companies_company"."auth_email", + "companies_company"."website", + "companies_company"."description", + "companies_company"."provided_support", + "companies_company"."source", + "companies_company"."created_by_id", + "companies_company"."block_job_applications", + "companies_company"."job_applications_blocked_at", + "companies_company"."convention_id", + "companies_company"."job_app_score", + "companies_company"."rdv_insertion_id", + EXISTS + (SELECT %s AS "a" + FROM "companies_siaeconvention" U0 + WHERE (U0."deactivated_at" >= %s + AND U0."id" = ("companies_company"."convention_id")) + LIMIT 1) AS "has_convention_in_grace_period" + FROM "companies_company" + INNER JOIN "companies_companymembership" ON ("companies_company"."id" = "companies_companymembership"."company_id") + WHERE (NOT ("companies_company"."siret" = %s) + AND "companies_companymembership"."user_id" = %s + AND "companies_company"."id" IN (%s) + AND (NOT ("companies_company"."kind" IN (%s, + %s, + %s, + %s, + %s)) + OR "companies_company"."source" = %s + OR EXISTS + (SELECT %s AS "a" + FROM "companies_siaeconvention" U0 + WHERE (U0."id" = ("companies_company"."convention_id") + AND U0."is_active") + LIMIT 1) + OR EXISTS + (SELECT %s AS "a" + FROM "companies_siaeconvention" U0 + WHERE (U0."deactivated_at" >= %s + AND U0."id" = ("companies_company"."convention_id")) + LIMIT 1))) + ORDER BY RANDOM() ASC + ''', + }), + dict({ + 'origin': list([ + 'Atomic.__enter__[/django/db/transaction.py]', + ]), + 'sql': 'SAVEPOINT ""', + }), + dict({ + 'origin': list([ + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT "job_applications_jobapplication"."id", + "job_applications_jobapplication"."job_seeker_id", + "job_applications_jobapplication"."eligibility_diagnosis_id", + "job_applications_jobapplication"."geiq_eligibility_diagnosis_id", + "job_applications_jobapplication"."create_employee_record", + "job_applications_jobapplication"."resume_link", + "job_applications_jobapplication"."sender_id", + "job_applications_jobapplication"."sender_kind", + "job_applications_jobapplication"."sender_company_id", + "job_applications_jobapplication"."sender_prescriber_organization_id", + "job_applications_jobapplication"."to_company_id", + "job_applications_jobapplication"."state", + "job_applications_jobapplication"."hired_job_id", + "job_applications_jobapplication"."message", + "job_applications_jobapplication"."answer", + "job_applications_jobapplication"."answer_to_prescriber", + "job_applications_jobapplication"."refusal_reason", + "job_applications_jobapplication"."refusal_reason_shared_with_job_seeker", + "job_applications_jobapplication"."hiring_start_at", + "job_applications_jobapplication"."hiring_end_at", + "job_applications_jobapplication"."hiring_without_approval", + "job_applications_jobapplication"."origin", + "job_applications_jobapplication"."approval_id", + "job_applications_jobapplication"."approval_delivery_mode", + "job_applications_jobapplication"."approval_number_sent_by_email", + "job_applications_jobapplication"."approval_number_sent_at", + "job_applications_jobapplication"."approval_manually_delivered_by_id", + "job_applications_jobapplication"."approval_manually_refused_by_id", + "job_applications_jobapplication"."approval_manually_refused_at", + "job_applications_jobapplication"."hidden_for_company", + "job_applications_jobapplication"."transferred_at", + "job_applications_jobapplication"."transferred_by_id", + "job_applications_jobapplication"."transferred_from_id", + "job_applications_jobapplication"."created_at", + "job_applications_jobapplication"."updated_at", + "job_applications_jobapplication"."processed_at", + "job_applications_jobapplication"."prehiring_guidance_days", + "job_applications_jobapplication"."contract_type", + "job_applications_jobapplication"."nb_hours_per_week", + "job_applications_jobapplication"."contract_type_details", + "job_applications_jobapplication"."qualification_type", + "job_applications_jobapplication"."qualification_level", + "job_applications_jobapplication"."planned_training_hours", + "job_applications_jobapplication"."inverted_vae_contract", + "job_applications_jobapplication"."diagoriente_invite_sent_at", + T5."id", + T5."password", + T5."last_login", + T5."is_superuser", + T5."username", + T5."first_name", + T5."last_name", + T5."is_staff", + T5."is_active", + T5."date_joined", + T5."address_line_1", + T5."address_line_2", + T5."post_code", + T5."city", + T5."department", + T5."coords", + T5."geocoding_score", + T5."geocoding_updated_at", + T5."ban_api_resolved_address", + T5."insee_city_id", + T5."title", + T5."email", + T5."phone", + T5."kind", + T5."identity_provider", + T5."has_completed_welcoming_tour", + T5."created_by_id", + T5."external_data_source_history", + T5."last_checked_at", + T5."public_id", + T5."address_filled_at", + T5."first_login", + "users_jobseekerprofile"."user_id", + "users_jobseekerprofile"."birthdate", + "users_jobseekerprofile"."birth_place_id", + "users_jobseekerprofile"."birth_country_id", + "users_jobseekerprofile"."nir", + "users_jobseekerprofile"."lack_of_nir_reason", + "users_jobseekerprofile"."pole_emploi_id", + "users_jobseekerprofile"."lack_of_pole_emploi_id_reason", + "users_jobseekerprofile"."asp_uid", + "users_jobseekerprofile"."education_level", + "users_jobseekerprofile"."resourceless", + "users_jobseekerprofile"."rqth_employee", + "users_jobseekerprofile"."oeth_employee", + "users_jobseekerprofile"."pole_emploi_since", + "users_jobseekerprofile"."unemployed_since", + "users_jobseekerprofile"."has_rsa_allocation", + "users_jobseekerprofile"."rsa_allocation_since", + "users_jobseekerprofile"."ass_allocation_since", + "users_jobseekerprofile"."aah_allocation_since", + "users_jobseekerprofile"."ata_allocation_since", + "users_jobseekerprofile"."hexa_lane_number", + "users_jobseekerprofile"."hexa_std_extension", + "users_jobseekerprofile"."hexa_non_std_extension", + "users_jobseekerprofile"."hexa_lane_type", + "users_jobseekerprofile"."hexa_lane_name", + "users_jobseekerprofile"."hexa_additional_address", + "users_jobseekerprofile"."hexa_post_code", + "users_jobseekerprofile"."hexa_commune_id", + "users_jobseekerprofile"."pe_obfuscated_nir", + "users_jobseekerprofile"."pe_last_certification_attempt_at", + "eligibility_eligibilitydiagnosis"."id", + "eligibility_eligibilitydiagnosis"."author_id", + "eligibility_eligibilitydiagnosis"."author_kind", + "eligibility_eligibilitydiagnosis"."author_prescriber_organization_id", + "eligibility_eligibilitydiagnosis"."created_at", + "eligibility_eligibilitydiagnosis"."updated_at", + "eligibility_eligibilitydiagnosis"."expires_at", + "eligibility_eligibilitydiagnosis"."job_seeker_id", + "eligibility_eligibilitydiagnosis"."author_siae_id", + T8."id", + T8."password", + T8."last_login", + T8."is_superuser", + T8."username", + T8."first_name", + T8."last_name", + T8."is_staff", + T8."is_active", + T8."date_joined", + T8."address_line_1", + T8."address_line_2", + T8."post_code", + T8."city", + T8."department", + T8."coords", + T8."geocoding_score", + T8."geocoding_updated_at", + T8."ban_api_resolved_address", + T8."insee_city_id", + T8."title", + T8."email", + T8."phone", + T8."kind", + T8."identity_provider", + T8."has_completed_welcoming_tour", + T8."created_by_id", + T8."external_data_source_history", + T8."last_checked_at", + T8."public_id", + T8."address_filled_at", + T8."first_login", + "prescribers_prescriberorganization"."id", + "prescribers_prescriberorganization"."address_line_1", + "prescribers_prescriberorganization"."address_line_2", + "prescribers_prescriberorganization"."post_code", + "prescribers_prescriberorganization"."city", + "prescribers_prescriberorganization"."department", + "prescribers_prescriberorganization"."coords", + "prescribers_prescriberorganization"."geocoding_score", + "prescribers_prescriberorganization"."geocoding_updated_at", + "prescribers_prescriberorganization"."ban_api_resolved_address", + "prescribers_prescriberorganization"."insee_city_id", + "prescribers_prescriberorganization"."name", + "prescribers_prescriberorganization"."created_at", + "prescribers_prescriberorganization"."updated_at", + "prescribers_prescriberorganization"."uid", + "prescribers_prescriberorganization"."active_members_email_reminder_last_sent_at", + "prescribers_prescriberorganization"."siret", + "prescribers_prescriberorganization"."is_head_office", + "prescribers_prescriberorganization"."kind", + "prescribers_prescriberorganization"."is_brsa", + "prescribers_prescriberorganization"."phone", + "prescribers_prescriberorganization"."email", + "prescribers_prescriberorganization"."website", + "prescribers_prescriberorganization"."description", + "prescribers_prescriberorganization"."is_authorized", + "prescribers_prescriberorganization"."code_safir_pole_emploi", + "prescribers_prescriberorganization"."created_by_id", + "prescribers_prescriberorganization"."authorization_status", + "prescribers_prescriberorganization"."authorization_updated_at", + "prescribers_prescriberorganization"."authorization_updated_by_id", + T10."id", + T10."password", + T10."last_login", + T10."is_superuser", + T10."username", + T10."first_name", + T10."last_name", + T10."is_staff", + T10."is_active", + T10."date_joined", + T10."address_line_1", + T10."address_line_2", + T10."post_code", + T10."city", + T10."department", + T10."coords", + T10."geocoding_score", + T10."geocoding_updated_at", + T10."ban_api_resolved_address", + T10."insee_city_id", + T10."title", + T10."email", + T10."phone", + T10."kind", + T10."identity_provider", + T10."has_completed_welcoming_tour", + T10."created_by_id", + T10."external_data_source_history", + T10."last_checked_at", + T10."public_id", + T10."address_filled_at", + T10."first_login", + T11."user_id", + T11."birthdate", + T11."birth_place_id", + T11."birth_country_id", + T11."nir", + T11."lack_of_nir_reason", + T11."pole_emploi_id", + T11."lack_of_pole_emploi_id_reason", + T11."asp_uid", + T11."education_level", + T11."resourceless", + T11."rqth_employee", + T11."oeth_employee", + T11."pole_emploi_since", + T11."unemployed_since", + T11."has_rsa_allocation", + T11."rsa_allocation_since", + T11."ass_allocation_since", + T11."aah_allocation_since", + T11."ata_allocation_since", + T11."hexa_lane_number", + T11."hexa_std_extension", + T11."hexa_non_std_extension", + T11."hexa_lane_type", + T11."hexa_lane_name", + T11."hexa_additional_address", + T11."hexa_post_code", + T11."hexa_commune_id", + T11."pe_obfuscated_nir", + T11."pe_last_certification_attempt_at", + T12."id", + T12."address_line_1", + T12."address_line_2", + T12."post_code", + T12."city", + T12."department", + T12."coords", + T12."geocoding_score", + T12."geocoding_updated_at", + T12."ban_api_resolved_address", + T12."insee_city_id", + T12."name", + T12."created_at", + T12."updated_at", + T12."uid", + T12."active_members_email_reminder_last_sent_at", + T12."siret", + T12."naf", + T12."kind", + T12."brand", + T12."phone", + T12."email", + T12."auth_email", + T12."website", + T12."description", + T12."provided_support", + T12."source", + T12."created_by_id", + T12."block_job_applications", + T12."job_applications_blocked_at", + T12."convention_id", + T12."job_app_score", + T12."rdv_insertion_id", + "eligibility_geiqeligibilitydiagnosis"."id", + "eligibility_geiqeligibilitydiagnosis"."author_id", + "eligibility_geiqeligibilitydiagnosis"."author_kind", + "eligibility_geiqeligibilitydiagnosis"."author_prescriber_organization_id", + "eligibility_geiqeligibilitydiagnosis"."created_at", + "eligibility_geiqeligibilitydiagnosis"."updated_at", + "eligibility_geiqeligibilitydiagnosis"."expires_at", + "eligibility_geiqeligibilitydiagnosis"."job_seeker_id", + "eligibility_geiqeligibilitydiagnosis"."author_geiq_id", + T14."id", + T14."password", + T14."last_login", + T14."is_superuser", + T14."username", + T14."first_name", + T14."last_name", + T14."is_staff", + T14."is_active", + T14."date_joined", + T14."address_line_1", + T14."address_line_2", + T14."post_code", + T14."city", + T14."department", + T14."coords", + T14."geocoding_score", + T14."geocoding_updated_at", + T14."ban_api_resolved_address", + T14."insee_city_id", + T14."title", + T14."email", + T14."phone", + T14."kind", + T14."identity_provider", + T14."has_completed_welcoming_tour", + T14."created_by_id", + T14."external_data_source_history", + T14."last_checked_at", + T14."public_id", + T14."address_filled_at", + T14."first_login", + T15."id", + T15."address_line_1", + T15."address_line_2", + T15."post_code", + T15."city", + T15."department", + T15."coords", + T15."geocoding_score", + T15."geocoding_updated_at", + T15."ban_api_resolved_address", + T15."insee_city_id", + T15."name", + T15."created_at", + T15."updated_at", + T15."uid", + T15."active_members_email_reminder_last_sent_at", + T15."siret", + T15."naf", + T15."kind", + T15."brand", + T15."phone", + T15."email", + T15."auth_email", + T15."website", + T15."description", + T15."provided_support", + T15."source", + T15."created_by_id", + T15."block_job_applications", + T15."job_applications_blocked_at", + T15."convention_id", + T15."job_app_score", + T15."rdv_insertion_id", + T16."id", + T16."address_line_1", + T16."address_line_2", + T16."post_code", + T16."city", + T16."department", + T16."coords", + T16."geocoding_score", + T16."geocoding_updated_at", + T16."ban_api_resolved_address", + T16."insee_city_id", + T16."name", + T16."created_at", + T16."updated_at", + T16."uid", + T16."active_members_email_reminder_last_sent_at", + T16."siret", + T16."is_head_office", + T16."kind", + T16."is_brsa", + T16."phone", + T16."email", + T16."website", + T16."description", + T16."is_authorized", + T16."code_safir_pole_emploi", + T16."created_by_id", + T16."authorization_status", + T16."authorization_updated_at", + T16."authorization_updated_by_id", + "companies_company"."id", + "companies_company"."address_line_1", + "companies_company"."address_line_2", + "companies_company"."post_code", + "companies_company"."city", + "companies_company"."department", + "companies_company"."coords", + "companies_company"."geocoding_score", + "companies_company"."geocoding_updated_at", + "companies_company"."ban_api_resolved_address", + "companies_company"."insee_city_id", + "companies_company"."name", + "companies_company"."created_at", + "companies_company"."updated_at", + "companies_company"."uid", + "companies_company"."active_members_email_reminder_last_sent_at", + "companies_company"."siret", + "companies_company"."naf", + "companies_company"."kind", + "companies_company"."brand", + "companies_company"."phone", + "companies_company"."email", + "companies_company"."auth_email", + "companies_company"."website", + "companies_company"."description", + "companies_company"."provided_support", + "companies_company"."source", + "companies_company"."created_by_id", + "companies_company"."block_job_applications", + "companies_company"."job_applications_blocked_at", + "companies_company"."convention_id", + "companies_company"."job_app_score", + "companies_company"."rdv_insertion_id", + "approvals_approval"."id", + "approvals_approval"."start_at", + "approvals_approval"."end_at", + "approvals_approval"."created_at", + "approvals_approval"."number", + "approvals_approval"."pe_notification_status", + "approvals_approval"."pe_notification_time", + "approvals_approval"."pe_notification_endpoint", + "approvals_approval"."pe_notification_exit_code", + "approvals_approval"."user_id", + "approvals_approval"."created_by_id", + "approvals_approval"."origin", + "approvals_approval"."eligibility_diagnosis_id", + "approvals_approval"."updated_at", + "approvals_approval"."origin_siae_siret", + "approvals_approval"."origin_siae_kind", + "approvals_approval"."origin_sender_kind", + "approvals_approval"."origin_prescriber_organization_kind" + FROM "job_applications_jobapplication" + INNER JOIN "companies_company" ON ("job_applications_jobapplication"."to_company_id" = "companies_company"."id") + INNER JOIN "companies_companymembership" ON ("companies_company"."id" = "companies_companymembership"."company_id") + INNER JOIN "users_user" ON ("companies_companymembership"."user_id" = "users_user"."id") + INNER JOIN "users_user" T5 ON ("job_applications_jobapplication"."job_seeker_id" = T5."id") + LEFT OUTER JOIN "users_jobseekerprofile" ON (T5."id" = "users_jobseekerprofile"."user_id") + LEFT OUTER JOIN "eligibility_eligibilitydiagnosis" ON ("job_applications_jobapplication"."eligibility_diagnosis_id" = "eligibility_eligibilitydiagnosis"."id") + LEFT OUTER JOIN "users_user" T8 ON ("eligibility_eligibilitydiagnosis"."author_id" = T8."id") + LEFT OUTER JOIN "prescribers_prescriberorganization" ON ("eligibility_eligibilitydiagnosis"."author_prescriber_organization_id" = "prescribers_prescriberorganization"."id") + LEFT OUTER JOIN "users_user" T10 ON ("eligibility_eligibilitydiagnosis"."job_seeker_id" = T10."id") + LEFT OUTER JOIN "users_jobseekerprofile" T11 ON (T10."id" = T11."user_id") + LEFT OUTER JOIN "companies_company" T12 ON ("eligibility_eligibilitydiagnosis"."author_siae_id" = T12."id") + LEFT OUTER JOIN "eligibility_geiqeligibilitydiagnosis" ON ("job_applications_jobapplication"."geiq_eligibility_diagnosis_id" = "eligibility_geiqeligibilitydiagnosis"."id") + LEFT OUTER JOIN "users_user" T14 ON ("job_applications_jobapplication"."sender_id" = T14."id") + LEFT OUTER JOIN "companies_company" T15 ON ("job_applications_jobapplication"."sender_company_id" = T15."id") + LEFT OUTER JOIN "prescribers_prescriberorganization" T16 ON ("job_applications_jobapplication"."sender_prescriber_organization_id" = T16."id") + LEFT OUTER JOIN "approvals_approval" ON ("job_applications_jobapplication"."approval_id" = "approvals_approval"."id") + WHERE ("companies_companymembership"."user_id" = %s + AND "users_user"."is_active" + AND NOT ("job_applications_jobapplication"."hidden_for_company") + AND "job_applications_jobapplication"."id" = %s) + LIMIT 21 + ''', + }), + dict({ + 'origin': list([ + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT ("job_applications_jobapplication_selected_jobs"."jobapplication_id") AS "_prefetch_related_val_jobapplication_id", + "companies_jobdescription"."id", + "companies_jobdescription"."appellation_id", + "companies_jobdescription"."company_id", + "companies_jobdescription"."created_at", + "companies_jobdescription"."updated_at", + "companies_jobdescription"."is_active", + "companies_jobdescription"."custom_name", + "companies_jobdescription"."description", + "companies_jobdescription"."ui_rank", + "companies_jobdescription"."contract_type", + "companies_jobdescription"."other_contract_type", + "companies_jobdescription"."contract_nature", + "companies_jobdescription"."location_id", + "companies_jobdescription"."hours_per_week", + "companies_jobdescription"."open_positions", + "companies_jobdescription"."profile_description", + "companies_jobdescription"."is_resume_mandatory", + "companies_jobdescription"."is_qpv_mandatory", + "companies_jobdescription"."market_context_description", + "companies_jobdescription"."source_id", + "companies_jobdescription"."source_kind", + "companies_jobdescription"."source_url", + "companies_jobdescription"."field_history", + "companies_jobdescription"."creation_source" + FROM "companies_jobdescription" + INNER JOIN "job_applications_jobapplication_selected_jobs" ON ("companies_jobdescription"."id" = "job_applications_jobapplication_selected_jobs"."jobdescription_id") + INNER JOIN "jobs_appellation" ON ("companies_jobdescription"."appellation_id" = "jobs_appellation"."code") + WHERE "job_applications_jobapplication_selected_jobs"."jobapplication_id" IN (%s) + ORDER BY "jobs_appellation"."name" ASC, + "companies_jobdescription"."ui_rank" ASC + ''', + }), + dict({ + 'origin': list([ + 'User.latest_approval[users/models.py]', + 'User.has_valid_common_approval[users/models.py]', + 'EligibilityDiagnosisManagerFromEligibilityDiagnosisQuerySet.has_considered_valid[eligibility/models/iae.py]', + 'EligibilityDiagnosisManagerFromEligibilityDiagnosisQuerySet.last_expired[eligibility/models/iae.py]', + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT "approvals_approval"."id", + "approvals_approval"."start_at", + "approvals_approval"."end_at", + "approvals_approval"."created_at", + "approvals_approval"."number", + "approvals_approval"."pe_notification_status", + "approvals_approval"."pe_notification_time", + "approvals_approval"."pe_notification_endpoint", + "approvals_approval"."pe_notification_exit_code", + "approvals_approval"."user_id", + "approvals_approval"."created_by_id", + "approvals_approval"."origin", + "approvals_approval"."eligibility_diagnosis_id", + "approvals_approval"."updated_at", + "approvals_approval"."origin_siae_siret", + "approvals_approval"."origin_siae_kind", + "approvals_approval"."origin_sender_kind", + "approvals_approval"."origin_prescriber_organization_kind" + FROM "approvals_approval" + WHERE "approvals_approval"."user_id" = %s + ORDER BY "approvals_approval"."created_at" DESC + ''', + }), + dict({ + 'origin': list([ + 'Company.is_active[companies/models.py]', + 'nav[utils/templatetags/nav.py]', + 'InclusionNode[layout/_header_authenticated.html]', + 'IncludeNode[layout/base.html]', + 'IfNode[layout/base.html]', + 'ExtendsNode[apply/process_base.html]', + 'ExtendsNode[apply/process_details_company.html]', + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT "companies_siaeconvention"."id", + "companies_siaeconvention"."kind", + "companies_siaeconvention"."siret_signature", + "companies_siaeconvention"."is_active", + "companies_siaeconvention"."deactivated_at", + "companies_siaeconvention"."reactivated_by_id", + "companies_siaeconvention"."reactivated_at", + "companies_siaeconvention"."asp_id", + "companies_siaeconvention"."created_at", + "companies_siaeconvention"."updated_at" + FROM "companies_siaeconvention" + WHERE "companies_siaeconvention"."id" = %s + LIMIT 21 + ''', + }), + dict({ + 'origin': list([ + 'Company.has_admin[common_apps/organizations/models.py]', + 'Company.convention_can_be_accessed_by[companies/models.py]', + 'nav[utils/templatetags/nav.py]', + 'InclusionNode[layout/_header_authenticated.html]', + 'IncludeNode[layout/base.html]', + 'IfNode[layout/base.html]', + 'ExtendsNode[apply/process_base.html]', + 'ExtendsNode[apply/process_details_company.html]', + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT %s AS "a" + FROM "users_user" + INNER JOIN "companies_companymembership" ON ("users_user"."id" = "companies_companymembership"."user_id") + WHERE ("companies_companymembership"."id" IN + (SELECT U0."id" + FROM "companies_companymembership" U0 + INNER JOIN "users_user" U2 ON (U0."user_id" = U2."id") + WHERE (U0."company_id" = %s + AND U2."is_active" + AND U0."is_active" + AND U0."is_admin" + AND U2."is_active")) + AND "users_user"."id" = %s) + LIMIT 1 + ''', + }), + dict({ + 'origin': list([ + 'IfNode[apply/includes/eligibility_diagnosis.html]', + 'WithNode[apply/includes/eligibility_diagnosis.html]', + 'IfNode[apply/includes/eligibility_diagnosis.html]', + 'IfNode[apply/includes/eligibility_diagnosis.html]', + 'IfNode[apply/includes/eligibility_diagnosis.html]', + 'IncludeNode[apply/process_details_company.html]', + 'IfNode[apply/process_details_company.html]', + 'BlockNode[apply/process_base.html]', + 'BlockNode[layout/base.html]', + 'ExtendsNode[apply/process_base.html]', + 'ExtendsNode[apply/process_details_company.html]', + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT "eligibility_administrativecriteria"."id", + "eligibility_administrativecriteria"."kind", + "eligibility_administrativecriteria"."level", + "eligibility_administrativecriteria"."name", + "eligibility_administrativecriteria"."desc", + "eligibility_administrativecriteria"."written_proof", + "eligibility_administrativecriteria"."written_proof_url", + "eligibility_administrativecriteria"."written_proof_validity", + "eligibility_administrativecriteria"."ui_rank", + "eligibility_administrativecriteria"."created_at", + "eligibility_administrativecriteria"."created_by_id" + FROM "eligibility_administrativecriteria" + INNER JOIN "eligibility_selectedadministrativecriteria" ON ("eligibility_administrativecriteria"."id" = "eligibility_selectedadministrativecriteria"."administrative_criteria_id") + WHERE "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id" = %s + ORDER BY "eligibility_administrativecriteria"."level" ASC, + "eligibility_administrativecriteria"."ui_rank" ASC + ''', + }), + dict({ + 'origin': list([ + 'User.latest_approval[users/models.py]', + 'User.has_valid_common_approval[users/models.py]', + 'EligibilityDiagnosis.considered_to_expire_at[eligibility/models/iae.py]', + 'VariableNode[apply/includes/eligibility_diagnosis.html]', + 'IfNode[apply/includes/eligibility_diagnosis.html]', + 'IfNode[apply/includes/eligibility_diagnosis.html]', + 'IfNode[apply/includes/eligibility_diagnosis.html]', + 'IncludeNode[apply/process_details_company.html]', + 'IfNode[apply/process_details_company.html]', + 'BlockNode[apply/process_base.html]', + 'BlockNode[layout/base.html]', + 'ExtendsNode[apply/process_base.html]', + 'ExtendsNode[apply/process_details_company.html]', + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT "approvals_approval"."id", + "approvals_approval"."start_at", + "approvals_approval"."end_at", + "approvals_approval"."created_at", + "approvals_approval"."number", + "approvals_approval"."pe_notification_status", + "approvals_approval"."pe_notification_time", + "approvals_approval"."pe_notification_endpoint", + "approvals_approval"."pe_notification_exit_code", + "approvals_approval"."user_id", + "approvals_approval"."created_by_id", + "approvals_approval"."origin", + "approvals_approval"."eligibility_diagnosis_id", + "approvals_approval"."updated_at", + "approvals_approval"."origin_siae_siret", + "approvals_approval"."origin_siae_kind", + "approvals_approval"."origin_sender_kind", + "approvals_approval"."origin_prescriber_organization_kind" + FROM "approvals_approval" + WHERE "approvals_approval"."user_id" = %s + ORDER BY "approvals_approval"."created_at" DESC + ''', + }), + dict({ + 'origin': list([ + 'ForNode[apply/includes/transition_logs.html]', + 'WithNode[apply/includes/transition_logs.html]', + 'IncludeNode[apply/process_details_company.html]', + 'BlockNode[apply/process_base.html]', + 'BlockNode[layout/base.html]', + 'ExtendsNode[apply/process_base.html]', + 'ExtendsNode[apply/process_details_company.html]', + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT "job_applications_jobapplicationtransitionlog"."id", + "job_applications_jobapplicationtransitionlog"."transition", + "job_applications_jobapplicationtransitionlog"."from_state", + "job_applications_jobapplicationtransitionlog"."to_state", + "job_applications_jobapplicationtransitionlog"."timestamp", + "job_applications_jobapplicationtransitionlog"."job_application_id", + "job_applications_jobapplicationtransitionlog"."user_id", + "job_applications_jobapplicationtransitionlog"."target_company_id", + "users_user"."id", + "users_user"."password", + "users_user"."last_login", + "users_user"."is_superuser", + "users_user"."username", + "users_user"."first_name", + "users_user"."last_name", + "users_user"."is_staff", + "users_user"."is_active", + "users_user"."date_joined", + "users_user"."address_line_1", + "users_user"."address_line_2", + "users_user"."post_code", + "users_user"."city", + "users_user"."department", + "users_user"."coords", + "users_user"."geocoding_score", + "users_user"."geocoding_updated_at", + "users_user"."ban_api_resolved_address", + "users_user"."insee_city_id", + "users_user"."title", + "users_user"."email", + "users_user"."phone", + "users_user"."kind", + "users_user"."identity_provider", + "users_user"."has_completed_welcoming_tour", + "users_user"."created_by_id", + "users_user"."external_data_source_history", + "users_user"."last_checked_at", + "users_user"."public_id", + "users_user"."address_filled_at", + "users_user"."first_login" + FROM "job_applications_jobapplicationtransitionlog" + LEFT OUTER JOIN "users_user" ON ("job_applications_jobapplicationtransitionlog"."user_id" = "users_user"."id") + WHERE "job_applications_jobapplicationtransitionlog"."job_application_id" = %s + ORDER BY "job_applications_jobapplicationtransitionlog"."timestamp" DESC + ''', + }), + dict({ + 'origin': list([ + 'JobApplication.can_be_cancelled[job_applications/models.py]', + 'IfNode[apply/process_details_company.html]', + 'BlockNode[apply/process_base.html]', + 'BlockNode[layout/base.html]', + 'ExtendsNode[apply/process_base.html]', + 'ExtendsNode[apply/process_details_company.html]', + 'details_for_company[www/apply/views/process_views.py]', + ]), + 'sql': ''' + SELECT %s AS "a" + FROM "employee_record_employeerecord" + WHERE "employee_record_employeerecord"."job_application_id" = %s + LIMIT 1 + ''', + }), + dict({ + 'origin': list([ + 'Atomic.__exit__[/django/db/transaction.py]', + ]), + 'sql': 'RELEASE SAVEPOINT ""', + }), + dict({ + 'origin': list([ + 'Atomic.__enter__[/django/db/transaction.py]', + 'SessionStore.save[/django/contrib/sessions/backends/db.py]', + ]), + 'sql': 'SAVEPOINT ""', + }), + dict({ + 'origin': list([ + 'SessionStore.save[/django/contrib/sessions/backends/db.py]', + ]), + 'sql': ''' + UPDATE "django_session" + SET "session_data" = %s, + "expire_date" = %s + WHERE "django_session"."session_key" = %s + ''', + }), + dict({ + 'origin': list([ + 'Atomic.__exit__[/django/db/transaction.py]', + 'SessionStore.save[/django/contrib/sessions/backends/db.py]', + ]), + 'sql': 'RELEASE SAVEPOINT ""', + }), + ]), + }) +# --- +# name: ProcessViewsTest.test_details_for_company_from_list[job application detail for company] dict({ 'num_queries': 18, 'queries': list([ diff --git a/tests/www/apply/test_process.py b/tests/www/apply/test_process.py index d9732856448..db9f0d1210a 100644 --- a/tests/www/apply/test_process.py +++ b/tests/www/apply/test_process.py @@ -117,8 +117,8 @@ def _get_transition_logs_content(self, response, job_application): soup = BeautifulSoup(response.content, "html5lib", from_encoding=response.charset or "utf-8") return soup.find("ul", attrs={"id": "transition_logs_" + str(job_application.id)}) - def test_details_for_company(self, *args, **kwargs): - """Display the details of a job application.""" + def test_details_for_company_from_approval(self, *args, **kwargs): + """Display the details of a job application coming from the approval detail page.""" job_application = JobApplicationFactory( sent_by_authorized_prescriber_organisation=True, resume_link="", with_approval=True @@ -165,7 +165,71 @@ def test_details_for_company(self, *args, **kwargs): self.assertContains( response, 'Numéro de sécurité socialeNon renseigné', html=True ) - assert_previous_step(response, reverse("apply:list_for_siae"), back_to_list=True) + assert_previous_step(response, back_url) # Back_url is restored from session + + job_application.job_seeker.jobseeker_profile.lack_of_nir_reason = LackOfNIRReason.TEMPORARY_NUMBER + job_application.job_seeker.jobseeker_profile.save() + + url = reverse("apply:details_for_company", kwargs={"job_application_id": job_application.pk}) + response = self.client.get(url) + self.assertContains(response, LackOfNIRReason.TEMPORARY_NUMBER.label) + + # Test resume presence: + resume_link = "https://server.com/sylvester-stallone.pdf" + job_application = JobApplicationSentByJobSeekerFactory(to_company=company, resume_link=resume_link) + url = reverse("apply:details_for_company", kwargs={"job_application_id": job_application.pk}) + response = self.client.get(url) + self.assertContains(response, resume_link) + self.assertNotContains(response, PRIOR_ACTION_SECTION_TITLE) + + def test_details_for_company_from_list(self, *args, **kwargs): + """Display the details of a job application coming from the job applications list.""" + + job_application = JobApplicationFactory( + sent_by_authorized_prescriber_organisation=True, resume_link="", with_approval=True + ) + company = job_application.to_company + employer = company.members.first() + self.client.force_login(employer) + + back_url = f"{reverse('apply:list_for_siae')}?job_seeker={job_application.job_seeker.id}" + url = add_url_params( + reverse("apply:details_for_company", kwargs={"job_application_id": job_application.pk}), + {"back_url": back_url}, + ) + with assertSnapshotQueries(self.snapshot(name="job application detail for company")): + response = self.client.get(url) + self.assertContains(response, "Ce candidat a pris le contrôle de son compte utilisateur.") + self.assertContains(response, format_nir(job_application.job_seeker.jobseeker_profile.nir)) + self.assertContains(response, job_application.job_seeker.jobseeker_profile.pole_emploi_id) + self.assertContains(response, job_application.job_seeker.phone.replace(" ", "")) + self.assertNotContains(response, PRIOR_ACTION_SECTION_TITLE) # the company is not a GEIQ + assert_previous_step(response, back_url, back_to_list=True) + + job_application.job_seeker.created_by = employer + job_application.job_seeker.phone = "" + job_application.job_seeker.save() + job_application.job_seeker.jobseeker_profile.nir = "" + job_application.job_seeker.jobseeker_profile.pole_emploi_id = "" + job_application.job_seeker.jobseeker_profile.save() + + url = reverse("apply:details_for_company", kwargs={"job_application_id": job_application.pk}) + response = self.client.get(url) + self.assertContains(response, "Modifier les informations") + self.assertContains(response, 'AdresseNon renseignée', html=True) + self.assertContains(response, 'TéléphoneNon renseigné', html=True) + self.assertContains( + response, 'Curriculum vitaeNon renseigné', html=True + ) + self.assertContains( + response, + 'Identifiant France TravailNon renseigné', + html=True, + ) + self.assertContains( + response, 'Numéro de sécurité socialeNon renseigné', html=True + ) + assert_previous_step(response, back_url, back_to_list=True) # Back_url is restored from session job_application.job_seeker.jobseeker_profile.lack_of_nir_reason = LackOfNIRReason.TEMPORARY_NUMBER job_application.job_seeker.jobseeker_profile.save() @@ -1476,82 +1540,85 @@ def test_diagoriente_invite_as_job_prescriber(self): assert len(mail.outbox) == 0 def test_diagoriente_invite_as_employee_for_authorized_prescriber(self): - job_application = JobApplicationFactory( - sent_by_authorized_prescriber_organisation=True, - resume_link="https://myresume.com/me", - ) - company = job_application.to_company - employee = company.members.first() - self.client.force_login(employee) - - # Should not perform any action if a resume is set - response = self.client.post( - reverse("apply:send_diagoriente_invite", kwargs={"job_application_id": job_application.pk}), - follow=True, - ) - self.assertMessages(response, []) - self.assertTemplateNotUsed(response, "apply/includes/job_application_diagoriente_invite.html") - self.assertNotContains(response, self.DIAGORIENTE_INVITE_TITLE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_PRESCRIBER_MESSAGE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_JOB_SEEKER_MESSAGE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_BUTTON_TITLE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_TOOLTIP) - job_application.refresh_from_db() - assert job_application.diagoriente_invite_sent_at is None - assert len(mail.outbox) == 0 + with freeze_time("2023-12-12 13:37:00") as frozen_time: + job_application = JobApplicationFactory( + sent_by_authorized_prescriber_organisation=True, + resume_link="https://myresume.com/me", + ) + company = job_application.to_company + employee = company.members.first() + self.client.force_login(employee) - # Unset resume, should now update the timestamp and send the mail - job_application.resume_link = "" - job_application.save(update_fields=["resume_link"]) - with freeze_time("2023-12-12 13:37:00") as initial_invite_time: + # Should not perform any action if a resume is set response = self.client.post( reverse("apply:send_diagoriente_invite", kwargs={"job_application_id": job_application.pk}), follow=True, ) - self.assertMessages( - response, [messages.Message(messages.SUCCESS, "L'invitation à utiliser Diagoriente a été envoyée.")] - ) - self.assertTemplateUsed(response, "apply/includes/job_application_diagoriente_invite.html") - self.assertNotContains(response, self.DIAGORIENTE_INVITE_TITLE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_PRESCRIBER_MESSAGE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_JOB_SEEKER_MESSAGE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_BUTTON_TITLE) - self.assertContains(response, self.DIAGORIENTE_INVITE_TOOLTIP) - job_application.refresh_from_db() - assert job_application.diagoriente_invite_sent_at == initial_invite_time().replace(tzinfo=datetime.UTC) - assert len(mail.outbox) == 1 - assert self.DIAGORIENTE_INVITE_EMAIL_SUBJECT in mail.outbox[0].subject - assert ( - self.DIAGORIENTE_INVITE_EMAIL_PRESCRIBER_BODY_HEADER_LINE_1.format( - company_name=job_application.to_company.display_name, - job_seeker_name=job_application.job_seeker.get_full_name(), + self.assertMessages(response, []) + self.assertTemplateNotUsed(response, "apply/includes/job_application_diagoriente_invite.html") + self.assertNotContains(response, self.DIAGORIENTE_INVITE_TITLE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_PRESCRIBER_MESSAGE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_JOB_SEEKER_MESSAGE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_BUTTON_TITLE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_TOOLTIP) + job_application.refresh_from_db() + assert job_application.diagoriente_invite_sent_at is None + assert len(mail.outbox) == 0 + + # Unset resume, should now update the timestamp and send the mail + job_application.resume_link = "" + job_application.save(update_fields=["resume_link"]) + frozen_time.tick() + initial_invite_time = frozen_time() + response = self.client.post( + reverse("apply:send_diagoriente_invite", kwargs={"job_application_id": job_application.pk}), + follow=True, ) - in mail.outbox[0].body - ) - assert self.DIAGORIENTE_INVITE_EMAIL_PRESCRIBER_BODY_HEADER_LINE_2 in mail.outbox[0].body - assert ( - self.DIAGORIENTE_INVITE_EMAIL_JOB_SEEKER_BODY_HEADER_LINE_1.format( - company_name=job_application.to_company.display_name + self.assertMessages( + response, [messages.Message(messages.SUCCESS, "L'invitation à utiliser Diagoriente a été envoyée.")] ) - not in mail.outbox[0].body - ) - assert self.DIAGORIENTE_INVITE_EMAIL_JOB_SEEKER_BODY_HEADER_LINE_2 not in mail.outbox[0].body + self.assertTemplateUsed(response, "apply/includes/job_application_diagoriente_invite.html") + self.assertNotContains(response, self.DIAGORIENTE_INVITE_TITLE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_PRESCRIBER_MESSAGE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_JOB_SEEKER_MESSAGE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_BUTTON_TITLE) + self.assertContains(response, self.DIAGORIENTE_INVITE_TOOLTIP) + job_application.refresh_from_db() + assert job_application.diagoriente_invite_sent_at == initial_invite_time.replace(tzinfo=datetime.UTC) + assert len(mail.outbox) == 1 + assert self.DIAGORIENTE_INVITE_EMAIL_SUBJECT in mail.outbox[0].subject + assert ( + self.DIAGORIENTE_INVITE_EMAIL_PRESCRIBER_BODY_HEADER_LINE_1.format( + company_name=job_application.to_company.display_name, + job_seeker_name=job_application.job_seeker.get_full_name(), + ) + in mail.outbox[0].body + ) + assert self.DIAGORIENTE_INVITE_EMAIL_PRESCRIBER_BODY_HEADER_LINE_2 in mail.outbox[0].body + assert ( + self.DIAGORIENTE_INVITE_EMAIL_JOB_SEEKER_BODY_HEADER_LINE_1.format( + company_name=job_application.to_company.display_name + ) + not in mail.outbox[0].body + ) + assert self.DIAGORIENTE_INVITE_EMAIL_JOB_SEEKER_BODY_HEADER_LINE_2 not in mail.outbox[0].body - # Concurrent/subsequent calls should not perform any action - response = self.client.post( - reverse("apply:send_diagoriente_invite", kwargs={"job_application_id": job_application.pk}), - follow=True, - ) - self.assertMessages(response, []) - self.assertTemplateUsed(response, "apply/includes/job_application_diagoriente_invite.html") - self.assertNotContains(response, self.DIAGORIENTE_INVITE_TITLE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_PRESCRIBER_MESSAGE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_JOB_SEEKER_MESSAGE) - self.assertNotContains(response, self.DIAGORIENTE_INVITE_BUTTON_TITLE) - self.assertContains(response, self.DIAGORIENTE_INVITE_TOOLTIP) - job_application.refresh_from_db() - assert job_application.diagoriente_invite_sent_at == initial_invite_time().replace(tzinfo=datetime.UTC) - assert len(mail.outbox) == 1 + # Concurrent/subsequent calls should not perform any action + frozen_time.tick() + response = self.client.post( + reverse("apply:send_diagoriente_invite", kwargs={"job_application_id": job_application.pk}), + follow=True, + ) + self.assertMessages(response, []) + self.assertTemplateUsed(response, "apply/includes/job_application_diagoriente_invite.html") + self.assertNotContains(response, self.DIAGORIENTE_INVITE_TITLE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_PRESCRIBER_MESSAGE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_JOB_SEEKER_MESSAGE) + self.assertNotContains(response, self.DIAGORIENTE_INVITE_BUTTON_TITLE) + self.assertContains(response, self.DIAGORIENTE_INVITE_TOOLTIP) + job_application.refresh_from_db() + assert job_application.diagoriente_invite_sent_at == initial_invite_time.replace(tzinfo=datetime.UTC) + assert len(mail.outbox) == 1 def test_diagoriente_invite_as_employee_for_unauthorized_prescriber(self): job_application = JobApplicationFactory(