Skip to content

Commit

Permalink
job_applications: Add reset transition for admin actions
Browse files Browse the repository at this point in the history
  • Loading branch information
tonial committed Jun 20, 2024
1 parent 82ed9ea commit f238541
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 135 deletions.
41 changes: 36 additions & 5 deletions itou/job_applications/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import uuid

import xworkflows
from django.contrib import admin, messages
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.safestring import mark_safe

Expand Down Expand Up @@ -101,6 +103,7 @@ class JobApplicationAdmin(InconsistencyCheckMixin, ItouModelAdmin):
"transferred_at",
"transferred_from",
"origin",
"state",
)
inlines = (JobsInline, PriorActionInline, TransitionLogInline, UUIDSupportRemarkInline)

Expand Down Expand Up @@ -269,11 +272,6 @@ def save_model(self, request, obj, form, change):
obj.origin = Origin.ADMIN

super().save_model(request, obj, form, change)
if form._job_application_to_accept:
if obj.state.is_new:
# The new -> accepted transition doesn't exist
obj.process(user=request.user)
obj.accept(user=request.user)

def get_form(self, request, obj=None, **kwargs):
"""
Expand All @@ -287,6 +285,39 @@ def get_form(self, request, obj=None, **kwargs):
kwargs.update({"help_texts": help_texts})
return super().get_form(request, obj, **kwargs)

def transition_error(self, request, error):
message = None
if error.args[0] == "Cannot create an approval without eligibility diagnosis here":
message = (
"Un diagnostic d'éligibilité valide pour ce candidat "
"et cette SIAE est obligatoire pour pouvoir créer un PASS IAE."
)
elif error.args[0] == "Cannot accept a job application with no hiring start date.":
message = "Le champ 'Date de début du contrat' est obligatoire pour accepter une candidature"
self.message_user(request, message or error, messages.ERROR)
return HttpResponseRedirect(request.get_full_path())

def response_change(self, request, obj):
"""
Override to add custom "actions" in `self.change_form_template` for:
* processing the job application
* accepting the job application
* refusing the job application
* reseting the job applciation
"""
for transition in ["accept", "cancel", "reset", "process"]:
if f"transition_{transition}" in request.POST:
try:
getattr(obj, transition)(user=request.user)
# Stay on same page
updated_request = request.POST.copy()
updated_request.update({"_continue": ["please"]})
request.POST = updated_request
except xworkflows.AbortTransition as e:
return self.transition_error(request, e)

return super().response_change(request, obj)


@admin.register(models.JobApplicationTransitionLog)
class JobApplicationTransitionLogAdmin(ItouModelAdmin):
Expand Down
28 changes: 1 addition & 27 deletions itou/job_applications/admin_forms.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from django import forms
from django.core.exceptions import ValidationError

from itou.eligibility.models import EligibilityDiagnosis
from itou.job_applications.enums import JobApplicationState, SenderKind
from itou.job_applications.enums import SenderKind
from itou.job_applications.models import JobApplication


Expand All @@ -24,31 +23,6 @@ def __init__(self, *args, **kwargs):
self._job_application_to_accept = False

def clean(self):
target_state = self.cleaned_data.get("state")
if target_state == JobApplicationState.ACCEPTED and target_state != self._initial_job_application_state:
self._job_application_to_accept = True
self.cleaned_data["state"] = self._initial_job_application_state or JobApplicationState.NEW

if self._job_application_to_accept and not self.cleaned_data.get("hiring_start_at"):
self.add_error("hiring_start_at", "Ce champ est obligatoire pour les candidatures acceptées.")

if (
self._job_application_to_accept
and (to_company := self.cleaned_data.get("to_company"))
and to_company.is_subject_to_eligibility_rules
and self.cleaned_data.get("hiring_without_approval") is not True
and (job_seeker := self.cleaned_data.get("job_seeker"))
and not job_seeker.has_valid_common_approval
and not EligibilityDiagnosis.objects.last_considered_valid(job_seeker, for_siae=to_company)
):
self.add_error(
None,
(
"Un diagnostic d'éligibilité valide pour ce candidat "
"et cette SIAE est obligatoire pour pouvoir créer un PASS IAE."
),
)

sender = self.cleaned_data["sender"]
sender_kind = self.cleaned_data["sender_kind"]
sender_company = self.cleaned_data.get("sender_company")
Expand Down
11 changes: 11 additions & 0 deletions itou/job_applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class JobApplicationWorkflow(xwf_models.Workflow):
TRANSITION_CANCEL = "cancel"
TRANSITION_RENDER_OBSOLETE = "render_obsolete"
TRANSITION_TRANSFER = "transfer"
TRANSITION_RESET = "reset"

TRANSITION_CHOICES = (
(TRANSITION_PROCESS, "Étudier la candidature"),
Expand All @@ -68,6 +69,7 @@ class JobApplicationWorkflow(xwf_models.Workflow):
(TRANSITION_CANCEL, "Annuler la candidature"),
(TRANSITION_RENDER_OBSOLETE, "Rendre obsolete la candidature"),
(TRANSITION_TRANSFER, "Transfert de la candidature vers une autre SIAE"),
(TRANSITION_RESET, "Réinitialiser la candidature"),
)

CAN_BE_ACCEPTED_STATES = [
Expand Down Expand Up @@ -111,6 +113,7 @@ class JobApplicationWorkflow(xwf_models.Workflow):
JobApplicationState.OBSOLETE,
),
(TRANSITION_TRANSFER, CAN_BE_TRANSFERRED_STATES, JobApplicationState.NEW),
(TRANSITION_RESET, JobApplicationState.OBSOLETE, JobApplicationState.NEW),
)

PENDING_STATES = [JobApplicationState.NEW, JobApplicationState.PROCESSING, JobApplicationState.POSTPONED]
Expand Down Expand Up @@ -883,6 +886,14 @@ def get_sender_kind_display(self):
def is_in_transferable_state(self):
return self.state in JobApplicationWorkflow.CAN_BE_TRANSFERRED_STATES

@property
def is_in_acceptable_state(self):
return self.state in JobApplicationWorkflow.CAN_BE_ACCEPTED_STATES

@property
def is_in_refusable_state(self):
return self.state in JobApplicationWorkflow.CAN_BE_REFUSED_STATES

def can_be_transferred(self, user, target_company):
# User must be member of both origin and target companies to make a transfer
if not (self.to_company.has_member(user) and target_company.has_member(user)):
Expand Down
33 changes: 33 additions & 0 deletions itou/templates/admin/job_applications/change_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% extends 'admin/change_form.html' %}

{% block submit_buttons_top %}

{{ block.super }}

<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>
{% if original.state.is_new %}
<input type='submit' class="danger js-with-confirm" name="transition_process" value="Passer à l'étude">
{% endif %}
{% if original.is_in_acceptable_state %}
<input type='submit' class="danger js-with-confirm" name="transition_accept" value="Accepter">
{% endif %}
{% if original.state.is_accepted and original.can_be_cancelled %}
<input type='submit' class="danger js-with-confirm" name="transition_cancel" value="Annuler">
{% endif %}
{% if original.state.is_obsolete %}
<input type='submit' class="danger js-with-confirm" name="transition_reset" value="Remettre au statut nouveau">
{% endif %}
</div>

<script nonce="{{ CSP_NONCE }}">
var buttons = document.getElementsByClassName("js-with-confirm");
Array.prototype.forEach.call(buttons, function(button) {
button.addEventListener("click", function(event) {
if (!confirm('Êtes vous certain de vouloir **' + event.target.value.toUpperCase() + '** la candidature ?')) {
event.preventDefault();
}
});
});
</script>
{% endblock %}
107 changes: 107 additions & 0 deletions tests/job_applications/__snapshots__/test_admin.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# serializer version: 1
# name: test_available_transitions[accepted]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>



<input class="danger js-with-confirm" name="transition_cancel" type="submit" value="Annuler"/>


</div>
'''
# ---
# name: test_available_transitions[cancelled]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>


<input class="danger js-with-confirm" name="transition_accept" type="submit" value="Accepter"/>



</div>
'''
# ---
# name: test_available_transitions[new]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>

<input class="danger js-with-confirm" name="transition_process" type="submit" value="Passer à l'étude"/>




</div>
'''
# ---
# name: test_available_transitions[obsolete]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>


<input class="danger js-with-confirm" name="transition_accept" type="submit" value="Accepter"/>



<input class="danger js-with-confirm" name="transition_reset" type="submit" value="Remettre au statut nouveau"/>

</div>
'''
# ---
# name: test_available_transitions[postponed]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>


<input class="danger js-with-confirm" name="transition_accept" type="submit" value="Accepter"/>



</div>
'''
# ---
# name: test_available_transitions[prior_to_hire]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>


<input class="danger js-with-confirm" name="transition_accept" type="submit" value="Accepter"/>



</div>
'''
# ---
# name: test_available_transitions[processing]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>


<input class="danger js-with-confirm" name="transition_accept" type="submit" value="Accepter"/>



</div>
'''
# ---
# name: test_available_transitions[refused]
'''
<div class="submit-row" id="job-app-transitions">
<p>Changer l'état de la candidature :</p>


<input class="danger js-with-confirm" name="transition_accept" type="submit" value="Accepter"/>



</div>
'''
# ---
Loading

0 comments on commit f238541

Please sign in to comment.