mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-17 03:56:31 +01:00
Changes before error encountered
Co-authored-by: dawnsystem <42047891+dawnsystem@users.noreply.github.com>
This commit is contained in:
parent
780decf543
commit
f67dd152e6
5 changed files with 686 additions and 0 deletions
|
|
@ -2696,3 +2696,61 @@ class StoragePathTestSerializer(SerializerWithPerms):
|
|||
label="Document",
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
|
||||
class DeletionRequestSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for DeletionRequest model with document details."""
|
||||
|
||||
document_details = serializers.SerializerMethodField()
|
||||
user_username = serializers.CharField(source='user.username', read_only=True)
|
||||
reviewed_by_username = serializers.CharField(
|
||||
source='reviewed_by.username',
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
from documents.models import DeletionRequest
|
||||
model = DeletionRequest
|
||||
fields = [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'requested_by_ai',
|
||||
'ai_reason',
|
||||
'user',
|
||||
'user_username',
|
||||
'status',
|
||||
'impact_summary',
|
||||
'reviewed_at',
|
||||
'reviewed_by',
|
||||
'reviewed_by_username',
|
||||
'review_comment',
|
||||
'completed_at',
|
||||
'completion_details',
|
||||
'document_details',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'reviewed_at',
|
||||
'reviewed_by',
|
||||
'completed_at',
|
||||
'completion_details',
|
||||
]
|
||||
|
||||
def get_document_details(self, obj):
|
||||
"""Get details of documents in this deletion request."""
|
||||
documents = obj.documents.all()
|
||||
return [
|
||||
{
|
||||
'id': doc.id,
|
||||
'title': doc.title,
|
||||
'created': doc.created.isoformat() if doc.created else None,
|
||||
'correspondent': doc.correspondent.name if doc.correspondent else None,
|
||||
'document_type': doc.document_type.name if doc.document_type else None,
|
||||
'tags': [tag.name for tag in doc.tags.all()],
|
||||
}
|
||||
for doc in documents
|
||||
]
|
||||
|
|
|
|||
359
src/documents/tests/test_api_deletion_requests.py
Normal file
359
src/documents/tests/test_api_deletion_requests.py
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
"""
|
||||
API tests for DeletionRequest endpoints.
|
||||
|
||||
Tests cover:
|
||||
- List and retrieve deletion requests
|
||||
- Approve endpoint with permissions and status validation
|
||||
- Reject endpoint with permissions and status validation
|
||||
- Cancel endpoint with permissions and status validation
|
||||
- Permission checking (owner vs non-owner vs admin)
|
||||
- Execution flow when approved
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.models import (
|
||||
Correspondent,
|
||||
DeletionRequest,
|
||||
Document,
|
||||
DocumentType,
|
||||
Tag,
|
||||
)
|
||||
|
||||
|
||||
class TestDeletionRequestAPI(APITestCase):
|
||||
"""Test DeletionRequest API endpoints."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
# Create users
|
||||
self.user1 = User.objects.create_user(username="user1", password="pass123")
|
||||
self.user2 = User.objects.create_user(username="user2", password="pass123")
|
||||
self.admin = User.objects.create_superuser(username="admin", password="admin123")
|
||||
|
||||
# Create test documents
|
||||
self.doc1 = Document.objects.create(
|
||||
title="Test Document 1",
|
||||
content="Content 1",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
self.doc2 = Document.objects.create(
|
||||
title="Test Document 2",
|
||||
content="Content 2",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
self.doc3 = Document.objects.create(
|
||||
title="Test Document 3",
|
||||
content="Content 3",
|
||||
checksum="checksum3",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
# Create deletion requests
|
||||
self.request1 = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Duplicate document detected",
|
||||
user=self.user1,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
impact_summary={"document_count": 1},
|
||||
)
|
||||
self.request1.documents.add(self.doc1)
|
||||
|
||||
self.request2 = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Low quality document",
|
||||
user=self.user2,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
impact_summary={"document_count": 1},
|
||||
)
|
||||
self.request2.documents.add(self.doc2)
|
||||
|
||||
def test_list_deletion_requests_as_owner(self):
|
||||
"""Test that users can list their own deletion requests."""
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
response = self.client.get("/api/deletion-requests/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["results"]), 1)
|
||||
self.assertEqual(response.data["results"][0]["id"], self.request1.id)
|
||||
|
||||
def test_list_deletion_requests_as_admin(self):
|
||||
"""Test that admin can list all deletion requests."""
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
response = self.client.get("/api/deletion-requests/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["results"]), 2)
|
||||
|
||||
def test_retrieve_deletion_request(self):
|
||||
"""Test retrieving a single deletion request."""
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
response = self.client.get(f"/api/deletion-requests/{self.request1.id}/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["id"], self.request1.id)
|
||||
self.assertEqual(response.data["ai_reason"], "Duplicate document detected")
|
||||
self.assertEqual(response.data["status"], DeletionRequest.STATUS_PENDING)
|
||||
self.assertIn("document_details", response.data)
|
||||
|
||||
def test_approve_deletion_request_as_owner(self):
|
||||
"""Test approving a deletion request as the owner."""
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
# Verify document exists
|
||||
self.assertTrue(Document.objects.filter(id=self.doc1.id).exists())
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/approve/",
|
||||
{"comment": "Approved by owner"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn("message", response.data)
|
||||
self.assertIn("execution_result", response.data)
|
||||
self.assertEqual(response.data["execution_result"]["deleted_count"], 1)
|
||||
|
||||
# Verify document was deleted
|
||||
self.assertFalse(Document.objects.filter(id=self.doc1.id).exists())
|
||||
|
||||
# Verify deletion request was updated
|
||||
self.request1.refresh_from_db()
|
||||
self.assertEqual(self.request1.status, DeletionRequest.STATUS_COMPLETED)
|
||||
self.assertIsNotNone(self.request1.reviewed_at)
|
||||
self.assertEqual(self.request1.reviewed_by, self.user1)
|
||||
self.assertEqual(self.request1.review_comment, "Approved by owner")
|
||||
|
||||
def test_approve_deletion_request_as_admin(self):
|
||||
"""Test approving a deletion request as admin."""
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request2.id}/approve/",
|
||||
{"comment": "Approved by admin"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn("execution_result", response.data)
|
||||
|
||||
# Verify document was deleted
|
||||
self.assertFalse(Document.objects.filter(id=self.doc2.id).exists())
|
||||
|
||||
# Verify deletion request was updated
|
||||
self.request2.refresh_from_db()
|
||||
self.assertEqual(self.request2.status, DeletionRequest.STATUS_COMPLETED)
|
||||
self.assertEqual(self.request2.reviewed_by, self.admin)
|
||||
|
||||
def test_approve_deletion_request_without_permission(self):
|
||||
"""Test that non-owners cannot approve deletion requests."""
|
||||
self.client.force_authenticate(user=self.user2)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/approve/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Verify document was NOT deleted
|
||||
self.assertTrue(Document.objects.filter(id=self.doc1.id).exists())
|
||||
|
||||
# Verify deletion request was NOT updated
|
||||
self.request1.refresh_from_db()
|
||||
self.assertEqual(self.request1.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
def test_approve_already_approved_request(self):
|
||||
"""Test that already approved requests cannot be approved again."""
|
||||
self.request1.status = DeletionRequest.STATUS_APPROVED
|
||||
self.request1.save()
|
||||
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/approve/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("error", response.data)
|
||||
self.assertIn("pending", response.data["error"].lower())
|
||||
|
||||
def test_reject_deletion_request_as_owner(self):
|
||||
"""Test rejecting a deletion request as the owner."""
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/reject/",
|
||||
{"comment": "Not needed"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn("message", response.data)
|
||||
|
||||
# Verify document was NOT deleted
|
||||
self.assertTrue(Document.objects.filter(id=self.doc1.id).exists())
|
||||
|
||||
# Verify deletion request was updated
|
||||
self.request1.refresh_from_db()
|
||||
self.assertEqual(self.request1.status, DeletionRequest.STATUS_REJECTED)
|
||||
self.assertIsNotNone(self.request1.reviewed_at)
|
||||
self.assertEqual(self.request1.reviewed_by, self.user1)
|
||||
self.assertEqual(self.request1.review_comment, "Not needed")
|
||||
|
||||
def test_reject_deletion_request_as_admin(self):
|
||||
"""Test rejecting a deletion request as admin."""
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request2.id}/reject/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Verify document was NOT deleted
|
||||
self.assertTrue(Document.objects.filter(id=self.doc2.id).exists())
|
||||
|
||||
# Verify deletion request was updated
|
||||
self.request2.refresh_from_db()
|
||||
self.assertEqual(self.request2.status, DeletionRequest.STATUS_REJECTED)
|
||||
self.assertEqual(self.request2.reviewed_by, self.admin)
|
||||
|
||||
def test_reject_deletion_request_without_permission(self):
|
||||
"""Test that non-owners cannot reject deletion requests."""
|
||||
self.client.force_authenticate(user=self.user2)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/reject/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Verify deletion request was NOT updated
|
||||
self.request1.refresh_from_db()
|
||||
self.assertEqual(self.request1.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
def test_reject_already_rejected_request(self):
|
||||
"""Test that already rejected requests cannot be rejected again."""
|
||||
self.request1.status = DeletionRequest.STATUS_REJECTED
|
||||
self.request1.save()
|
||||
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/reject/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("error", response.data)
|
||||
|
||||
def test_cancel_deletion_request_as_owner(self):
|
||||
"""Test canceling a deletion request as the owner."""
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/cancel/",
|
||||
{"comment": "Changed my mind"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn("message", response.data)
|
||||
|
||||
# Verify document was NOT deleted
|
||||
self.assertTrue(Document.objects.filter(id=self.doc1.id).exists())
|
||||
|
||||
# Verify deletion request was updated
|
||||
self.request1.refresh_from_db()
|
||||
self.assertEqual(self.request1.status, DeletionRequest.STATUS_CANCELLED)
|
||||
self.assertIsNotNone(self.request1.reviewed_at)
|
||||
self.assertEqual(self.request1.reviewed_by, self.user1)
|
||||
self.assertIn("Changed my mind", self.request1.review_comment)
|
||||
|
||||
def test_cancel_deletion_request_without_permission(self):
|
||||
"""Test that non-owners cannot cancel deletion requests."""
|
||||
self.client.force_authenticate(user=self.user2)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/cancel/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Verify deletion request was NOT updated
|
||||
self.request1.refresh_from_db()
|
||||
self.assertEqual(self.request1.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
def test_cancel_already_approved_request(self):
|
||||
"""Test that approved requests cannot be cancelled."""
|
||||
self.request1.status = DeletionRequest.STATUS_APPROVED
|
||||
self.request1.save()
|
||||
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/cancel/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("error", response.data)
|
||||
|
||||
def test_approve_with_multiple_documents(self):
|
||||
"""Test approving a deletion request with multiple documents."""
|
||||
# Create a deletion request with multiple documents
|
||||
multi_request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Multiple duplicates",
|
||||
user=self.user1,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
impact_summary={"document_count": 2},
|
||||
)
|
||||
multi_request.documents.add(self.doc1, self.doc3)
|
||||
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{multi_request.id}/approve/",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["execution_result"]["deleted_count"], 2)
|
||||
self.assertEqual(response.data["execution_result"]["total_documents"], 2)
|
||||
|
||||
# Verify both documents were deleted
|
||||
self.assertFalse(Document.objects.filter(id=self.doc1.id).exists())
|
||||
self.assertFalse(Document.objects.filter(id=self.doc3.id).exists())
|
||||
|
||||
def test_document_details_in_response(self):
|
||||
"""Test that document details are properly included in response."""
|
||||
# Add some metadata to the document
|
||||
tag = Tag.objects.create(name="test-tag")
|
||||
correspondent = Correspondent.objects.create(name="Test Corp")
|
||||
doc_type = DocumentType.objects.create(name="Invoice")
|
||||
|
||||
self.doc1.tags.add(tag)
|
||||
self.doc1.correspondent = correspondent
|
||||
self.doc1.document_type = doc_type
|
||||
self.doc1.save()
|
||||
|
||||
self.client.force_authenticate(user=self.user1)
|
||||
response = self.client.get(f"/api/deletion-requests/{self.request1.id}/")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
doc_details = response.data["document_details"]
|
||||
self.assertEqual(len(doc_details), 1)
|
||||
self.assertEqual(doc_details[0]["id"], self.doc1.id)
|
||||
self.assertEqual(doc_details[0]["title"], "Test Document 1")
|
||||
self.assertEqual(doc_details[0]["correspondent"], "Test Corp")
|
||||
self.assertEqual(doc_details[0]["document_type"], "Invoice")
|
||||
self.assertIn("test-tag", doc_details[0]["tags"])
|
||||
|
||||
def test_unauthenticated_access(self):
|
||||
"""Test that unauthenticated users cannot access the API."""
|
||||
response = self.client.get("/api/deletion-requests/")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/deletion-requests/{self.request1.id}/approve/",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
5
src/documents/views/__init__.py
Normal file
5
src/documents/views/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""Views module for documents app."""
|
||||
|
||||
from documents.views.deletion_request import DeletionRequestViewSet
|
||||
|
||||
__all__ = ["DeletionRequestViewSet"]
|
||||
262
src/documents/views/deletion_request.py
Normal file
262
src/documents/views/deletion_request.py
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
"""
|
||||
API ViewSet for DeletionRequest management.
|
||||
|
||||
Provides endpoints for:
|
||||
- Listing and retrieving deletion requests
|
||||
- Approving deletion requests (POST /api/deletion-requests/{id}/approve/)
|
||||
- Rejecting deletion requests (POST /api/deletion-requests/{id}/reject/)
|
||||
- Canceling deletion requests (POST /api/deletion-requests/{id}/cancel/)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from documents.models import DeletionRequest
|
||||
from documents.serialisers import DeletionRequestSerializer
|
||||
|
||||
logger = logging.getLogger("paperless.api")
|
||||
|
||||
|
||||
class DeletionRequestViewSet(ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing deletion requests.
|
||||
|
||||
Provides CRUD operations plus custom actions for approval workflow.
|
||||
"""
|
||||
|
||||
model = DeletionRequest
|
||||
serializer_class = DeletionRequestSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Return deletion requests for the current user.
|
||||
|
||||
Superusers can see all requests.
|
||||
Regular users only see their own requests.
|
||||
"""
|
||||
user = self.request.user
|
||||
if user.is_superuser:
|
||||
return DeletionRequest.objects.all()
|
||||
return DeletionRequest.objects.filter(user=user)
|
||||
|
||||
def _can_manage_request(self, deletion_request):
|
||||
"""
|
||||
Check if current user can manage (approve/reject/cancel) the request.
|
||||
|
||||
Args:
|
||||
deletion_request: The DeletionRequest instance
|
||||
|
||||
Returns:
|
||||
bool: True if user is the owner or a superuser
|
||||
"""
|
||||
user = self.request.user
|
||||
return user.is_superuser or deletion_request.user == user
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def approve(self, request, pk=None):
|
||||
"""
|
||||
Approve a pending deletion request and execute the deletion.
|
||||
|
||||
Validates:
|
||||
- User has permission (owner or admin)
|
||||
- Status is pending
|
||||
|
||||
Returns:
|
||||
Response with execution results
|
||||
"""
|
||||
deletion_request = self.get_object()
|
||||
|
||||
# Check permissions
|
||||
if not self._can_manage_request(deletion_request):
|
||||
return HttpResponseForbidden(
|
||||
"You don't have permission to approve this deletion request."
|
||||
)
|
||||
|
||||
# Validate status
|
||||
if deletion_request.status != DeletionRequest.STATUS_PENDING:
|
||||
return Response(
|
||||
{
|
||||
"error": "Only pending deletion requests can be approved.",
|
||||
"current_status": deletion_request.status,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
comment = request.data.get("comment", "")
|
||||
|
||||
# Execute approval and deletion in a transaction
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Approve the request
|
||||
if not deletion_request.approve(request.user, comment):
|
||||
return Response(
|
||||
{"error": "Failed to approve deletion request."},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
# Execute the deletion
|
||||
documents = list(deletion_request.documents.all())
|
||||
deleted_count = 0
|
||||
failed_deletions = []
|
||||
|
||||
for doc in documents:
|
||||
try:
|
||||
doc_id = doc.id
|
||||
doc_title = doc.title
|
||||
doc.delete()
|
||||
deleted_count += 1
|
||||
logger.info(
|
||||
f"Deleted document {doc_id} ('{doc_title}') "
|
||||
f"as part of deletion request {deletion_request.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete document {doc.id}: {str(e)}"
|
||||
)
|
||||
failed_deletions.append({
|
||||
"id": doc.id,
|
||||
"title": doc.title,
|
||||
"error": str(e),
|
||||
})
|
||||
|
||||
# Update completion status
|
||||
deletion_request.status = DeletionRequest.STATUS_COMPLETED
|
||||
deletion_request.completed_at = timezone.now()
|
||||
deletion_request.completion_details = {
|
||||
"deleted_count": deleted_count,
|
||||
"failed_deletions": failed_deletions,
|
||||
"total_documents": len(documents),
|
||||
}
|
||||
deletion_request.save()
|
||||
|
||||
logger.info(
|
||||
f"Deletion request {deletion_request.id} completed. "
|
||||
f"Deleted {deleted_count}/{len(documents)} documents."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error executing deletion request {deletion_request.id}: {str(e)}"
|
||||
)
|
||||
return Response(
|
||||
{"error": f"Failed to execute deletion: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(deletion_request)
|
||||
return Response(
|
||||
{
|
||||
"message": "Deletion request approved and executed successfully.",
|
||||
"execution_result": deletion_request.completion_details,
|
||||
"deletion_request": serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def reject(self, request, pk=None):
|
||||
"""
|
||||
Reject a pending deletion request.
|
||||
|
||||
Validates:
|
||||
- User has permission (owner or admin)
|
||||
- Status is pending
|
||||
|
||||
Returns:
|
||||
Response with updated deletion request
|
||||
"""
|
||||
deletion_request = self.get_object()
|
||||
|
||||
# Check permissions
|
||||
if not self._can_manage_request(deletion_request):
|
||||
return HttpResponseForbidden(
|
||||
"You don't have permission to reject this deletion request."
|
||||
)
|
||||
|
||||
# Validate status
|
||||
if deletion_request.status != DeletionRequest.STATUS_PENDING:
|
||||
return Response(
|
||||
{
|
||||
"error": "Only pending deletion requests can be rejected.",
|
||||
"current_status": deletion_request.status,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
comment = request.data.get("comment", "")
|
||||
|
||||
# Reject the request
|
||||
if not deletion_request.reject(request.user, comment):
|
||||
return Response(
|
||||
{"error": "Failed to reject deletion request."},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Deletion request {deletion_request.id} rejected by user {request.user.username}"
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(deletion_request)
|
||||
return Response(
|
||||
{
|
||||
"message": "Deletion request rejected successfully.",
|
||||
"deletion_request": serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def cancel(self, request, pk=None):
|
||||
"""
|
||||
Cancel a pending deletion request.
|
||||
|
||||
Validates:
|
||||
- User has permission (owner or admin)
|
||||
- Status is pending
|
||||
|
||||
Returns:
|
||||
Response with updated deletion request
|
||||
"""
|
||||
deletion_request = self.get_object()
|
||||
|
||||
# Check permissions
|
||||
if not self._can_manage_request(deletion_request):
|
||||
return HttpResponseForbidden(
|
||||
"You don't have permission to cancel this deletion request."
|
||||
)
|
||||
|
||||
# Validate status
|
||||
if deletion_request.status != DeletionRequest.STATUS_PENDING:
|
||||
return Response(
|
||||
{
|
||||
"error": "Only pending deletion requests can be cancelled.",
|
||||
"current_status": deletion_request.status,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Cancel the request
|
||||
deletion_request.status = DeletionRequest.STATUS_CANCELLED
|
||||
deletion_request.reviewed_by = request.user
|
||||
deletion_request.reviewed_at = timezone.now()
|
||||
deletion_request.review_comment = request.data.get("comment", "Cancelled by user")
|
||||
deletion_request.save()
|
||||
|
||||
logger.info(
|
||||
f"Deletion request {deletion_request.id} cancelled by user {request.user.username}"
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(deletion_request)
|
||||
return Response(
|
||||
{
|
||||
"message": "Deletion request cancelled successfully.",
|
||||
"deletion_request": serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
|
@ -43,6 +43,7 @@ from documents.views import WorkflowActionViewSet
|
|||
from documents.views import WorkflowTriggerViewSet
|
||||
from documents.views import WorkflowViewSet
|
||||
from documents.views import serve_logo
|
||||
from documents.views.deletion_request import DeletionRequestViewSet
|
||||
from paperless.consumers import StatusConsumer
|
||||
from paperless.views import ApplicationConfigurationViewSet
|
||||
from paperless.views import DisconnectSocialAccountView
|
||||
|
|
@ -79,6 +80,7 @@ api_router.register(r"workflows", WorkflowViewSet)
|
|||
api_router.register(r"custom_fields", CustomFieldViewSet)
|
||||
api_router.register(r"config", ApplicationConfigurationViewSet)
|
||||
api_router.register(r"processed_mail", ProcessedMailViewSet)
|
||||
api_router.register(r"deletion-requests", DeletionRequestViewSet, basename="deletion-requests")
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue