From 81e4c0799c10d304fd74191eeb9de29c4f0f708d Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Tue, 17 Dec 2024 17:48:21 +0100 Subject: [PATCH] wip to reuse wizardview --- itou/www/apply/views/batch_views.py | 5 + itou/www/apply/views/common.py | 136 +++++++++++++++++++++++++- itou/www/apply/views/process_views.py | 24 +---- 3 files changed, 141 insertions(+), 24 deletions(-) diff --git a/itou/www/apply/views/batch_views.py b/itou/www/apply/views/batch_views.py index 4524ce1de9c..de920905fe6 100644 --- a/itou/www/apply/views/batch_views.py +++ b/itou/www/apply/views/batch_views.py @@ -16,6 +16,7 @@ from itou.utils.perms.company import get_current_company_or_404 from itou.utils.urls import get_safe_url from itou.www.apply.forms import BatchPostponeForm +from itou.www.apply.views.process_views import JobApplicationRefuseView logger = logging.getLogger(__name__) @@ -166,3 +167,7 @@ def transfer(request): ",".join(str(app_uid) for app_uid in transfered_ids), ) return HttpResponseRedirect(next_url) + + +class BatchRefuseView(JobApplicationRefuseView): + pass diff --git a/itou/www/apply/views/common.py b/itou/www/apply/views/common.py index a90c9141b64..d88ca544155 100644 --- a/itou/www/apply/views/common.py +++ b/itou/www/apply/views/common.py @@ -1,22 +1,27 @@ from django.conf import settings from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.db import transaction from django.forms import ValidationError from django.http import Http404, HttpResponseRedirect -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render +from django.template import loader from django.template.response import TemplateResponse from django.urls import reverse from django.utils import timezone from django.utils.safestring import mark_safe from django_htmx.http import HttpResponseClientRedirect from django_xworkflows import models as xwf_models +from formtools.wizard.views import NamedUrlSessionWizardView from itou.common_apps.address.forms import JobSeekerAddressForm from itou.companies.enums import CompanyKind, ContractType from itou.eligibility.models import EligibilityDiagnosis from itou.eligibility.models.geiq import GEIQEligibilityDiagnosis from itou.eligibility.utils import geiq_allowance_amount +from itou.job_applications import enums as job_applications_enums +from itou.job_applications.models import JobApplication, JobApplicationWorkflow from itou.users.enums import UserKind from itou.utils import constants as global_constants from itou.utils.htmx import hx_trigger_modal_control @@ -25,6 +30,9 @@ AcceptForm, CertifiedCriteriaInfoRequiredForm, CheckJobSeekerGEIQEligibilityForm, + JobApplicationRefusalJobSeekerAnswerForm, + JobApplicationRefusalPrescriberAnswerForm, + JobApplicationRefusalReasonForm, JobSeekerPersonalDataForm, ) from itou.www.eligibility_views.forms import AdministrativeCriteriaForm @@ -291,3 +299,129 @@ def _geiq_eligibility_criteria( context["job_seeker"] = job_seeker return render(request, template_name, context) + + +def _show_prescriber_answer_form(wizard): + return wizard.job_application.sender_kind == job_applications_enums.SenderKind.PRESCRIBER + + +class RefuseSessionWizardView(LoginRequiredMixin, NamedUrlSessionWizardView): + STEP_REASON = "reason" + STEP_JOB_SEEKER_ANSWER = "job-seeker-answer" + STEP_PRESCRIBER_ANSWER = "prescriber-answer" + + template_name = "apply/process_refuse.html" + form_list = [ + (STEP_REASON, JobApplicationRefusalReasonForm), + (STEP_JOB_SEEKER_ANSWER, JobApplicationRefusalJobSeekerAnswerForm), + (STEP_PRESCRIBER_ANSWER, JobApplicationRefusalPrescriberAnswerForm), + ] + condition_dict = { + STEP_PRESCRIBER_ANSWER: _show_prescriber_answer_form, + } + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + + if request.user.is_authenticated: + self.job_application = get_object_or_404( + JobApplication.objects.is_active_company_member(request.user).select_related("job_seeker"), + pk=kwargs["job_application_id"], + ) + + def check_wizard_state(self, *args, **kwargs): + # Redirect to job application details if the state is not refusable + if self.job_application.state not in JobApplicationWorkflow.CAN_BE_REFUSED_STATES: + message = "Action déjà effectuée." if self.job_application.state.is_refused else "Action impossible." + messages.error(self.request, message, extra_tags="toast") + return HttpResponseRedirect( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + + # Redirect to first step if form data is not retrieved in session (eg. direct url access) + if kwargs.get("step") in [ + self.STEP_JOB_SEEKER_ANSWER, + self.STEP_PRESCRIBER_ANSWER, + ] and not self.get_cleaned_data_for_step(self.STEP_REASON): + return HttpResponseRedirect(self.get_step_url(self.STEP_REASON)) + + def get(self, *args, **kwargs): + if check_response := self.check_wizard_state(*args, **kwargs): + return check_response + return super().get(*args, **kwargs) + + def post(self, *args, **kwargs): + if check_response := self.check_wizard_state(*args, **kwargs): + return check_response + return super().post(*args, **kwargs) + + def done(self, form_list, *args, **kwargs): + try: + # After each successful transition, a save() is performed by django-xworkflows. + cleaned_data = self.get_all_cleaned_data() + self.job_application.refusal_reason = cleaned_data["refusal_reason"] + self.job_application.refusal_reason_shared_with_job_seeker = cleaned_data[ + "refusal_reason_shared_with_job_seeker" + ] + self.job_application.answer = cleaned_data["job_seeker_answer"] + self.job_application.answer_to_prescriber = cleaned_data.get("prescriber_answer", "") + self.job_application.refuse(user=self.request.user) + messages.success( + self.request, + f"La candidature de {self.job_application.job_seeker.get_full_name()} a bien été déclinée.", + extra_tags="toast", + ) + except xwf_models.InvalidTransitionError: + messages.error(self.request, "Action déjà effectuée.", extra_tags="toast") + + next_url = reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + return HttpResponseRedirect(next_url) + + def get_prefix(self, request, *args, **kwargs): + """ + Ensure that each refuse session is bound to a job application. + Avoid session conflicts when using multiple tabs. + """ + return f"job_application_{self.job_application.pk}_refuse" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.steps.current != self.STEP_REASON: + cleaned_data = self.get_cleaned_data_for_step(self.STEP_REASON) + context["refusal_reason_label"] = job_applications_enums.RefusalReason( + cleaned_data["refusal_reason"] + ).label + context["refusal_reason_shared_with_job_seeker"] = cleaned_data["refusal_reason_shared_with_job_seeker"] + return context | { + "job_application": self.job_application, + "can_view_personal_information": True, # SIAE members have access to personal info + "matomo_custom_title": "Candidature refusée", + "matomo_event_name": f"refuse-application-{self.steps.current}-submit", + "primary_button_label": "Suivant" if context["wizard"]["steps"].next else "Confirmer le refus", + "secondary_button_label": "Précédent" if context["wizard"]["steps"].prev else "Annuler", + } + + def get_form_kwargs(self, step=None): + if step in (self.STEP_REASON, self.STEP_PRESCRIBER_ANSWER): + return { + "job_application": self.job_application, + } + return {} + + def get_form_initial(self, step): + initial_data = self.initial_dict.get(step, {}) + if step == self.STEP_JOB_SEEKER_ANSWER: + refusal_reason = self.get_cleaned_data_for_step(self.STEP_REASON).get("refusal_reason") + if refusal_reason: + initial_data["job_seeker_answer"] = loader.render_to_string( + f"apply/refusal_messages/{refusal_reason}.txt", + context={ + "job_application": self.job_application, + }, + request=self.request, + ) + + return initial_data + + def get_step_url(self, step): + return reverse(f"apply:{self.url_name}", kwargs={"job_application_id": self.job_application.pk, "step": step}) diff --git a/itou/www/apply/views/process_views.py b/itou/www/apply/views/process_views.py index 8464e21db18..315784536e2 100644 --- a/itou/www/apply/views/process_views.py +++ b/itou/www/apply/views/process_views.py @@ -19,7 +19,6 @@ from django.views.decorators.http import require_POST from django.views.generic.base import TemplateView from django_xworkflows import models as xwf_models -from formtools.wizard.views import NamedUrlSessionWizardView from itou.companies.enums import CompanyKind, ContractType from itou.companies.models import Company @@ -35,9 +34,6 @@ from itou.www.apply.forms import ( AcceptForm, AnswerForm, - JobApplicationRefusalJobSeekerAnswerForm, - JobApplicationRefusalPrescriberAnswerForm, - JobApplicationRefusalReasonForm, PriorActionForm, TransferJobApplicationForm, ) @@ -371,25 +367,7 @@ def process(request, job_application_id): return HttpResponseRedirect(next_url) -def _show_prescriber_answer_form(wizard): - return wizard.job_application.sender_kind == job_applications_enums.SenderKind.PRESCRIBER - - -class JobApplicationRefuseView(LoginRequiredMixin, NamedUrlSessionWizardView): - STEP_REASON = "reason" - STEP_JOB_SEEKER_ANSWER = "job-seeker-answer" - STEP_PRESCRIBER_ANSWER = "prescriber-answer" - - template_name = "apply/process_refuse.html" - form_list = [ - (STEP_REASON, JobApplicationRefusalReasonForm), - (STEP_JOB_SEEKER_ANSWER, JobApplicationRefusalJobSeekerAnswerForm), - (STEP_PRESCRIBER_ANSWER, JobApplicationRefusalPrescriberAnswerForm), - ] - condition_dict = { - STEP_PRESCRIBER_ANSWER: _show_prescriber_answer_form, - } - +class JobApplicationRefuseView(common_views.RefuseSessionWizardView): def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs)