Skip to content

Commit

Permalink
job_applications.models: Stop calling .full_clean() on each save
Browse files Browse the repository at this point in the history
Regexp used to find usages: `job(_app(lication)?)?\.save\(`
  • Loading branch information
rsebille committed Aug 8, 2024
1 parent 66892a4 commit 97df937
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 97 deletions.
4 changes: 0 additions & 4 deletions itou/job_applications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,10 +799,6 @@ def clean(self):
if self.inverted_vae_contract is not None:
raise ValidationError("Un contrat associé à une VAE inversée n'est possible que pour les GEIQ")

def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)

@property
def is_pending(self):
return self.state in JobApplicationWorkflow.PENDING_STATES
Expand Down
4 changes: 2 additions & 2 deletions itou/www/apply/views/process_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ def archive(request, job_application_id):
siae_name = job_application.to_company.display_name

job_application.hidden_for_company = True
job_application.save()
job_application.save(update_fields={"hidden_for_company"})

success_message = f"La candidature de {username} chez {siae_name} a bien été supprimée."
messages.success(request, success_message, extra_tags="toast")
Expand Down Expand Up @@ -736,7 +736,7 @@ def form_valid(self):
self.job_application.external_transfer(target_company=self.company, user=self.request.user)
if self.form.cleaned_data.get("keep_original_resume"):
new_job_application.resume_link = self.job_application.resume_link
new_job_application.save()
new_job_application.save(update_fields={"resume_link"})
return new_job_application

def get_next_url(self, job_application):
Expand Down
1 change: 1 addition & 0 deletions itou/www/approvals_views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,7 @@ def pe_approval_create(request, pe_approval_id):

# Link both and save the application
job_application.approval = approval_from_pe
job_application.full_clean() # Manual call because we don't use a form
job_application.save()

messages.success(
Expand Down
2 changes: 1 addition & 1 deletion tests/approvals/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ def test_employee_record_status(self, subtests):
assert msg == "Non proposé à la création"

# When hiring start date is before employee record availability date
job_application = JobApplicationFactory(hiring_start_at="2021-09-26")
job_application = JobApplicationFactory(hiring_start_at=datetime.date(2021, 9, 26))
msg = inline.employee_record_status(job_application)
assert msg == "Date de début du contrat avant l'interopérabilité"

Expand Down
6 changes: 1 addition & 5 deletions tests/job_applications/test_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,12 @@ def test_model_fields():
ContentType.objects.get_for_model(target_company)
with assertNumQueries(
2 # Check user is in both origin and dest siae
+ 1 # Check if approvals are linked to diagnosis because of on_delete=set_null
+ 1 # Check if job applications are linked because of on_delete=set_null
+ 2 # Delete diagnosis and criteria made by the SIAE
+ 4 # Delete (+ SET_NULL) diagnosis and criteria made by the SIAE
+ 1 # Select user for email
+ 1 # Select employer notification settings
+ 1 # Insert employer email in emails table
+ 1 # Select job seeker notification settings
+ 1 # Insert job seeker email in emails table
+ 6 # Caused by `full_clean()` : `clean_fields()`
+ 4 # Integrity constraints check (full clean)
+ 1 # Update job application
+ 1 # Add job application transition log
):
Expand Down
156 changes: 71 additions & 85 deletions tests/job_applications/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
from itou.employee_record.enums import Status
from itou.job_applications.admin_forms import JobApplicationAdminForm
from itou.job_applications.enums import (
GEIQ_MAX_HOURS_PER_WEEK,
GEIQ_MIN_HOURS_PER_WEEK,
JobApplicationState,
Origin,
QualificationLevel,
Expand Down Expand Up @@ -180,118 +178,106 @@ def test_get_sender_kind_display(self):
with self.subTest(sender_kind_display):
assert job_application.get_sender_kind_display() == sender_kind_display

def test_geiq_fields_validation(self):
# Full clean
def test_application_on_non_job_seeker(self):
with self.assertRaisesRegex(
ValidationError, "Le nombre d'heures par semaine ne peut être saisi que pour un GEIQ"
ValidationError,
"Impossible de candidater pour cet utilisateur, celui-ci n'est pas un compte candidat",
):
JobApplicationFactory(to_company__kind=CompanyKind.EI, nb_hours_per_week=20)
JobApplicationFactory(job_seeker=PrescriberFactory()).clean()

def test_inverted_vae_contract(self):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, inverted_vae_contract=True).clean()
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, inverted_vae_contract=False).clean()
JobApplicationFactory(to_company__kind=CompanyKind.EI, inverted_vae_contract=None).clean()
with self.assertRaisesRegex(
ValidationError, "Les précisions sur le type de contrat ne peuvent être saisies que pour un GEIQ"
ValidationError, "Un contrat associé à une VAE inversée n'est possible que pour les GEIQ"
):
JobApplicationFactory(to_company__kind=CompanyKind.EI, contract_type_details="foo")

with self.assertRaisesRegex(ValidationError, "Le type de contrat ne peut être saisi que pour un GEIQ"):
JobApplicationFactory(to_company__kind=CompanyKind.EI, contract_type=ContractType.OTHER)

# Constraints
with self.assertRaisesRegex(ValidationError, "Incohérence dans les champs concernant le contrat GEIQ"):
JobApplicationFactory(
to_company__kind=CompanyKind.GEIQ,
contract_type=ContractType.PROFESSIONAL_TRAINING,
contract_type_details="foo",
)

with self.assertRaisesRegex(ValidationError, "Incohérence dans les champs concernant le contrat GEIQ"):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, nb_hours_per_week=1)
JobApplicationFactory(to_company__kind=CompanyKind.AI, inverted_vae_contract=True).clean()

with self.assertRaisesRegex(ValidationError, "Incohérence dans les champs concernant le contrat GEIQ"):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, contract_type=ContractType.OTHER)

with self.assertRaisesRegex(ValidationError, "Incohérence dans les champs concernant le contrat GEIQ"):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, contract_type_details="foo")

with self.assertRaisesRegex(ValidationError, "Incohérence dans les champs concernant le contrat GEIQ"):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, contract_type_details="foo", nb_hours_per_week=1)
def test_can_be_cancelled():
assert JobApplicationFactory().can_be_cancelled is True

with self.assertRaisesRegex(ValidationError, "Incohérence dans les champs concernant le contrat GEIQ"):
JobApplicationFactory(
to_company__kind=CompanyKind.GEIQ, contract_type=ContractType.OTHER, nb_hours_per_week=1
)

# Mind the parens in RE...
with self.assertRaisesRegex(
ValidationError, "Une candidature ne peut avoir les deux types de diagnostics \\(IAE et GEIQ\\)"
):
JobApplicationFactory(
with_geiq_eligibility_diagnosis=True,
eligibility_diagnosis=IAEEligibilityDiagnosisFactory(from_prescriber=True),
)
def test_can_be_cancelled_when_origin_is_ai_stock():
assert JobApplicationFactory(origin=Origin.AI_STOCK).can_be_cancelled is False

# Validators
with self.assertRaisesRegex(
ValidationError,
f"Assurez-vous que cette valeur est supérieure ou égale à {GEIQ_MIN_HOURS_PER_WEEK}.",
):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, nb_hours_per_week=0)

with self.assertRaisesRegex(
ValidationError,
f"Assurez-vous que cette valeur est inférieure ou égale à {GEIQ_MAX_HOURS_PER_WEEK}.",
):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, nb_hours_per_week=49)
def test_diagnoses_coherence_contraint():
job_application = JobApplicationFactory(with_geiq_eligibility_diagnosis=True)
job_application.eligibility_diagnosis = IAEEligibilityDiagnosisFactory(from_prescriber=True)

# Should pass: normal cases
JobApplicationFactory()
# Mind the parens in RE...
with pytest.raises(
ValidationError, match="Une candidature ne peut avoir les deux types de diagnostics \\(IAE et GEIQ\\)"
):
job_application.validate_constraints()

for contract_type in [ContractType.APPRENTICESHIP, ContractType.PROFESSIONAL_TRAINING]:
with self.subTest(contract_type):
JobApplicationFactory(
to_company__kind=CompanyKind.GEIQ, contract_type=contract_type, nb_hours_per_week=35
)

JobApplicationFactory(
to_company__kind=CompanyKind.GEIQ,
contract_type=ContractType.OTHER,
nb_hours_per_week=30,
contract_type_details="foo",
)
@pytest.mark.parametrize(
"data,error",
[
({"nb_hours_per_week": 20}, "Le nombre d'heures par semaine ne peut être saisi que pour un GEIQ"),
(
{"contract_type_details": "foo"},
"Les précisions sur le type de contrat ne peuvent être saisies que pour un GEIQ",
),
({"contract_type": ContractType.OTHER}, "Le type de contrat ne peut être saisi que pour un GEIQ"),
],
ids=repr,
)
def test_geiq_fields_validation_error(data, error):
job_application = JobApplicationFactory(to_company__kind=CompanyKind.EI)
for name, value in data.items():
setattr(job_application, name, value)

def test_application_on_non_job_seeker(self):
with self.assertRaisesRegex(
ValidationError,
"Impossible de candidater pour cet utilisateur, celui-ci n'est pas un compte candidat",
):
JobApplicationFactory(job_seeker=PrescriberFactory())
with pytest.raises(ValidationError, match=error):
job_application.clean()

def test_inverted_vae_contract(self):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, inverted_vae_contract=True)
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, inverted_vae_contract=False)
JobApplicationFactory(to_company__kind=CompanyKind.EI, inverted_vae_contract=None)
with self.assertRaisesRegex(
ValidationError, "Un contrat associé à une VAE inversée n'est possible que pour les GEIQ"
):
JobApplicationFactory(to_company__kind=CompanyKind.AI, inverted_vae_contract=True)

@pytest.mark.parametrize(
"data",
[
{"contract_type": ContractType.APPRENTICESHIP, "nb_hours_per_week": 35},
{"contract_type": ContractType.PROFESSIONAL_TRAINING, "nb_hours_per_week": 35},
{"contract_type": ContractType.OTHER, "nb_hours_per_week": 30, "contract_type_details": "foo"},
],
ids=repr,
)
def test_geiq_fields_validation_success(data):
JobApplicationFactory(to_company__kind=CompanyKind.GEIQ, **data)

def test_can_be_cancelled():
assert JobApplicationFactory().can_be_cancelled is True

@pytest.mark.parametrize(
"data",
[
{"contract_type": ContractType.PROFESSIONAL_TRAINING, "contract_type_details": "foo"},
{"contract_type": ContractType.OTHER},
{"contract_type_details": "foo"},
{"nb_hours_per_week": 1},
{"nb_hours_per_week": 1, "contract_type_details": "foo"},
{"nb_hours_per_week": 1, "contract_type": ContractType.OTHER},
],
ids=repr,
)
def test_geiq_contract_fields_contraint(data):
job_application = JobApplicationFactory(to_company__kind=CompanyKind.GEIQ)
for name, value in data.items():
setattr(job_application, name, value)

def test_can_be_cancelled_when_origin_is_ai_stock():
assert JobApplicationFactory(origin=Origin.AI_STOCK).can_be_cancelled is False
with pytest.raises(ValidationError, match="Incohérence dans les champs concernant le contrat GEIQ"):
job_application.validate_constraints()


def test_geiq_qualification_fields_contraint():
with pytest.raises(
Exception, match="Incohérence dans les champs concernant la qualification pour le contrat GEIQ"
):
JobApplicationFactory(
JobApplicationFactory.build(
to_company__kind=CompanyKind.GEIQ,
qualification_type=QualificationType.STATE_DIPLOMA,
qualification_level=QualificationLevel.NOT_RELEVANT,
)
).validate_constraints()

for qualification_type in [QualificationType.CQP, QualificationType.CCN]:
JobApplicationFactory(
Expand Down

0 comments on commit 97df937

Please sign in to comment.