From ff8bbed6f93e49ce0a77d024b480a61e181fa41e Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:29:30 +0530 Subject: [PATCH] chore: changed the soft deletion logic (#6171) --- apiserver/plane/app/views/issue/base.py | 2 - apiserver/plane/bgtasks/deletion_task.py | 117 ++++++++++++++++++----- apiserver/plane/db/models/__init__.py | 10 -- 3 files changed, 92 insertions(+), 37 deletions(-) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 7c15f91da9..d0c614368f 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -15,8 +15,6 @@ from django.db.models import ( UUIDField, Value, Subquery, - Case, - When, ) from django.db.models.functions import Coalesce from django.utils import timezone diff --git a/apiserver/plane/bgtasks/deletion_task.py b/apiserver/plane/bgtasks/deletion_task.py index b350c2299f..30ff7e8bd3 100644 --- a/apiserver/plane/bgtasks/deletion_task.py +++ b/apiserver/plane/bgtasks/deletion_task.py @@ -3,7 +3,8 @@ from django.utils import timezone from django.apps import apps from django.conf import settings from django.db import models -from django.core.exceptions import ObjectDoesNotExist +from django.db.models.fields.related import OneToOneRel + # Third party imports from celery import shared_task @@ -11,32 +12,98 @@ from celery import shared_task @shared_task def soft_delete_related_objects(app_label, model_name, instance_pk, using=None): + """ + Soft delete related objects for a given model instance + """ + # Get the model class using app registry model_class = apps.get_model(app_label, model_name) - instance = model_class.all_objects.get(pk=instance_pk) - related_fields = instance._meta.get_fields() - for field in related_fields: - if field.one_to_many or field.one_to_one: - try: - # Check if the field has CASCADE on delete - if ( - hasattr(field, "remote_field") - and hasattr(field.remote_field, "on_delete") - and field.remote_field.on_delete == models.CASCADE - ): - if field.one_to_many: - related_objects = getattr(instance, field.name).all() - elif field.one_to_one: - related_object = getattr(instance, field.name) - related_objects = ( - [related_object] if related_object is not None else [] - ) - for obj in related_objects: - if obj: - obj.deleted_at = timezone.now() - obj.save(using=using) - except ObjectDoesNotExist: - pass + # Get the instance using all_objects to ensure we can get even if it's already soft deleted + try: + instance = model_class.all_objects.get(pk=instance_pk) + except model_class.DoesNotExist: + return + + # Get all related fields that are reverse relationships + all_related = [ + f + for f in instance._meta.get_fields() + if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete + ] + + # Handle each related field + for relation in all_related: + related_name = relation.get_accessor_name() + + # Skip if the relation doesn't exist + if not hasattr(instance, related_name): + continue + + # Get the on_delete behavior name + on_delete_name = ( + relation.on_delete.__name__ + if hasattr(relation.on_delete, "__name__") + else "" + ) + + if on_delete_name == "DO_NOTHING": + continue + + elif on_delete_name == "SET_NULL": + # Handle SET_NULL relationships + if isinstance(relation, OneToOneRel): + # For OneToOne relationships + related_obj = getattr(instance, related_name, None) + if related_obj and isinstance(related_obj, models.Model): + setattr(related_obj, relation.remote_field.name, None) + related_obj.save(update_fields=[relation.remote_field.name]) + else: + # For other relationships + related_queryset = getattr(instance, related_name).all() + related_queryset.update(**{relation.remote_field.name: None}) + + else: + # Handle CASCADE and other delete behaviors + try: + if relation.one_to_one: + # Handle OneToOne relationships + related_obj = getattr(instance, related_name, None) + if related_obj: + if hasattr(related_obj, "deleted_at"): + if not related_obj.deleted_at: + related_obj.deleted_at = timezone.now() + related_obj.save() + # Recursively handle related objects + soft_delete_related_objects( + related_obj._meta.app_label, + related_obj._meta.model_name, + related_obj.pk, + using, + ) + else: + # Handle other relationships + related_queryset = getattr(instance, related_name).all() + for related_obj in related_queryset: + if hasattr(related_obj, "deleted_at"): + if not related_obj.deleted_at: + related_obj.deleted_at = timezone.now() + related_obj.save() + # Recursively handle related objects + soft_delete_related_objects( + related_obj._meta.app_label, + related_obj._meta.model_name, + related_obj.pk, + using, + ) + except Exception as e: + # Log the error or handle as needed + print(f"Error handling relation {related_name}: {str(e)}") + continue + + # Finally, soft delete the instance itself if it hasn't been deleted yet + if hasattr(instance, "deleted_at") and not instance.deleted_at: + instance.deleted_at = timezone.now() + instance.save() # @shared_task diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 36810956c2..e3a9df2542 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -53,7 +53,6 @@ from .project import ( ProjectMemberInvite, ProjectPublicMember, ) -from .deploy_board import DeployBoard from .session import Session from .social_connection import SocialLoginConnection from .state import State @@ -69,23 +68,14 @@ from .workspace import ( WorkspaceUserProperties, ) -from .importer import Importer -from .page import Page, PageLog, PageLabel -from .estimate import Estimate, EstimatePoint -from .intake import Intake, IntakeIssue -from .analytic import AnalyticView -from .notification import Notification, UserNotificationPreference, EmailNotificationLog -from .exporter import ExporterHistory -from .webhook import Webhook, WebhookLog -from .dashboard import Dashboard, DashboardWidget, Widget from .favorite import UserFavorite