From 189e0a0b198196e66a498fcb9784df898bded01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20S=C3=A9bille?= Date: Thu, 5 Oct 2023 12:20:34 +0200 Subject: [PATCH] models: Use more sensitive `on_delete` clauses for foreign keys --- ...0004_alter_approval_created_by_and_more.py | 229 ++++++++++++++++++ itou/approvals/models.py | 27 ++- itou/common_apps/address/models.py | 2 +- itou/common_apps/organizations/models.py | 2 +- ...ed_by_alter_company_insee_city_and_more.py | 75 ++++++ itou/companies/models.py | 17 +- ...inistrativecriteria_created_by_and_more.py | 128 ++++++++++ itou/eligibility/models/common.py | 10 +- itou/eligibility/models/geiq.py | 6 +- itou/eligibility/models/iae.py | 10 +- ...entationassessment_reviewed_by_and_more.py | 39 +++ itou/geiq/models.py | 4 +- ...6_alter_institution_insee_city_and_more.py | 34 +++ itou/institutions/models.py | 2 +- ..._alter_jobapplication_approval_and_more.py | 161 ++++++++++++ itou/job_applications/models.py | 42 ++-- ...rescribermembership_updated_by_and_more.py | 58 +++++ itou/prescribers/models.py | 6 +- ...iteria_administrative_criteria_and_more.py | 78 ++++++ itou/siae_evaluations/models.py | 12 +- ...r_user_created_by_alter_user_insee_city.py | 33 +++ itou/users/models.py | 2 +- tests/job_applications/test_transfer.py | 1 + tests/www/apply/test_process.py | 20 +- .../test_prolongation_requests.py | 11 +- 25 files changed, 929 insertions(+), 80 deletions(-) create mode 100644 itou/approvals/migrations/0004_alter_approval_created_by_and_more.py create mode 100644 itou/companies/migrations/0006_alter_company_created_by_alter_company_insee_city_and_more.py create mode 100644 itou/eligibility/migrations/0005_alter_administrativecriteria_created_by_and_more.py create mode 100644 itou/geiq/migrations/0002_alter_implementationassessment_reviewed_by_and_more.py create mode 100644 itou/institutions/migrations/0006_alter_institution_insee_city_and_more.py create mode 100644 itou/job_applications/migrations/0008_alter_jobapplication_approval_and_more.py create mode 100644 itou/prescribers/migrations/0005_alter_prescribermembership_updated_by_and_more.py create mode 100644 itou/siae_evaluations/migrations/0002_alter_evaluatedadministrativecriteria_administrative_criteria_and_more.py create mode 100644 itou/users/migrations/0009_alter_user_created_by_alter_user_insee_city.py diff --git a/itou/approvals/migrations/0004_alter_approval_created_by_and_more.py b/itou/approvals/migrations/0004_alter_approval_created_by_and_more.py new file mode 100644 index 0000000000..6835c67032 --- /dev/null +++ b/itou/approvals/migrations/0004_alter_approval_created_by_and_more.py @@ -0,0 +1,229 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("approvals", "0003_alter_approval_updated_at"), + ("companies", "0005_company_rdv_insertion_id"), + ("eligibility", "0004_geiqadministrativecriteria_certifiable"), + ("prescribers", "0004_poleemploi_to_francetravail"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="approval", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="approval", + name="eligibility_diagnosis", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="eligibility.eligibilitydiagnosis", + verbose_name="diagnostic d'éligibilité", + ), + ), + migrations.AlterField( + model_name="approval", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="approvals", + to=settings.AUTH_USER_MODEL, + verbose_name="demandeur d'emploi", + ), + ), + migrations.AlterField( + model_name="prolongation", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_created", + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="prolongation", + name="declared_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_declared", + to=settings.AUTH_USER_MODEL, + verbose_name="déclarée par", + ), + ), + migrations.AlterField( + model_name="prolongation", + name="declared_by_siae", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="companies.company", + verbose_name="SIAE du déclarant", + ), + ), + migrations.AlterField( + model_name="prolongation", + name="prescriber_organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="prescribers.prescriberorganization", + verbose_name="organisation du prescripteur habilité", + ), + ), + migrations.AlterField( + model_name="prolongation", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_updated", + to=settings.AUTH_USER_MODEL, + verbose_name="modifié par", + ), + ), + migrations.AlterField( + model_name="prolongation", + name="validated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_validated", + to=settings.AUTH_USER_MODEL, + verbose_name="prescripteur habilité qui a autorisé cette prolongation", + ), + ), + migrations.AlterField( + model_name="prolongationrequest", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_created", + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="prolongationrequest", + name="declared_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_declared", + to=settings.AUTH_USER_MODEL, + verbose_name="déclarée par", + ), + ), + migrations.AlterField( + model_name="prolongationrequest", + name="declared_by_siae", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="companies.company", + verbose_name="SIAE du déclarant", + ), + ), + migrations.AlterField( + model_name="prolongationrequest", + name="prescriber_organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="prescribers.prescriberorganization", + verbose_name="organisation du prescripteur habilité", + ), + ), + migrations.AlterField( + model_name="prolongationrequest", + name="processed_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)s_processed", + to=settings.AUTH_USER_MODEL, + verbose_name="traité par", + ), + ), + migrations.AlterField( + model_name="prolongationrequest", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_updated", + to=settings.AUTH_USER_MODEL, + verbose_name="modifié par", + ), + ), + migrations.AlterField( + model_name="prolongationrequest", + name="validated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="%(class)ss_validated", + to=settings.AUTH_USER_MODEL, + verbose_name="prescripteur habilité qui a autorisé cette prolongation", + ), + ), + migrations.AlterField( + model_name="suspension", + name="created_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="approvals_suspended_set", + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="suspension", + name="siae", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="approvals_suspended", + to="companies.company", + verbose_name="SIAE", + ), + ), + migrations.AlterField( + model_name="suspension", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to=settings.AUTH_USER_MODEL, + verbose_name="mis à jour par", + ), + ), + ] diff --git a/itou/approvals/models.py b/itou/approvals/models.py index e0f2cc915b..a7a8a34469 100644 --- a/itou/approvals/models.py +++ b/itou/approvals/models.py @@ -513,7 +513,7 @@ class Approval(PENotificationMixin, CommonApprovalMixin): user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="demandeur d'emploi", - on_delete=models.CASCADE, + on_delete=models.PROTECT, # 2-step deletion, first the Approval to create a CancelledApproval then the User related_name="approvals", ) created_by = models.ForeignKey( @@ -521,7 +521,7 @@ class Approval(PENotificationMixin, CommonApprovalMixin): verbose_name="créé par", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability ) origin = models.CharField( verbose_name="origine du pass", @@ -536,7 +536,7 @@ class Approval(PENotificationMixin, CommonApprovalMixin): verbose_name="diagnostic d'éligibilité", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # To not mess with the `approval_eligibility_diagnosis` constraint ) updated_at = models.DateTimeField(verbose_name="date de modification", auto_now=True) @@ -674,6 +674,7 @@ def delete(self, *args, **kwargs): origin_sender_kind=sender_kind, origin_prescriber_organization_kind=prescriber_organization_kind, ).save() + self.jobapplication_set.update(approval=None) super().delete() def clean(self): @@ -1104,7 +1105,7 @@ def displayed_choices_for_siae(siae): "companies.Company", verbose_name="SIAE", null=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # Prevent a soft lock, also for traceability and accountability related_name="approvals_suspended", ) reason = models.CharField( @@ -1119,7 +1120,7 @@ def displayed_choices_for_siae(siae): settings.AUTH_USER_MODEL, verbose_name="créé par", null=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability related_name="approvals_suspended_set", ) updated_at = models.DateTimeField(verbose_name="date de modification", auto_now=True) @@ -1128,7 +1129,7 @@ def displayed_choices_for_siae(siae): verbose_name="mis à jour par", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability, the dates can be edited ) objects = SuspensionQuerySet.as_manager() @@ -1404,14 +1405,14 @@ class CommonProlongation(models.Model): settings.AUTH_USER_MODEL, verbose_name="déclarée par", null=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability related_name="%(class)ss_declared", ) declared_by_siae = models.ForeignKey( "companies.Company", verbose_name="SIAE du déclarant", null=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability, people's organization can change ) # It is assumed that an authorized prescriber has validated the prolongation beforehand. @@ -1420,7 +1421,7 @@ class CommonProlongation(models.Model): verbose_name="prescripteur habilité qui a autorisé cette prolongation", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability related_name="%(class)ss_validated", ) @@ -1429,7 +1430,7 @@ class CommonProlongation(models.Model): verbose_name="organisation du prescripteur habilité", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability, people's organization can change ) # `created_by` can be different from `declared_by` when created in admin. @@ -1438,7 +1439,7 @@ class CommonProlongation(models.Model): settings.AUTH_USER_MODEL, verbose_name="créé par", null=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability related_name="%(class)ss_created", ) updated_at = models.DateTimeField(verbose_name="date de modification", auto_now=True) @@ -1447,7 +1448,7 @@ class CommonProlongation(models.Model): verbose_name="modifié par", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability related_name="%(class)ss_updated", ) @@ -1576,7 +1577,7 @@ class ProlongationRequest(CommonProlongation): processed_by = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="traité par", - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability related_name="%(class)s_processed", null=True, blank=True, diff --git a/itou/common_apps/address/models.py b/itou/common_apps/address/models.py index adce5ee5c4..b86ff69cfd 100644 --- a/itou/common_apps/address/models.py +++ b/itou/common_apps/address/models.py @@ -175,7 +175,7 @@ class AddressMixin(models.Model): null=True, ) - insee_city = models.ForeignKey("cities.City", null=True, blank=True, on_delete=models.SET_NULL) + insee_city = models.ForeignKey("cities.City", null=True, blank=True, on_delete=models.RESTRICT) class Meta: abstract = True diff --git a/itou/common_apps/organizations/models.py b/itou/common_apps/organizations/models.py index dc24022431..1e034f6f67 100644 --- a/itou/common_apps/organizations/models.py +++ b/itou/common_apps/organizations/models.py @@ -214,7 +214,7 @@ class MembershipAbstract(models.Model): settings.AUTH_USER_MODEL, related_name="updated_membershipmodel_set", null=True, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability verbose_name="mis à jour par", ) diff --git a/itou/companies/migrations/0006_alter_company_created_by_alter_company_insee_city_and_more.py b/itou/companies/migrations/0006_alter_company_created_by_alter_company_insee_city_and_more.py new file mode 100644 index 0000000000..1d1b62d1f3 --- /dev/null +++ b/itou/companies/migrations/0006_alter_company_created_by_alter_company_insee_city_and_more.py @@ -0,0 +1,75 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("cities", "0001_initial"), + ("companies", "0005_company_rdv_insertion_id"), + ("jobs", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="company", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="created_company_set", + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="company", + name="insee_city", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to="cities.city" + ), + ), + migrations.AlterField( + model_name="companymembership", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="updated_companymembership_set", + to=settings.AUTH_USER_MODEL, + verbose_name="mis à jour par", + ), + ), + migrations.AlterField( + model_name="jobdescription", + name="appellation", + field=models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to="jobs.appellation"), + ), + migrations.AlterField( + model_name="jobdescription", + name="location", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="cities.city", + verbose_name="localisation du poste", + ), + ), + migrations.AlterField( + model_name="siaeconvention", + name="reactivated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="reactivated_siae_convention_set", + to=settings.AUTH_USER_MODEL, + verbose_name="réactivée manuellement par", + ), + ), + ] diff --git a/itou/companies/models.py b/itou/companies/models.py index 7001a25054..ad18617c82 100644 --- a/itou/companies/models.py +++ b/itou/companies/models.py @@ -247,7 +247,7 @@ class Company(AddressMixin, OrganizationAbstract): related_name="created_company_set", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability ) # Ability to block new job applications @@ -256,10 +256,9 @@ class Company(AddressMixin, OrganizationAbstract): verbose_name="date du dernier blocage de candidatures", blank=True, null=True ) - # A convention can only be deleted if it is no longer linked to any siae. convention = models.ForeignKey( "SiaeConvention", - on_delete=models.RESTRICT, + on_delete=models.RESTRICT, # A convention should only be deleted if it is no longer linked to any siae. blank=True, null=True, related_name="siaes", @@ -507,7 +506,7 @@ class CompanyMembership(MembershipAbstract): settings.AUTH_USER_MODEL, related_name="updated_companymembership_set", null=True, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability verbose_name="mis à jour par", ) notifications = models.JSONField(verbose_name="notifications", default=dict, blank=True) @@ -584,7 +583,7 @@ class JobDescription(models.Model): # Max number or workable hours per week in France (Code du Travail) MAX_WORKED_HOURS_PER_WEEK = 48 - appellation = models.ForeignKey("jobs.Appellation", on_delete=models.CASCADE) + appellation = models.ForeignKey("jobs.Appellation", on_delete=models.RESTRICT) company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="job_description_through") created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now) updated_at = models.DateTimeField(verbose_name="date de modification", auto_now=True, db_index=True) @@ -602,7 +601,7 @@ class JobDescription(models.Model): ) location = models.ForeignKey( "cities.City", - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, null=True, blank=True, verbose_name="localisation du poste", @@ -829,7 +828,7 @@ class SiaeConvention(models.Model): related_name="reactivated_siae_convention_set", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # Only staff can update it, and we shouldn't delete one of those accounts ) reactivated_at = models.DateTimeField(verbose_name="date de réactivation manuelle", blank=True, null=True) @@ -923,11 +922,9 @@ class SiaeFinancialAnnex(models.Model): created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now) updated_at = models.DateTimeField(verbose_name="date de modification", auto_now=True) - # A financial annex cannot exist without a convention, and - # deleting a convention will delete all its financial annexes. convention = models.ForeignKey( "SiaeConvention", - on_delete=models.CASCADE, + on_delete=models.CASCADE, # A financial annex cannot exist without a convention related_name="financial_annexes", ) diff --git a/itou/eligibility/migrations/0005_alter_administrativecriteria_created_by_and_more.py b/itou/eligibility/migrations/0005_alter_administrativecriteria_created_by_and_more.py new file mode 100644 index 0000000000..f3f078dd08 --- /dev/null +++ b/itou/eligibility/migrations/0005_alter_administrativecriteria_created_by_and_more.py @@ -0,0 +1,128 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("companies", "0006_alter_company_created_by_alter_company_insee_city_and_more"), + ("eligibility", "0004_geiqadministrativecriteria_certifiable"), + ("prescribers", "0004_poleemploi_to_francetravail"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="administrativecriteria", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="eligibilitydiagnosis", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, to=settings.AUTH_USER_MODEL, verbose_name="auteur" + ), + ), + migrations.AlterField( + model_name="eligibilitydiagnosis", + name="author_prescriber_organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="prescribers.prescriberorganization", + verbose_name="organisation du prescripteur de l'auteur", + ), + ), + migrations.AlterField( + model_name="eligibilitydiagnosis", + name="author_siae", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="companies.company", + verbose_name="SIAE de l'auteur", + ), + ), + migrations.AlterField( + model_name="geiqadministrativecriteria", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="geiqadministrativecriteria", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="eligibility.geiqadministrativecriteria", + verbose_name="critère parent", + ), + ), + migrations.AlterField( + model_name="geiqeligibilitydiagnosis", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, to=settings.AUTH_USER_MODEL, verbose_name="auteur" + ), + ), + migrations.AlterField( + model_name="geiqeligibilitydiagnosis", + name="author_geiq", + field=models.ForeignKey( + blank=True, + limit_choices_to={"kind": "GEIQ"}, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="geiq_eligibilitydiagnosis_set", + to="companies.company", + verbose_name="GEIQ de l'auteur", + ), + ), + migrations.AlterField( + model_name="geiqeligibilitydiagnosis", + name="author_prescriber_organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="prescribers.prescriberorganization", + verbose_name="organisation du prescripteur de l'auteur", + ), + ), + migrations.AlterField( + model_name="geiqselectedadministrativecriteria", + name="administrative_criteria", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="administrative_criteria_through", + to="eligibility.geiqadministrativecriteria", + ), + ), + migrations.AlterField( + model_name="selectedadministrativecriteria", + name="administrative_criteria", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="administrative_criteria_through", + to="eligibility.administrativecriteria", + ), + ), + ] diff --git a/itou/eligibility/models/common.py b/itou/eligibility/models/common.py index 4354596a51..82d14c148e 100644 --- a/itou/eligibility/models/common.py +++ b/itou/eligibility/models/common.py @@ -34,7 +34,7 @@ class AbstractEligibilityDiagnosisModel(models.Model): author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="auteur", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability # removed clashing on former unused related_name `eligibility_diagnoses_made` ) author_kind = models.CharField( @@ -49,7 +49,7 @@ class AbstractEligibilityDiagnosisModel(models.Model): verbose_name="organisation du prescripteur de l'auteur", null=True, blank=True, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability ) created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now, db_index=True) updated_at = models.DateTimeField(verbose_name="date de modification", auto_now=True, db_index=True) @@ -122,7 +122,11 @@ class AbstractAdministrativeCriteria(models.Model): ui_rank = models.PositiveSmallIntegerField(default=MAX_UI_RANK) created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now) created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, verbose_name="créé par", null=True, blank=True, on_delete=models.SET_NULL + settings.AUTH_USER_MODEL, + verbose_name="créé par", + null=True, + blank=True, + on_delete=models.RESTRICT, # For traceability and accountability ) class Meta: diff --git a/itou/eligibility/models/geiq.py b/itou/eligibility/models/geiq.py index d6f403b940..bf65ef4a18 100644 --- a/itou/eligibility/models/geiq.py +++ b/itou/eligibility/models/geiq.py @@ -73,7 +73,7 @@ class GEIQEligibilityDiagnosis(AbstractEligibilityDiagnosisModel): null=True, blank=True, limit_choices_to={"kind": CompanyKind.GEIQ}, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability ) administrative_criteria = models.ManyToManyField( "eligibility.GEIQAdministrativeCriteria", @@ -228,7 +228,7 @@ class GEIQAdministrativeCriteria(AbstractAdministrativeCriteria): verbose_name="critère parent", blank=True, null=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # Prevent promoting a criteria from child to parent ) # Some criteria do not belong to an annex or a level annex = models.CharField( @@ -294,7 +294,7 @@ class GEIQSelectedAdministrativeCriteria(models.Model): ) administrative_criteria = models.ForeignKey( GEIQAdministrativeCriteria, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="administrative_criteria_through", ) created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now) diff --git a/itou/eligibility/models/iae.py b/itou/eligibility/models/iae.py index 7494d39807..6e94be80ac 100644 --- a/itou/eligibility/models/iae.py +++ b/itou/eligibility/models/iae.py @@ -121,7 +121,7 @@ class EligibilityDiagnosis(AbstractEligibilityDiagnosisModel): verbose_name="SIAE de l'auteur", null=True, blank=True, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability ) # Administrative criteria are mandatory only when an SIAE is performing an eligibility diagnosis. administrative_criteria = models.ManyToManyField( @@ -242,7 +242,11 @@ class AdministrativeCriteria(AbstractAdministrativeCriteria): ui_rank = models.PositiveSmallIntegerField(default=MAX_UI_RANK) created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now) created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, verbose_name="créé par", null=True, blank=True, on_delete=models.SET_NULL + settings.AUTH_USER_MODEL, + verbose_name="créé par", + null=True, + blank=True, + on_delete=models.RESTRICT, # For traceability and accountability ) objects = AdministrativeCriteriaQuerySet.as_manager() @@ -267,7 +271,7 @@ class SelectedAdministrativeCriteria(models.Model): eligibility_diagnosis = models.ForeignKey(EligibilityDiagnosis, on_delete=models.CASCADE) administrative_criteria = models.ForeignKey( AdministrativeCriteria, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="administrative_criteria_through", ) created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now) diff --git a/itou/geiq/migrations/0002_alter_implementationassessment_reviewed_by_and_more.py b/itou/geiq/migrations/0002_alter_implementationassessment_reviewed_by_and_more.py new file mode 100644 index 0000000000..87fe594b72 --- /dev/null +++ b/itou/geiq/migrations/0002_alter_implementationassessment_reviewed_by_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("geiq", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="implementationassessment", + name="reviewed_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="reviewed_geiq_assessment_set", + to=settings.AUTH_USER_MODEL, + verbose_name="contrôlé par", + ), + ), + migrations.AlterField( + model_name="implementationassessment", + name="submitted_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="submitted_geiq_assessment_set", + to=settings.AUTH_USER_MODEL, + verbose_name="transmis par", + ), + ), + ] diff --git a/itou/geiq/models.py b/itou/geiq/models.py index 44ad1c76eb..3c5651dfb2 100644 --- a/itou/geiq/models.py +++ b/itou/geiq/models.py @@ -58,7 +58,7 @@ class ImplementationAssessment(models.Model): related_name="submitted_geiq_assessment_set", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability ) reviewed_at = models.DateTimeField("date de contrôle", blank=True, null=True) @@ -74,7 +74,7 @@ class ImplementationAssessment(models.Model): related_name="reviewed_geiq_assessment_set", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability ) review_institution = models.ForeignKey( Institution, diff --git a/itou/institutions/migrations/0006_alter_institution_insee_city_and_more.py b/itou/institutions/migrations/0006_alter_institution_insee_city_and_more.py new file mode 100644 index 0000000000..0073958bde --- /dev/null +++ b/itou/institutions/migrations/0006_alter_institution_insee_city_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("cities", "0001_initial"), + ("institutions", "0005_set_institutions_active_members_email_reminder_last_sent_at"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="institution", + name="insee_city", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to="cities.city" + ), + ), + migrations.AlterField( + model_name="institutionmembership", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="updated_institutionmembership_set", + to=settings.AUTH_USER_MODEL, + verbose_name="mis à jour par", + ), + ), + ] diff --git a/itou/institutions/models.py b/itou/institutions/models.py index a5539e09ee..d6a781e0fe 100644 --- a/itou/institutions/models.py +++ b/itou/institutions/models.py @@ -56,7 +56,7 @@ class InstitutionMembership(MembershipAbstract): settings.AUTH_USER_MODEL, related_name="updated_institutionmembership_set", null=True, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability verbose_name="mis à jour par", ) diff --git a/itou/job_applications/migrations/0008_alter_jobapplication_approval_and_more.py b/itou/job_applications/migrations/0008_alter_jobapplication_approval_and_more.py new file mode 100644 index 0000000000..53ee00a06d --- /dev/null +++ b/itou/job_applications/migrations/0008_alter_jobapplication_approval_and_more.py @@ -0,0 +1,161 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("approvals", "0004_alter_approval_created_by_and_more"), + ("companies", "0006_alter_company_created_by_alter_company_insee_city_and_more"), + ("eligibility", "0005_alter_administrativecriteria_created_by_and_more"), + ("job_applications", "0007_jobapplicationtransitionlog_target_company"), + ("prescribers", "0004_poleemploi_to_francetravail"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="jobapplication", + name="approval", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="approvals.approval", + verbose_name="PASS IAE", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="approval_manually_delivered_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="approval_manually_delivered", + to=settings.AUTH_USER_MODEL, + verbose_name="PASS IAE délivré manuellement par", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="approval_manually_refused_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="approval_manually_refused", + to=settings.AUTH_USER_MODEL, + verbose_name="PASS IAE refusé manuellement par", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="eligibility_diagnosis", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="eligibility.eligibilitydiagnosis", + verbose_name="diagnostic d'éligibilité", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="geiq_eligibility_diagnosis", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="job_applications", + to="eligibility.geiqeligibilitydiagnosis", + verbose_name="diagnostic d'éligibilité GEIQ", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="job_seeker", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="job_applications", + to=settings.AUTH_USER_MODEL, + verbose_name="demandeur d'emploi", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="sender", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="job_applications_sent", + to=settings.AUTH_USER_MODEL, + verbose_name="utilisateur émetteur", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="sender_company", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="companies.company", + verbose_name="entreprise émettrice", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="sender_prescriber_organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to="prescribers.prescriberorganization", + verbose_name="organisation du prescripteur émettrice", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="to_company", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="job_applications_received", + to="companies.company", + verbose_name="entreprise destinataire", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="transferred_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to=settings.AUTH_USER_MODEL, + verbose_name="transférée par", + ), + ), + migrations.AlterField( + model_name="jobapplication", + name="transferred_from", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="job_application_transferred", + to="companies.company", + verbose_name="entreprise d'origine", + ), + ), + migrations.AlterField( + model_name="jobapplicationtransitionlog", + name="user", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/itou/job_applications/models.py b/itou/job_applications/models.py index ef7d06cf44..560165ce8a 100644 --- a/itou/job_applications/models.py +++ b/itou/job_applications/models.py @@ -506,7 +506,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): job_seeker = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="demandeur d'emploi", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # This object is central to us and the SIAE related_name="job_applications", ) @@ -520,7 +520,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): verbose_name="diagnostic d'éligibilité", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, ) geiq_eligibility_diagnosis = models.ForeignKey( @@ -528,7 +528,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): verbose_name="diagnostic d'éligibilité GEIQ", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, related_name="job_applications", ) @@ -544,7 +544,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): sender = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="utilisateur émetteur", - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability null=True, blank=True, related_name="job_applications_sent", @@ -559,7 +559,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): # When the sender is an employer, keep a track of his current company. sender_company = models.ForeignKey( - "companies.Company", verbose_name="entreprise émettrice", null=True, blank=True, on_delete=models.CASCADE + "companies.Company", verbose_name="entreprise émettrice", null=True, blank=True, on_delete=models.RESTRICT ) # When the sender is a prescriber, keep a track of his current organization (if any). @@ -568,13 +568,13 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): verbose_name="organisation du prescripteur émettrice", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability ) to_company = models.ForeignKey( "companies.Company", verbose_name="entreprise destinataire", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="job_applications_received", ) @@ -588,7 +588,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): verbose_name="poste retenu", blank=True, null=True, - on_delete=models.SET_NULL, + on_delete=models.SET_NULL, # SET_NULL so employers can delete job descriptions in their dashboards related_name="hired_job_applications", ) @@ -616,7 +616,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): # Job applications sent to SIAEs subject to eligibility rules can obtain an # Approval after being accepted. approval = models.ForeignKey( - "approvals.Approval", verbose_name="PASS IAE", null=True, blank=True, on_delete=models.SET_NULL + "approvals.Approval", verbose_name="PASS IAE", null=True, blank=True, on_delete=models.RESTRICT ) approval_delivery_mode = models.CharField( verbose_name="mode d'attribution du PASS IAE", @@ -633,7 +633,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): approval_manually_delivered_by = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="PASS IAE délivré manuellement par", - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability null=True, blank=True, related_name="approval_manually_delivered", @@ -641,7 +641,7 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): approval_manually_refused_by = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="PASS IAE refusé manuellement par", - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability null=True, blank=True, related_name="approval_manually_refused", @@ -654,14 +654,18 @@ class JobApplication(xwf_models.WorkflowEnabled, models.Model): transferred_at = models.DateTimeField(verbose_name="date de transfert", null=True, blank=True) transferred_by = models.ForeignKey( - settings.AUTH_USER_MODEL, verbose_name="transférée par", null=True, blank=True, on_delete=models.SET_NULL + settings.AUTH_USER_MODEL, + verbose_name="transférée par", + null=True, + blank=True, + on_delete=models.RESTRICT, # For traceability and accountability ) transferred_from = models.ForeignKey( "companies.Company", verbose_name="entreprise d'origine", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability related_name="job_application_transferred", ) @@ -978,10 +982,7 @@ def transfer(self, *, user, target_company): ) if is_eligibility_diagnosis_made_by_siae: self.eligibility_diagnosis = None - - # As 1:N or 1:1 objects must have a pk before being saved, - # eligibility diagnosis must be deleted after saving current object. - if is_eligibility_diagnosis_made_by_siae: + self.save(update_fields={"eligibility_diagnosis"}) eligibility_diagnosis.delete() notification_context = { @@ -1303,7 +1304,12 @@ class JobApplicationTransitionLog(xwf_models.BaseTransitionLog): ("target_company", "target_company", None), # used in external transfer and transfer ) job_application = models.ForeignKey(JobApplication, related_name="logs", on_delete=models.CASCADE) - user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + blank=True, + null=True, + on_delete=models.RESTRICT, # For traceability and accountability + ) target_company = models.ForeignKey( "companies.Company", verbose_name="entreprise destinataire", diff --git a/itou/prescribers/migrations/0005_alter_prescribermembership_updated_by_and_more.py b/itou/prescribers/migrations/0005_alter_prescribermembership_updated_by_and_more.py new file mode 100644 index 0000000000..2826b4ae11 --- /dev/null +++ b/itou/prescribers/migrations/0005_alter_prescribermembership_updated_by_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("cities", "0001_initial"), + ("prescribers", "0004_poleemploi_to_francetravail"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="prescribermembership", + name="updated_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="updated_prescribermembership_set", + to=settings.AUTH_USER_MODEL, + verbose_name="mis à jour par", + ), + ), + migrations.AlterField( + model_name="prescriberorganization", + name="authorization_updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="authorization_status_set", + to=settings.AUTH_USER_MODEL, + verbose_name="dernière MAJ de l'habilitation par", + ), + ), + migrations.AlterField( + model_name="prescriberorganization", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="created_prescriber_organization_set", + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="prescriberorganization", + name="insee_city", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to="cities.city" + ), + ), + ] diff --git a/itou/prescribers/models.py b/itou/prescribers/models.py index 6ec1e36cf6..f9028a0f1c 100644 --- a/itou/prescribers/models.py +++ b/itou/prescribers/models.py @@ -156,7 +156,7 @@ class PrescriberOrganization(AddressMixin, OrganizationAbstract): related_name="created_prescriber_organization_set", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability ) authorization_status = models.CharField( @@ -172,7 +172,7 @@ class PrescriberOrganization(AddressMixin, OrganizationAbstract): related_name="authorization_status_set", null=True, blank=True, - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # Only staff can update it, and we shouldn't delete one of those accounts ) # Use the generic relation to let NotificationSettings being collected on deletion @@ -345,7 +345,7 @@ class PrescriberMembership(MembershipAbstract): settings.AUTH_USER_MODEL, related_name="updated_prescribermembership_set", null=True, - on_delete=models.CASCADE, + on_delete=models.RESTRICT, # For traceability and accountability verbose_name="mis à jour par", ) diff --git a/itou/siae_evaluations/migrations/0002_alter_evaluatedadministrativecriteria_administrative_criteria_and_more.py b/itou/siae_evaluations/migrations/0002_alter_evaluatedadministrativecriteria_administrative_criteria_and_more.py new file mode 100644 index 0000000000..8bd0b2bc7d --- /dev/null +++ b/itou/siae_evaluations/migrations/0002_alter_evaluatedadministrativecriteria_administrative_criteria_and_more.py @@ -0,0 +1,78 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.db import migrations, models + +import itou.siae_evaluations.models + + +class Migration(migrations.Migration): + dependencies = [ + ("companies", "0006_alter_company_created_by_alter_company_insee_city_and_more"), + ("eligibility", "0005_alter_administrativecriteria_created_by_and_more"), + ("files", "0001_initial"), + ("institutions", "0006_alter_institution_insee_city_and_more"), + ("job_applications", "0008_alter_jobapplication_approval_and_more"), + ("siae_evaluations", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="evaluatedadministrativecriteria", + name="administrative_criteria", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="evaluated_administrative_criteria", + to="eligibility.administrativecriteria", + verbose_name="critère administratif", + ), + ), + migrations.AlterField( + model_name="evaluatedadministrativecriteria", + name="proof", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to="files.file" + ), + ), + migrations.AlterField( + model_name="evaluatedjobapplication", + name="evaluated_siae", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="evaluated_job_applications", + to="siae_evaluations.evaluatedsiae", + verbose_name="SIAE évaluée", + ), + ), + migrations.AlterField( + model_name="evaluatedjobapplication", + name="job_application", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="evaluated_job_applications", + to="job_applications.jobapplication", + verbose_name="candidature", + ), + ), + migrations.AlterField( + model_name="evaluatedsiae", + name="siae", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="evaluated_siaes", + to="companies.company", + verbose_name="SIAE", + ), + ), + migrations.AlterField( + model_name="evaluationcampaign", + name="institution", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="evaluation_campaigns", + to="institutions.institution", + validators=[itou.siae_evaluations.models.validate_institution], + verbose_name="DDETS IAE responsable du contrôle", + ), + ), + ] diff --git a/itou/siae_evaluations/models.py b/itou/siae_evaluations/models.py index 58f72ad2f1..4b9478c169 100644 --- a/itou/siae_evaluations/models.py +++ b/itou/siae_evaluations/models.py @@ -182,7 +182,7 @@ class EvaluationCampaign(models.Model): institution = models.ForeignKey( "institutions.Institution", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="evaluation_campaigns", verbose_name="DDETS IAE responsable du contrôle", validators=[validate_institution], @@ -474,7 +474,7 @@ class EvaluatedSiae(models.Model): siae = models.ForeignKey( "companies.Company", verbose_name="SIAE", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="evaluated_siaes", ) # In “phase amiable” until documents have been reviewed. @@ -664,14 +664,14 @@ class EvaluatedJobApplication(models.Model): job_application = models.ForeignKey( "job_applications.JobApplication", verbose_name="candidature", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="evaluated_job_applications", ) evaluated_siae = models.ForeignKey( EvaluatedSiae, verbose_name="SIAE évaluée", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="evaluated_job_applications", ) labor_inspector_explanation = models.TextField(verbose_name="commentaires de l'inspecteur du travail", blank=True) @@ -788,7 +788,7 @@ class EvaluatedAdministrativeCriteria(models.Model): administrative_criteria = models.ForeignKey( "eligibility.AdministrativeCriteria", verbose_name="critère administratif", - on_delete=models.CASCADE, + on_delete=models.RESTRICT, related_name="evaluated_administrative_criteria", ) @@ -799,7 +799,7 @@ class EvaluatedAdministrativeCriteria(models.Model): related_name="evaluated_administrative_criteria", ) - proof = models.ForeignKey("files.File", on_delete=models.CASCADE, blank=True, null=True) + proof = models.ForeignKey("files.File", on_delete=models.RESTRICT, blank=True, null=True) uploaded_at = models.DateTimeField(verbose_name="téléversé le", blank=True, null=True) submitted_at = models.DateTimeField(verbose_name="transmis le", blank=True, null=True) review_state = models.CharField( diff --git a/itou/users/migrations/0009_alter_user_created_by_alter_user_insee_city.py b/itou/users/migrations/0009_alter_user_created_by_alter_user_insee_city.py new file mode 100644 index 0000000000..2a5a1805f8 --- /dev/null +++ b/itou/users/migrations/0009_alter_user_created_by_alter_user_insee_city.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.7 on 2024-08-06 09:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("cities", "0001_initial"), + ("users", "0008_fix_user_public_id"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to=settings.AUTH_USER_MODEL, + verbose_name="créé par", + ), + ), + migrations.AlterField( + model_name="user", + name="insee_city", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to="cities.city" + ), + ), + ] diff --git a/itou/users/models.py b/itou/users/models.py index d02d21791b..586b7f7277 100644 --- a/itou/users/models.py +++ b/itou/users/models.py @@ -226,7 +226,7 @@ class User(AbstractUser, AddressMixin): created_by = models.ForeignKey( "self", verbose_name="créé par", - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, # For traceability and accountability null=True, blank=True, ) diff --git a/tests/job_applications/test_transfer.py b/tests/job_applications/test_transfer.py index 2909bd3dd7..82bbb2ade7 100644 --- a/tests/job_applications/test_transfer.py +++ b/tests/job_applications/test_transfer.py @@ -176,6 +176,7 @@ def test_model_fields(): ContentType.objects.get_for_model(target_company) with assertNumQueries( 2 # Check user is in both origin and dest siae + + 1 # Update job application to dereference eligibility diagnosis + 4 # Delete (+ SET_NULL) diagnosis and criteria made by the SIAE + 1 # Select user for email + 1 # Select employer notification settings diff --git a/tests/www/apply/test_process.py b/tests/www/apply/test_process.py index 5982d0a457..693e8641a9 100644 --- a/tests/www/apply/test_process.py +++ b/tests/www/apply/test_process.py @@ -1921,8 +1921,7 @@ def test_select_job_description_for_job_application(self): assertNotContains(response, "Préciser le nom du poste (code ROME)") def test_no_job_description_for_job_application(self): - job_descriptions = self.company.jobs.all() - job_descriptions.delete() + self.company.jobs.clear() job_application = self.create_job_application() employer = self.company.members.first() self.client.force_login(employer) @@ -1985,26 +1984,25 @@ def test_no_address(self): response.context["form_user_address"], "address_for_autocomplete", "Ce champ est obligatoire." ) - def test_no_diagnosis(self): + def test_no_diagnosis_on_job_application(self): diagnosis = IAEEligibilityDiagnosisFactory(from_prescriber=True) - job_application = self.create_job_application() + job_application = self.create_job_application(with_iae_eligibility_diagnosis=False) self.job_seeker.eligibility_diagnoses.add(diagnosis) # No eligibility diagnosis -> if job_seeker has a valid eligibility diagnosis, it's OK - job_application.eligibility_diagnosis = None - job_application.save() + assert job_application.eligibility_diagnosis is None employer = self.company.members.first() self.client.force_login(employer) self.accept_job_application(job_application=job_application, assert_successful=True, post_data={}) + def test_no_diagnosis(self): # if no, should not see the confirm button, nor accept posted data - job_application = self.create_job_application() - job_application.eligibility_diagnosis = None - job_application.save() - for approval in job_application.job_seeker.approvals.all(): - approval.delete() + job_application = self.create_job_application(with_iae_eligibility_diagnosis=False) + assert job_application.eligibility_diagnosis is None job_application.job_seeker.eligibility_diagnoses.all().delete() + employer = self.company.members.first() + self.client.force_login(employer) url_accept = reverse("apply:accept", kwargs={"job_application_id": job_application.pk}) response = self.client.get(url_accept, follow=True) self.assertRedirects( diff --git a/tests/www/approvals_views/test_prolongation_requests.py b/tests/www/approvals_views/test_prolongation_requests.py index 126a388d61..66a591bcee 100644 --- a/tests/www/approvals_views/test_prolongation_requests.py +++ b/tests/www/approvals_views/test_prolongation_requests.py @@ -19,6 +19,7 @@ from tests.approvals import factories as approvals_factories from tests.prescribers import factories as prescribers_factories from tests.users import factories as users_factories +from tests.users.factories import EmployerFactory from tests.utils.test import BASE_NUM_QUERIES, assert_previous_step, parse_response_to_soup @@ -76,8 +77,9 @@ def test_list_view(snapshot, client): def test_list_view_no_siae(snapshot, client): - prolongation_request = approvals_factories.ProlongationRequestFactory(for_snapshot=True) - prolongation_request.declared_by_siae.delete() + prolongation_request = approvals_factories.ProlongationRequestFactory( + for_snapshot=True, declared_by_siae=None, declared_by=EmployerFactory() + ) client.force_login(prolongation_request.validated_by) response = client.get(reverse("approvals:prolongation_requests_list")) @@ -145,8 +147,9 @@ def test_show_view(snapshot, client): def test_show_view_no_siae(client): - prolongation_request = approvals_factories.ProlongationRequestFactory() - prolongation_request.declared_by_siae.delete() + prolongation_request = approvals_factories.ProlongationRequestFactory( + declared_by_siae=None, declared_by=EmployerFactory() + ) client.force_login(prolongation_request.validated_by) default_storage.location = "snapshot"