800 lines
27 KiB
Python
800 lines
27 KiB
Python
"""
|
|
Admin Router
|
|
|
|
This router contains admin-only endpoints for managing soft-deleted data
|
|
and other administrative functions.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from sqlalchemy.orm import Session
|
|
from typing import List, Optional
|
|
from pydantic import BaseModel
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
from database import get_db
|
|
from models.user import User, UserRole
|
|
from utils.auth import get_current_user_from_token
|
|
from services.recovery_service import RecoveryService
|
|
from services.batch_operations import BatchOperationsService
|
|
|
|
router = APIRouter()
|
|
|
|
# Simple rate limiting storage (in production, use Redis or similar)
|
|
_rate_limit_storage = defaultdict(list)
|
|
PERMANENT_DELETE_RATE_LIMIT = 10 # Max 10 permanent delete operations per minute per user
|
|
|
|
|
|
class BulkRecoveryRequest(BaseModel):
|
|
shot_ids: List[int] = []
|
|
asset_ids: List[int] = []
|
|
|
|
|
|
class BulkDeletionRequest(BaseModel):
|
|
shot_ids: List[int] = []
|
|
asset_ids: List[int] = []
|
|
batch_size: Optional[int] = 50
|
|
|
|
|
|
class BatchPreviewRequest(BaseModel):
|
|
shot_ids: List[int] = []
|
|
asset_ids: List[int] = []
|
|
|
|
|
|
class PermanentDeleteRequest(BaseModel):
|
|
confirmation_token: str
|
|
|
|
|
|
class BulkPermanentDeleteRequest(BaseModel):
|
|
shot_ids: List[int] = []
|
|
asset_ids: List[int] = []
|
|
confirmation_token: str
|
|
|
|
|
|
def require_admin(
|
|
token_data: dict = Depends(get_current_user_from_token),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Require admin role."""
|
|
from utils.auth import _get_user_from_db
|
|
current_user = _get_user_from_db(db, token_data["user_id"])
|
|
|
|
if not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin permission required"
|
|
)
|
|
return current_user
|
|
|
|
|
|
def check_permanent_delete_rate_limit(user_id: int):
|
|
"""Check if user has exceeded permanent delete rate limit."""
|
|
current_time = time.time()
|
|
user_requests = _rate_limit_storage[user_id]
|
|
|
|
# Remove requests older than 1 minute
|
|
user_requests[:] = [req_time for req_time in user_requests if current_time - req_time < 60]
|
|
|
|
if len(user_requests) >= PERMANENT_DELETE_RATE_LIMIT:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
detail=f"Rate limit exceeded. Maximum {PERMANENT_DELETE_RATE_LIMIT} permanent delete operations per minute."
|
|
)
|
|
|
|
# Add current request
|
|
user_requests.append(current_time)
|
|
|
|
|
|
def validate_confirmation_token(token: str, expected_action: str):
|
|
"""Validate confirmation token for permanent delete operations."""
|
|
# Simple token validation - in production, use proper token generation/validation
|
|
expected_token = f"CONFIRM_{expected_action}_PERMANENT_DELETE"
|
|
if token != expected_token:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid confirmation token. Permanent deletion requires explicit confirmation."
|
|
)
|
|
|
|
|
|
@router.get("/deleted-shots/")
|
|
async def get_deleted_shots(
|
|
project_id: Optional[int] = Query(None, description="Filter by project ID"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Get list of deleted shots for admin recovery interface"""
|
|
recovery_service = RecoveryService()
|
|
deleted_shots = recovery_service.get_deleted_shots(project_id, db)
|
|
|
|
return [
|
|
{
|
|
"id": shot.id,
|
|
"name": shot.name,
|
|
"episode_name": shot.episode_name,
|
|
"project_id": shot.project_id,
|
|
"project_name": shot.project_name,
|
|
"deleted_at": shot.deleted_at,
|
|
"deleted_by": shot.deleted_by,
|
|
"deleted_by_name": shot.deleted_by_name,
|
|
"task_count": shot.task_count,
|
|
"submission_count": shot.submission_count,
|
|
"attachment_count": shot.attachment_count,
|
|
"note_count": shot.note_count,
|
|
"review_count": shot.review_count
|
|
}
|
|
for shot in deleted_shots
|
|
]
|
|
|
|
|
|
@router.get("/deleted-assets/")
|
|
async def get_deleted_assets(
|
|
project_id: Optional[int] = Query(None, description="Filter by project ID"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Get list of deleted assets for admin recovery interface"""
|
|
recovery_service = RecoveryService()
|
|
deleted_assets = recovery_service.get_deleted_assets(project_id, db)
|
|
|
|
return [
|
|
{
|
|
"id": asset.id,
|
|
"name": asset.name,
|
|
"category": asset.category,
|
|
"project_id": asset.project_id,
|
|
"project_name": asset.project_name,
|
|
"deleted_at": asset.deleted_at,
|
|
"deleted_by": asset.deleted_by,
|
|
"deleted_by_name": asset.deleted_by_name,
|
|
"task_count": asset.task_count,
|
|
"submission_count": asset.submission_count,
|
|
"attachment_count": asset.attachment_count,
|
|
"note_count": asset.note_count,
|
|
"review_count": asset.review_count
|
|
}
|
|
for asset in deleted_assets
|
|
]
|
|
|
|
|
|
@router.get("/shots/{shot_id}/recovery-preview")
|
|
async def get_shot_recovery_preview(
|
|
shot_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Get information about what will be recovered when restoring a shot"""
|
|
recovery_service = RecoveryService()
|
|
recovery_info = recovery_service.preview_shot_recovery(shot_id, db)
|
|
|
|
if not recovery_info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Deleted shot not found"
|
|
)
|
|
|
|
return {
|
|
"shot_id": recovery_info.shot_id,
|
|
"name": recovery_info.name,
|
|
"episode_name": recovery_info.episode_name,
|
|
"project_id": recovery_info.project_id,
|
|
"project_name": recovery_info.project_name,
|
|
"task_count": recovery_info.task_count,
|
|
"submission_count": recovery_info.submission_count,
|
|
"attachment_count": recovery_info.attachment_count,
|
|
"note_count": recovery_info.note_count,
|
|
"review_count": recovery_info.review_count,
|
|
"deleted_at": recovery_info.deleted_at,
|
|
"deleted_by": recovery_info.deleted_by,
|
|
"deleted_by_name": recovery_info.deleted_by_name,
|
|
"files_preserved": recovery_info.files_preserved,
|
|
"file_count": recovery_info.file_count
|
|
}
|
|
|
|
|
|
@router.get("/assets/{asset_id}/recovery-preview")
|
|
async def get_asset_recovery_preview(
|
|
asset_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Get information about what will be recovered when restoring an asset"""
|
|
recovery_service = RecoveryService()
|
|
recovery_info = recovery_service.preview_asset_recovery(asset_id, db)
|
|
|
|
if not recovery_info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Deleted asset not found"
|
|
)
|
|
|
|
return {
|
|
"asset_id": recovery_info.asset_id,
|
|
"name": recovery_info.name,
|
|
"project_name": recovery_info.project_name,
|
|
"task_count": recovery_info.task_count,
|
|
"submission_count": recovery_info.submission_count,
|
|
"attachment_count": recovery_info.attachment_count,
|
|
"note_count": recovery_info.note_count,
|
|
"review_count": recovery_info.review_count,
|
|
"deleted_at": recovery_info.deleted_at,
|
|
"deleted_by": recovery_info.deleted_by,
|
|
"deleted_by_name": recovery_info.deleted_by_name,
|
|
"files_preserved": recovery_info.files_preserved,
|
|
"file_count": recovery_info.file_count
|
|
}
|
|
|
|
|
|
@router.post("/shots/{shot_id}/recover")
|
|
async def recover_shot(
|
|
shot_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Recover a soft-deleted shot and all its related data"""
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.recover_shot(shot_id, db, current_user)
|
|
|
|
if not result.success:
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"message": "Failed to recover shot",
|
|
"errors": result.errors
|
|
}
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"message": f"Shot '{result.name}' and all related data have been recovered",
|
|
"shot_id": result.shot_id,
|
|
"name": result.name,
|
|
"recovered_at": result.recovered_at,
|
|
"recovered_by": result.recovered_by,
|
|
"recovered_tasks": result.recovered_tasks,
|
|
"recovered_submissions": result.recovered_submissions,
|
|
"recovered_attachments": result.recovered_attachments,
|
|
"recovered_notes": result.recovered_notes,
|
|
"recovered_reviews": result.recovered_reviews,
|
|
"operation_duration": result.operation_duration
|
|
}
|
|
|
|
|
|
@router.post("/assets/{asset_id}/recover")
|
|
async def recover_asset(
|
|
asset_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Recover a soft-deleted asset and all its related data"""
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.recover_asset(asset_id, db, current_user)
|
|
|
|
if not result.success:
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"message": "Failed to recover asset",
|
|
"errors": result.errors
|
|
}
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"message": f"Asset '{result.name}' and all related data have been recovered",
|
|
"asset_id": result.asset_id,
|
|
"name": result.name,
|
|
"recovered_at": result.recovered_at,
|
|
"recovered_by": result.recovered_by,
|
|
"recovered_tasks": result.recovered_tasks,
|
|
"recovered_submissions": result.recovered_submissions,
|
|
"recovered_attachments": result.recovered_attachments,
|
|
"recovered_notes": result.recovered_notes,
|
|
"recovered_reviews": result.recovered_reviews,
|
|
"operation_duration": result.operation_duration
|
|
}
|
|
|
|
|
|
@router.post("/shots/bulk-recover")
|
|
async def bulk_recover_shots(
|
|
request: BulkRecoveryRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Bulk recover multiple shots"""
|
|
if not request.shot_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No shot IDs provided"
|
|
)
|
|
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.bulk_recover_shots(request.shot_ids, db, current_user)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"total_items": result.total_items,
|
|
"successful_recoveries": result.successful_recoveries,
|
|
"failed_recoveries": result.failed_recoveries,
|
|
"results": [
|
|
{
|
|
"success": r.success,
|
|
"shot_id": r.shot_id,
|
|
"name": r.name,
|
|
"recovered_tasks": r.recovered_tasks,
|
|
"recovered_submissions": r.recovered_submissions,
|
|
"recovered_attachments": r.recovered_attachments,
|
|
"recovered_notes": r.recovered_notes,
|
|
"recovered_reviews": r.recovered_reviews,
|
|
"operation_duration": r.operation_duration,
|
|
"errors": r.errors,
|
|
"warnings": r.warnings
|
|
}
|
|
for r in result.results
|
|
],
|
|
"errors": [
|
|
{
|
|
"item_id": e.item_id,
|
|
"item_type": e.item_type,
|
|
"error": e.error
|
|
}
|
|
for e in result.errors
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("/assets/bulk-recover")
|
|
async def bulk_recover_assets(
|
|
request: BulkRecoveryRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Bulk recover multiple assets"""
|
|
if not request.asset_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No asset IDs provided"
|
|
)
|
|
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.bulk_recover_assets(request.asset_ids, db, current_user)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"total_items": result.total_items,
|
|
"successful_recoveries": result.successful_recoveries,
|
|
"failed_recoveries": result.failed_recoveries,
|
|
"results": [
|
|
{
|
|
"success": r.success,
|
|
"asset_id": r.asset_id,
|
|
"name": r.name,
|
|
"recovered_tasks": r.recovered_tasks,
|
|
"recovered_submissions": r.recovered_submissions,
|
|
"recovered_attachments": r.recovered_attachments,
|
|
"recovered_notes": r.recovered_notes,
|
|
"recovered_reviews": r.recovered_reviews,
|
|
"operation_duration": r.operation_duration,
|
|
"errors": r.errors,
|
|
"warnings": r.warnings
|
|
}
|
|
for r in result.results
|
|
],
|
|
"errors": [
|
|
{
|
|
"item_id": e.item_id,
|
|
"item_type": e.item_type,
|
|
"error": e.error
|
|
}
|
|
for e in result.errors
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/recovery-stats/")
|
|
async def get_recovery_stats(
|
|
project_id: Optional[int] = Query(None, description="Filter by project ID"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Get recovery statistics for admin dashboard"""
|
|
recovery_service = RecoveryService()
|
|
stats = recovery_service.get_recovery_stats(project_id, db)
|
|
|
|
return {
|
|
"deleted_shots_count": stats.deleted_shots_count,
|
|
"deleted_assets_count": stats.deleted_assets_count,
|
|
"total_deleted_tasks": stats.total_deleted_tasks,
|
|
"total_deleted_files": stats.total_deleted_files,
|
|
"oldest_deletion_date": stats.oldest_deletion_date
|
|
}
|
|
|
|
|
|
# Permanent Delete Endpoints
|
|
|
|
@router.delete("/shots/{shot_id}/permanent")
|
|
async def permanent_delete_shot(
|
|
shot_id: int,
|
|
request: PermanentDeleteRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Permanently delete a soft-deleted shot and all its related data"""
|
|
# Check rate limit
|
|
check_permanent_delete_rate_limit(current_user.id)
|
|
|
|
# Validate confirmation token
|
|
validate_confirmation_token(request.confirmation_token, "SHOT")
|
|
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.permanent_delete_shot(shot_id, db, current_user)
|
|
|
|
if not result.success:
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"message": "Failed to permanently delete shot",
|
|
"errors": result.errors,
|
|
"warnings": result.warnings
|
|
}
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"message": f"Shot '{result.name}' has been permanently deleted",
|
|
"shot_id": result.shot_id,
|
|
"name": result.name,
|
|
"deleted_at": result.deleted_at,
|
|
"deleted_by": result.deleted_by,
|
|
"deleted_tasks": result.deleted_tasks,
|
|
"deleted_submissions": result.deleted_submissions,
|
|
"deleted_attachments": result.deleted_attachments,
|
|
"deleted_notes": result.deleted_notes,
|
|
"deleted_reviews": result.deleted_reviews,
|
|
"deleted_files": result.deleted_files,
|
|
"database_records_deleted": result.database_records_deleted,
|
|
"operation_duration": result.operation_duration,
|
|
"warnings": result.warnings
|
|
}
|
|
|
|
|
|
@router.delete("/assets/{asset_id}/permanent")
|
|
async def permanent_delete_asset(
|
|
asset_id: int,
|
|
request: PermanentDeleteRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Permanently delete a soft-deleted asset and all its related data"""
|
|
# Check rate limit
|
|
check_permanent_delete_rate_limit(current_user.id)
|
|
|
|
# Validate confirmation token
|
|
validate_confirmation_token(request.confirmation_token, "ASSET")
|
|
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.permanent_delete_asset(asset_id, db, current_user)
|
|
|
|
if not result.success:
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"message": "Failed to permanently delete asset",
|
|
"errors": result.errors,
|
|
"warnings": result.warnings
|
|
}
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"message": f"Asset '{result.name}' has been permanently deleted",
|
|
"asset_id": result.asset_id,
|
|
"name": result.name,
|
|
"deleted_at": result.deleted_at,
|
|
"deleted_by": result.deleted_by,
|
|
"deleted_tasks": result.deleted_tasks,
|
|
"deleted_submissions": result.deleted_submissions,
|
|
"deleted_attachments": result.deleted_attachments,
|
|
"deleted_notes": result.deleted_notes,
|
|
"deleted_reviews": result.deleted_reviews,
|
|
"deleted_files": result.deleted_files,
|
|
"database_records_deleted": result.database_records_deleted,
|
|
"operation_duration": result.operation_duration,
|
|
"warnings": result.warnings
|
|
}
|
|
|
|
|
|
@router.delete("/shots/bulk-permanent")
|
|
async def bulk_permanent_delete_shots(
|
|
request: BulkPermanentDeleteRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Permanently delete multiple shots in bulk"""
|
|
if not request.shot_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No shot IDs provided"
|
|
)
|
|
|
|
# Check rate limit (bulk operations count as multiple operations)
|
|
for _ in request.shot_ids:
|
|
check_permanent_delete_rate_limit(current_user.id)
|
|
|
|
# Validate confirmation token
|
|
validate_confirmation_token(request.confirmation_token, "BULK_SHOTS")
|
|
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.bulk_permanent_delete_shots(request.shot_ids, db, current_user)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"message": f"Bulk permanent deletion completed: {result.successful_deletions} successful, {result.failed_deletions} failed",
|
|
"total_items": result.total_items,
|
|
"successful_deletions": result.successful_deletions,
|
|
"failed_deletions": result.failed_deletions,
|
|
"deleted_items": result.deleted_items,
|
|
"files_deleted": result.files_deleted,
|
|
"database_records_deleted": result.database_records_deleted,
|
|
"errors": result.errors
|
|
}
|
|
|
|
|
|
@router.delete("/assets/bulk-permanent")
|
|
async def bulk_permanent_delete_assets(
|
|
request: BulkPermanentDeleteRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Permanently delete multiple assets in bulk"""
|
|
if not request.asset_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No asset IDs provided"
|
|
)
|
|
|
|
# Check rate limit (bulk operations count as multiple operations)
|
|
for _ in request.asset_ids:
|
|
check_permanent_delete_rate_limit(current_user.id)
|
|
|
|
# Validate confirmation token
|
|
validate_confirmation_token(request.confirmation_token, "BULK_ASSETS")
|
|
|
|
recovery_service = RecoveryService()
|
|
result = recovery_service.bulk_permanent_delete_assets(request.asset_ids, db, current_user)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"message": f"Bulk permanent deletion completed: {result.successful_deletions} successful, {result.failed_deletions} failed",
|
|
"total_items": result.total_items,
|
|
"successful_deletions": result.successful_deletions,
|
|
"failed_deletions": result.failed_deletions,
|
|
"deleted_items": result.deleted_items,
|
|
"files_deleted": result.files_deleted,
|
|
"database_records_deleted": result.database_records_deleted,
|
|
"errors": result.errors
|
|
}
|
|
|
|
|
|
# Batch Operations Endpoints
|
|
|
|
@router.post("/batch-deletion-preview")
|
|
async def get_batch_deletion_preview(
|
|
request: BatchPreviewRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Get preview information for a batch deletion operation"""
|
|
if not request.shot_ids and not request.asset_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No shot or asset IDs provided"
|
|
)
|
|
|
|
batch_service = BatchOperationsService()
|
|
preview = batch_service.get_batch_deletion_preview(
|
|
request.shot_ids, request.asset_ids, db
|
|
)
|
|
|
|
return preview
|
|
|
|
|
|
@router.post("/shots/batch-delete")
|
|
async def batch_delete_shots(
|
|
request: BulkDeletionRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Batch delete multiple shots"""
|
|
if not request.shot_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No shot IDs provided"
|
|
)
|
|
|
|
batch_service = BatchOperationsService()
|
|
result = batch_service.batch_delete_shots(
|
|
request.shot_ids, db, current_user, request.batch_size or 50
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"total_items": result.total_items,
|
|
"successful_deletions": result.successful_deletions,
|
|
"failed_deletions": result.failed_deletions,
|
|
"operation_duration": result.operation_duration,
|
|
"total_deleted_tasks": result.total_deleted_tasks,
|
|
"total_deleted_submissions": result.total_deleted_submissions,
|
|
"total_deleted_attachments": result.total_deleted_attachments,
|
|
"total_deleted_notes": result.total_deleted_notes,
|
|
"total_deleted_reviews": result.total_deleted_reviews,
|
|
"items": [
|
|
{
|
|
"id": item.id,
|
|
"name": item.name,
|
|
"type": item.type,
|
|
"success": item.success,
|
|
"error": item.error,
|
|
"deleted_counts": item.deleted_counts
|
|
}
|
|
for item in result.items
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("/assets/batch-delete")
|
|
async def batch_delete_assets(
|
|
request: BulkDeletionRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Batch delete multiple assets"""
|
|
if not request.asset_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No asset IDs provided"
|
|
)
|
|
|
|
batch_service = BatchOperationsService()
|
|
result = batch_service.batch_delete_assets(
|
|
request.asset_ids, db, current_user, request.batch_size or 50
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"total_items": result.total_items,
|
|
"successful_deletions": result.successful_deletions,
|
|
"failed_deletions": result.failed_deletions,
|
|
"operation_duration": result.operation_duration,
|
|
"total_deleted_tasks": result.total_deleted_tasks,
|
|
"total_deleted_submissions": result.total_deleted_submissions,
|
|
"total_deleted_attachments": result.total_deleted_attachments,
|
|
"total_deleted_notes": result.total_deleted_notes,
|
|
"total_deleted_reviews": result.total_deleted_reviews,
|
|
"items": [
|
|
{
|
|
"id": item.id,
|
|
"name": item.name,
|
|
"type": item.type,
|
|
"success": item.success,
|
|
"error": item.error,
|
|
"deleted_counts": item.deleted_counts
|
|
}
|
|
for item in result.items
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("/shots/batch-recover")
|
|
async def batch_recover_shots_enhanced(
|
|
request: BulkRecoveryRequest,
|
|
batch_size: Optional[int] = Query(50, description="Batch size for processing"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Enhanced batch recover multiple shots with configurable batch size"""
|
|
if not request.shot_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No shot IDs provided"
|
|
)
|
|
|
|
batch_service = BatchOperationsService()
|
|
result = batch_service.batch_recover_shots(
|
|
request.shot_ids, db, current_user, batch_size
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"total_items": result.total_items,
|
|
"successful_recoveries": result.successful_recoveries,
|
|
"failed_recoveries": result.failed_recoveries,
|
|
"operation_duration": result.operation_duration,
|
|
"total_recovered_tasks": result.total_recovered_tasks,
|
|
"total_recovered_submissions": result.total_recovered_submissions,
|
|
"total_recovered_attachments": result.total_recovered_attachments,
|
|
"total_recovered_notes": result.total_recovered_notes,
|
|
"total_recovered_reviews": result.total_recovered_reviews,
|
|
"items": [
|
|
{
|
|
"id": item.id,
|
|
"name": item.name,
|
|
"type": item.type,
|
|
"success": item.success,
|
|
"error": item.error,
|
|
"recovered_counts": item.recovered_counts
|
|
}
|
|
for item in result.items
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("/assets/batch-recover")
|
|
async def batch_recover_assets_enhanced(
|
|
request: BulkRecoveryRequest,
|
|
batch_size: Optional[int] = Query(50, description="Batch size for processing"),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_admin)
|
|
):
|
|
"""Enhanced batch recover multiple assets with configurable batch size"""
|
|
if not request.asset_ids:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="No asset IDs provided"
|
|
)
|
|
|
|
batch_service = BatchOperationsService()
|
|
result = batch_service.batch_recover_assets(
|
|
request.asset_ids, db, current_user, batch_size
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"total_items": result.total_items,
|
|
"successful_recoveries": result.successful_recoveries,
|
|
"failed_recoveries": result.failed_recoveries,
|
|
"operation_duration": result.operation_duration,
|
|
"total_recovered_tasks": result.total_recovered_tasks,
|
|
"total_recovered_submissions": result.total_recovered_submissions,
|
|
"total_recovered_attachments": result.total_recovered_attachments,
|
|
"total_recovered_notes": result.total_recovered_notes,
|
|
"total_recovered_reviews": result.total_recovered_reviews,
|
|
"items": [
|
|
{
|
|
"id": item.id,
|
|
"name": item.name,
|
|
"type": item.type,
|
|
"success": item.success,
|
|
"error": item.error,
|
|
"recovered_counts": item.recovered_counts
|
|
}
|
|
for item in result.items
|
|
]
|
|
} |