92 lines
2.8 KiB
Python

"""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()