92 lines
2.8 KiB
Python
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()
|