"""Application use cases for document-folder trees.""" from django.db import transaction from filer.models import Folder from documents.models import Document from guests.models import Guest class FolderTreeDeletionError(Exception): """Expected business-rule failure while deleting a folder tree.""" class GuestManagedFolderError(FolderTreeDeletionError): """A guest-owned folder must be managed through the guest workflow.""" class ProtectedFolderDocumentsError(FolderTreeDeletionError): """The actor cannot delete every document affected by the operation.""" def collect_descendant_folder_ids(folder_id: int) -> set[int]: """Return one folder id and every descendant id without recursion limits.""" folder_ids = {folder_id} current_level = [folder_id] while current_level: current_level = list( Folder.objects.filter(parent_id__in=current_level).values_list( "id", flat=True, ) ) folder_ids.update(current_level) return folder_ids def collect_cascading_document_ids(folder_ids: set[int]) -> set[int]: """Return documents in the tree and documents explicitly related to them.""" document_ids = set( Document.objects.filter(folder_id__in=folder_ids).values_list( "id", flat=True, ) ) if document_ids: document_ids.update( Document.objects.filter( related_documents__id__in=document_ids, ).values_list("id", flat=True) ) return document_ids def _ensure_actor_can_delete_documents(*, actor, document_ids: set[int]) -> None: if ( actor.is_staff or actor.is_superuser or actor.has_perm("documents.delete_document") ): return protected_exists = any( document.uploaded_by_id != actor.id and not actor.has_perm("documents.delete_document", document) for document in Document.objects.filter(id__in=document_ids).only( "id", "uploaded_by_id", ) ) if protected_exists: raise ProtectedFolderDocumentsError @transaction.atomic def delete_folder_tree(*, folder_id: int, actor) -> None: """Delete a folder tree and its documented cascade as one transaction.""" locked_folder = Folder.objects.select_for_update().get(pk=folder_id) folder_ids = collect_descendant_folder_ids(locked_folder.id) if Guest.objects.filter(document_folder_id__in=folder_ids).exists(): raise GuestManagedFolderError document_ids = collect_cascading_document_ids(folder_ids) _ensure_actor_can_delete_documents( actor=actor, document_ids=document_ids, ) if document_ids: Document.objects.filter(id__in=document_ids).delete() Folder.objects.filter(id__in=folder_ids).delete()