From a40b5b6f2794d7c1b38a4ceee2d68c6184839305 Mon Sep 17 00:00:00 2001 From: Angelo Date: Tue, 4 Jul 2023 19:34:48 +0800 Subject: [PATCH] feat:adapting soft delete to CASCADE/PROTECT cases --- backend/dvadmin/utils/exception.py | 14 ++-- backend/dvadmin/utils/models.py | 113 ++++++++++++++++++++++++++--- 2 files changed, 108 insertions(+), 19 deletions(-) diff --git a/backend/dvadmin/utils/exception.py b/backend/dvadmin/utils/exception.py index b03a26c..37adebe 100644 --- a/backend/dvadmin/utils/exception.py +++ b/backend/dvadmin/utils/exception.py @@ -9,7 +9,7 @@ import logging import traceback -from django.db.models import ProtectedError +from django.db.models import ProtectedError, RestrictedError from django.http import Http404 from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed from rest_framework.status import HTTP_407_PROXY_AUTHENTICATION_REQUIRED, HTTP_401_UNAUTHORIZED @@ -29,32 +29,32 @@ def CustomExceptionHandler(ex, context): :param context: :return: """ - msg = '' + msg = "" code = 4000 # 调用默认的异常处理函数 response = exception_handler(ex, context) if isinstance(ex, AuthenticationFailed): # 如果是身份验证错误 - if response and response.data.get('detail') =="Given token not valid for any token type": + if response and response.data.get("detail") == "Given token not valid for any token type": code = 401 msg = ex.detail - elif response and response.data.get('detail') =="Token is blacklisted": + elif response and response.data.get("detail") == "Token is blacklisted": # token在黑名单 return ErrorResponse(status=HTTP_401_UNAUTHORIZED) else: code = 401 msg = ex.detail - elif isinstance(ex,Http404): + elif isinstance(ex, Http404): code = 400 msg = "接口地址不正确" elif isinstance(ex, DRFAPIException): set_rollback() msg = ex.detail - if isinstance(msg,dict): + if isinstance(msg, dict): for k, v in msg.items(): for i in v: msg = "%s:%s" % (k, i) - elif isinstance(ex, ProtectedError): + elif isinstance(ex, (ProtectedError, RestrictedError)): set_rollback() msg = "无法删除:该条数据与其他数据有相关绑定" # elif isinstance(ex, DatabaseError): diff --git a/backend/dvadmin/utils/models.py b/backend/dvadmin/utils/models.py index 9f741b0..5f29af5 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -10,8 +10,8 @@ import uuid from datetime import date, timedelta from django.apps import apps -from django.db import models, connection, ProgrammingError -from django.db.models import QuerySet +from django.db import models, transaction, connection, ProgrammingError +from django.core.exceptions import ObjectDoesNotExist from application import settings from application.dispatch import is_tenants_mode @@ -19,10 +19,16 @@ from application.dispatch import is_tenants_mode table_prefix = settings.TABLE_PREFIX # 数据库表名前缀 -class SoftDeleteQuerySet(QuerySet): - pass - +class SoftDeleteQuerySet(models.query.QuerySet): + @transaction.atomic + def delete(self, cascade=True): + if cascade: # delete one by one if cascade + for obj in self.all(): + obj.delete(cascade=cascade) + return self.update(is_deleted=True) + def hard_delete(self): + return super().delete() class SoftDeleteManager(models.Manager): @@ -34,8 +40,8 @@ class SoftDeleteManager(models.Manager): def filter(self, *args, **kwargs): # 考虑是否主动传入is_deleted - if not kwargs.get('is_deleted') is None: - self.__add_is_del_filter = True + if not kwargs.get("is_deleted") is None: + self.__add_is_del_filter = kwargs.get("is_deleted") return super(SoftDeleteManager, self).filter(*args, **kwargs) def get_queryset(self): @@ -49,8 +55,10 @@ class SoftDeleteManager(models.Manager): def get_month_range(start_day, end_day): months = (end_day.year - start_day.year) * 12 + end_day.month - start_day.month - month_range = ['%s-%s-01' % (start_day.year + mon // 12, str(mon % 12 + 1).zfill(2)) - for mon in range(start_day.month - 1, start_day.month + months)] + month_range = [ + "%s-%s-01" % (start_day.year + mon // 12, str(mon % 12 + 1).zfill(2)) + for mon in range(start_day.month - 1, start_day.month + months) + ] return month_range @@ -59,20 +67,101 @@ class SoftDeleteModel(models.Model): 软删除模型 一旦继承,就将开启软删除 """ - is_deleted = models.BooleanField(verbose_name="是否软删除", help_text='是否软删除', default=False, db_index=True) + + is_deleted = models.BooleanField(verbose_name="是否软删除", help_text="是否软删除", default=False, db_index=True) objects = SoftDeleteManager() class Meta: abstract = True - verbose_name = '软删除模型' + verbose_name = "软删除模型" verbose_name_plural = verbose_name - def delete(self, using=None, soft_delete=True, *args, **kwargs): + @transaction.atomic + def delete(self, using=None, cascade=True, *args, **kwargs): """ 重写删除方法,直接开启软删除 """ self.is_deleted = True self.save(using=using) + if cascade: + self.delete_related_objects(raise_exception=True) + # raise Exception("delete_related_objects") + + def hard_delete(self): + return super().delete() + + soft_delete_kwargs = { + "related_names": [], + } + + @classmethod + def _get_kwargs(cls): + return cls.soft_delete_kwargs + + @classmethod + def _get_relations(cls): + relations = {"foreign": [], "self": []} + related_fields = cls._get_kwargs().get("related_names", []) + if not related_fields: + fields = cls._meta.get_fields(include_hidden=True) + mutated_fields = [field for field in fields if field.is_relation and hasattr(field, "related_name")] + m2m_models = [field.through for field in mutated_fields if field.many_to_many] + related_fields = [ + field.related_name + for field in mutated_fields + if not field.many_to_many and field.related_model not in m2m_models and field.related_name + ] + tree_model_field = [ + field.field.name + for field in mutated_fields + if not field.many_to_many and field.related_model is field.model + ] + relations["foreign"] = related_fields + relations["self"] = f"{tree_model_field[0]}_id" if len(tree_model_field) == 1 else None + print(f"{relations=}", flush=True) + return relations + + def _is_cascade(self, relation): + on_delete_case = self._meta.get_field(relation).on_delete.__name__ + return on_delete_case == "CASCADE" + + def _get_related_objects(self, relation): + qs = getattr(self, relation) + if isinstance(qs, models.Manager): + return qs + return + + def related_objects(self, raise_exception=False, use_soft_manager=False): + relations = self._get_relations() + objects = {} + for relation in relations["foreign"]: + try: + qs = self._get_related_objects(relation) + except ObjectDoesNotExist as e: + if raise_exception: + raise e + continue + else: + objects[relation] = qs + if relations["self"]: + objects["self"] = self.__class__.objects.filter(**{relations["self"]: self.id}) + print(f"related_objects: {objects}", flush=True) + return objects + + def delete_related_objects(self, raise_exception=False): + for relation, qs in self.related_objects(raise_exception=raise_exception).items(): + if relation == "self": + qs.delete() + continue + if self._is_cascade(relation): + print(f"model {self.__class__} : cascade delete {relation} objects {qs.all()}", flush=True) + qs.all().delete() + else: + print(f"model {self.__class__} : protect delete {relation} objects {qs.all()}", flush=True) + if qs.all().exists(): + self.hard_delete() + qs.all().hard_delete() + # raise Exception("xxxxxxxxxxx for test xxxxxxxxxxx") class CoreModel(models.Model):