From 2dad3374c64ba01316c79e813d88abe78a2895f4 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 --- itou/approvals/models.py | 18 +++++++++--------- itou/common_apps/organizations/models.py | 2 +- itou/companies/models.py | 8 ++++---- itou/eligibility/models/geiq.py | 4 ++-- itou/eligibility/models/iae.py | 2 +- itou/employee_record/models.py | 2 +- itou/institutions/models.py | 2 +- itou/job_applications/models.py | 12 +++++++----- itou/prescribers/models.py | 4 ++-- itou/siae_evaluations/models.py | 16 +++++++++------- itou/users/models.py | 2 +- 11 files changed, 38 insertions(+), 34 deletions(-) diff --git a/itou/approvals/models.py b/itou/approvals/models.py index 3a1312e94a7..326ebdccd70 100644 --- a/itou/approvals/models.py +++ b/itou/approvals/models.py @@ -484,7 +484,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( @@ -507,7 +507,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 ) # 2023-08-17: An experiment to add a denormalized field “last_suspension_ended_at” did not exhibit large @@ -1083,7 +1083,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) @@ -1092,7 +1092,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() @@ -1336,14 +1336,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. @@ -1352,7 +1352,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", ) @@ -1361,7 +1361,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. @@ -1511,7 +1511,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/organizations/models.py b/itou/common_apps/organizations/models.py index d20263951bb..09f9dac6feb 100644 --- a/itou/common_apps/organizations/models.py +++ b/itou/common_apps/organizations/models.py @@ -192,7 +192,7 @@ class MembershipAbstract(models.Model): settings.AUTH_USER_MODEL, related_name="updated_membershipmodel_set", null=True, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, verbose_name="mis à jour par", ) diff --git a/itou/companies/models.py b/itou/companies/models.py index d4c42f98a7e..4d07fb42daa 100644 --- a/itou/companies/models.py +++ b/itou/companies/models.py @@ -486,7 +486,7 @@ class CompanyMembership(MembershipAbstract): settings.AUTH_USER_MODEL, related_name="updated_companymembership_set", null=True, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, verbose_name="mis à jour par", ) notifications = models.JSONField(verbose_name="notifications", default=dict, blank=True) @@ -563,7 +563,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) @@ -581,7 +581,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", @@ -808,7 +808,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) diff --git a/itou/eligibility/models/geiq.py b/itou/eligibility/models/geiq.py index b8d32e026dd..3eda5565ff9 100644 --- a/itou/eligibility/models/geiq.py +++ b/itou/eligibility/models/geiq.py @@ -235,7 +235,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 form child to parent ) # Some criteria do not belong to an annex or a level annex = models.CharField( @@ -289,7 +289,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 3de9be14b28..32c08f8b8ff 100644 --- a/itou/eligibility/models/iae.py +++ b/itou/eligibility/models/iae.py @@ -276,7 +276,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/employee_record/models.py b/itou/employee_record/models.py index a66970c45f1..6fdbdcb7753 100644 --- a/itou/employee_record/models.py +++ b/itou/employee_record/models.py @@ -177,7 +177,7 @@ class EmployeeRecord(ASPExchangeInformation): # - Approval job_application = models.ForeignKey( "job_applications.jobapplication", - on_delete=models.SET_NULL, + on_delete=models.RESTRICT, null=True, verbose_name="candidature / embauche", related_name="employee_record", diff --git a/itou/institutions/models.py b/itou/institutions/models.py index 90e133c9216..e34a8ff38d0 100644 --- a/itou/institutions/models.py +++ b/itou/institutions/models.py @@ -43,7 +43,7 @@ class InstitutionMembership(MembershipAbstract): settings.AUTH_USER_MODEL, related_name="updated_institutionmembership_set", null=True, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, verbose_name="mis à jour par", ) diff --git a/itou/job_applications/models.py b/itou/job_applications/models.py index 56280104d52..d54cf4b5ac1 100644 --- a/itou/job_applications/models.py +++ b/itou/job_applications/models.py @@ -467,7 +467,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", ) @@ -505,7 +505,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.SET_NULL, # FIXME: Do we need traceability and accountability? null=True, blank=True, related_name="job_applications_sent", @@ -529,7 +529,7 @@ 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.SET_NULL, # FIXME: Do we need traceability and accountability? ) to_company = models.ForeignKey( @@ -549,7 +549,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, # FIXME: Maybe RESTRICT because its value should'nt changes, also exposed in API related_name="hired_job_applications", ) @@ -1230,7 +1230,9 @@ class JobApplicationTransitionLog(xwf_models.BaseTransitionLog): MODIFIED_OBJECT_FIELD = "job_application" EXTRA_LOG_ATTRIBUTES = (("user", "user", None),) 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.SET_NULL + ) # FIXME: Do we need traceability and accountability? class Meta: verbose_name = "log des transitions de la candidature" diff --git a/itou/prescribers/models.py b/itou/prescribers/models.py index a0229df485c..ed6e68e0398 100644 --- a/itou/prescribers/models.py +++ b/itou/prescribers/models.py @@ -170,7 +170,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 ) objects = PrescriberOrganizationManager.from_queryset(PrescriberOrganizationQuerySet)() @@ -333,7 +333,7 @@ class PrescriberMembership(MembershipAbstract): settings.AUTH_USER_MODEL, related_name="updated_prescribermembership_set", null=True, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, verbose_name="mis à jour par", ) diff --git a/itou/siae_evaluations/models.py b/itou/siae_evaluations/models.py index b5152ff1b0b..5c7984c11dd 100644 --- a/itou/siae_evaluations/models.py +++ b/itou/siae_evaluations/models.py @@ -181,7 +181,7 @@ class EvaluationCampaign(models.Model): institution = models.ForeignKey( "institutions.Institution", - on_delete=models.CASCADE, + on_delete=models.CASCADE, # FIXME: Seems dangerous to allow campaign deletion on institution deletion related_name="evaluation_campaigns", verbose_name="DDETS IAE responsable du contrôle", validators=[validate_institution], @@ -445,7 +445,7 @@ class EvaluatedSiae(models.Model): siae = models.ForeignKey( "companies.Company", verbose_name="SIAE", - on_delete=models.CASCADE, + on_delete=models.CASCADE, # FIXME: RESTRICT because CaP is an auditability tool? related_name="evaluated_siaes", ) # In “phase amiable” until documents have been reviewed. @@ -635,14 +635,14 @@ class EvaluatedJobApplication(models.Model): job_application = models.ForeignKey( "job_applications.JobApplication", verbose_name="candidature", - on_delete=models.CASCADE, + on_delete=models.CASCADE, # FIXME: RESTRICT because CaP is an auditability tool? related_name="evaluated_job_applications", ) evaluated_siae = models.ForeignKey( EvaluatedSiae, verbose_name="SIAE évaluée", - on_delete=models.CASCADE, + on_delete=models.CASCADE, # FIXME: RESTRICT because CaP is an auditability tool? related_name="evaluated_job_applications", ) labor_inspector_explanation = models.TextField(verbose_name="commentaires de l'inspecteur du travail", blank=True) @@ -759,18 +759,20 @@ class EvaluatedAdministrativeCriteria(models.Model): administrative_criteria = models.ForeignKey( "eligibility.AdministrativeCriteria", verbose_name="critère administratif", - on_delete=models.CASCADE, + on_delete=models.CASCADE, # FIXME: RESTRICT because CaP is an auditability tool? related_name="evaluated_administrative_criteria", ) evaluated_job_application = models.ForeignKey( EvaluatedJobApplication, verbose_name="candidature évaluée", - on_delete=models.CASCADE, + on_delete=models.CASCADE, # FIXME: RESTRICT because CaP is an auditability tool? 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.CASCADE, blank=True, null=True + ) # FIXME: RESTRICT because CaP is an auditability tool? 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/models.py b/itou/users/models.py index baac1da0eed..62c39db9b3d 100644 --- a/itou/users/models.py +++ b/itou/users/models.py @@ -196,7 +196,7 @@ class User(AbstractUser, AddressMixin): created_by = models.ForeignKey( "self", verbose_name="créé par", - on_delete=models.SET_NULL, + on_delete=models.SET_NULL, # FIXME: Do we need traceability and accountability? null=True, blank=True, )