"""Database model utilities for Trustpoint."""from__future__importannotationsfromtypingimportTYPE_CHECKING,Any,finalfromdjango.dbimportmodels,transactionifTYPE_CHECKING:fromcollections.abcimportIterablefromdjango.db.modelsimportManager_ModelBase=models.Modelelse:_ModelBase=object__all__=['CustomDeleteActionManager','CustomDeleteActionModel','CustomDeleteActionQuerySet',]
[docs]classCustomDeleteActionManager[T:'CustomDeleteActionModel'](models.Manager[T]):"""Default manager for CustomDeleteActionModel. It ensures the CustomDeleteActionQuerySet is the default queryset. """
[docs]defget_queryset(self)->CustomDeleteActionQuerySet[T,T]:"""Return the queryset with individual delete."""returnCustomDeleteActionQuerySet(self.model,using=self._db)
[docs]classCustomDeleteActionModel(models.Model):"""Model that provides the pre_delete() and post_delete() methods to implement custom deletion logic. It uses a custom manager to ensure the methods are called both on individual and bulk (queryset) deletes. """
[docs]defpre_delete(self)->None:"""Pre-delete hook for custom logic before actual deletion. This can for example be used to check if deletion prerequisites are met. """
[docs]defpost_delete(self)->None:"""Post-delete hook for custom logic after actual deletion. This can for example be used to clean up orphaned related objects. Keep in mind the model is no longer in the database at the time this function is called. """
@final@transaction.atomic
[docs]defdelete(self,*args:Any,**kwargs:Any)->tuple[int,dict[str,int]]:"""Delete the object and run pre_delete() and post_delete() hooks."""self.pre_delete()count=super().delete(*args,**kwargs)self.post_delete()returncount
[docs]classCustomDeleteActionQuerySet[_Model:CustomDeleteActionModel,_Row:CustomDeleteActionModel](models.QuerySet[_Model,_Row]):"""Overrides a model's queryset to invoke pre- and post-delete hooks. This ensures the pre_delete() and post_delete() methods are called on each object in the queryset. """@transaction.atomic
[docs]defdelete(self,*args:Any,**kwargs:Any)->tuple[int,dict[str,int]]:"""Runs pre_delete() on each object, bulk deletes the queryset and runs post_delete() on each object. Args: *args: Positional arguments passed to super().delete(). **kwargs: Keyword arguments, for reference check, and passed to super().delete(). Returns: tuple[int, dict[str, int]]: A tuple of: a) the total number of objects deleted and b) a dictionary with the model name and the count. """# Pre-delete actions# create a copy of the models in the queryset for post-delete actions since it is cleared during the deletiondelargsdelkwargsobj_set:set[_Row]=set()forobjinself:obj_set.add(obj)obj.pre_delete()# Perform the actual deletioncount=super().delete()# Post-delete actionsforobjinobj_set:obj.post_delete()returncount
classOrphanDeletionMixin(_ModelBase):"""Mixin for referenced models that should be deleted after their referenced object is deleted. This mixin does not implicitly check for remaining references and always tries to delete the object. Therefore, it shall only be used when ALL references to the object either a) use on_delete=models.PROTECT (which will prevent deletion of the object if it is still referenced) or b) are ok with the reference being deleted even if not strictly orphaned (e.g. any remaining referencing object with on_delete=models.CASCADE will also be deleted). c) the reference is explicitly listed to be checked (by adding it to the "check_references_on_delete" class attribute tuple in the model class). """check_references_on_delete:tuple[str,...]|None=Noneobjects:Manager[OrphanDeletionMixin]@classmethoddefdelete_if_orphaned(cls,instance:OrphanDeletionMixin|None)->None:"""Removes the model instance if no longer referenced. This method checks if the referenced object is still referenced by other objects and only deletes it if it is not. The related fields to check for remaining references can be specified in the class attribute tuple check_references_on_delete. It is only necessary to check fields that are not protected (e.g. ManyToManyField). Args: instance: The instance to check and delete if orphaned. """ifnotinstanceornotinstance.pk:returnifinstance.check_references_on_delete:forrelininstance.check_references_on_delete:rel_qs=getattr(instance,rel)ifrel_qsandrel_qs.exists():returntry:instance.delete()exceptmodels.ProtectedError:return@classmethoddefmulti_delete_if_orphaned(cls,instance_pks:Iterable[int]|None)->None:"""Deletes multiple model instances by PK if no longer referenced."""ifnotinstance_pks:returnforinstanceininstance_pks:cls.delete_if_orphaned(cls.objects.filter(pk=instance).first())