742 lines
26 KiB
Python
742 lines
26 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from typing import List, Dict
|
|
|
|
from database import get_db
|
|
from models.asset import Asset, AssetCategory
|
|
from models.project import Project, ProjectMember
|
|
from models.task import Task, TaskType, TaskStatus
|
|
from models.user import User, UserRole
|
|
from schemas.asset import AssetCreate, AssetUpdate, AssetResponse, AssetListResponse, TaskStatusInfo
|
|
from schemas.task import TaskCreate
|
|
from utils.auth import get_current_user_from_token
|
|
from services.asset_soft_deletion import AssetSoftDeletionService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def get_current_user_with_db(
|
|
token_data: dict = Depends(get_current_user_from_token),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get current user with proper database dependency."""
|
|
from utils.auth import _get_user_from_db
|
|
return _get_user_from_db(db, token_data["user_id"])
|
|
|
|
|
|
def require_coordinator_or_admin(
|
|
token_data: dict = Depends(get_current_user_from_token),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Require coordinator or admin role."""
|
|
from utils.auth import _get_user_from_db
|
|
current_user = _get_user_from_db(db, token_data["user_id"])
|
|
|
|
if current_user.role != UserRole.COORDINATOR and not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Insufficient permissions"
|
|
)
|
|
return current_user
|
|
|
|
|
|
def get_status_sort_order(status: str, project_custom_statuses: list = None) -> int:
|
|
"""Get sort order for task status, including custom statuses."""
|
|
# Default system status order
|
|
system_status_order = {
|
|
"not_started": 0,
|
|
"in_progress": 1,
|
|
"submitted": 2,
|
|
"retake": 3,
|
|
"approved": 4
|
|
}
|
|
|
|
# If it's a system status, return its order
|
|
if status in system_status_order:
|
|
return system_status_order[status]
|
|
|
|
# For custom statuses, use their defined order + offset to place them after system statuses
|
|
if project_custom_statuses:
|
|
for custom_status in project_custom_statuses:
|
|
if isinstance(custom_status, dict) and custom_status.get('id') == status:
|
|
# Custom statuses start after system statuses (5+)
|
|
return 5 + custom_status.get('order', 0)
|
|
|
|
# Unknown status defaults to 0 (same as not_started)
|
|
return 0
|
|
|
|
|
|
def get_project_custom_statuses(project_id: int, db: Session) -> list:
|
|
"""Get custom task statuses for a project."""
|
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
if not project or not project.custom_task_statuses:
|
|
return []
|
|
|
|
custom_statuses_data = project.custom_task_statuses
|
|
if isinstance(custom_statuses_data, str):
|
|
try:
|
|
import json
|
|
custom_statuses_data = json.loads(custom_statuses_data)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return []
|
|
|
|
return custom_statuses_data if isinstance(custom_statuses_data, list) else []
|
|
|
|
|
|
def check_project_access(project_id: int, current_user: User, db: Session):
|
|
"""Check if user has access to the project."""
|
|
# Check if project exists
|
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
if not project:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Project not found"
|
|
)
|
|
|
|
# Check access for artists
|
|
if current_user.role == UserRole.ARTIST:
|
|
member = db.query(ProjectMember).filter(
|
|
ProjectMember.project_id == project_id,
|
|
ProjectMember.user_id == current_user.id
|
|
).first()
|
|
if not member:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to this project"
|
|
)
|
|
|
|
return project
|
|
|
|
|
|
# Standard asset task types (read-only)
|
|
STANDARD_ASSET_TASK_TYPES = ["modeling", "surfacing", "rigging"]
|
|
|
|
# Default asset tasks by category (using string values instead of enums)
|
|
DEFAULT_ASSET_TASKS = {
|
|
AssetCategory.CHARACTERS: [TaskType.MODELING.value, TaskType.SURFACING.value, TaskType.RIGGING.value],
|
|
AssetCategory.PROPS: [TaskType.MODELING.value, TaskType.SURFACING.value],
|
|
AssetCategory.SETS: [TaskType.MODELING.value, TaskType.SURFACING.value],
|
|
AssetCategory.VEHICLES: [TaskType.MODELING.value, TaskType.SURFACING.value, TaskType.RIGGING.value]
|
|
}
|
|
|
|
|
|
def get_default_asset_task_types(category: AssetCategory) -> List[str]:
|
|
"""Get default task types for an asset category."""
|
|
return DEFAULT_ASSET_TASKS.get(category, [])
|
|
|
|
|
|
def get_all_asset_task_types(project_id: int, db: Session) -> List[str]:
|
|
"""Get all task types (standard + custom) for assets in a project."""
|
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
if not project:
|
|
return STANDARD_ASSET_TASK_TYPES
|
|
|
|
custom_types = project.custom_asset_task_types or []
|
|
return STANDARD_ASSET_TASK_TYPES + custom_types
|
|
|
|
|
|
def create_default_tasks_for_asset(asset: Asset, task_types: List[str], db: Session) -> List[Task]:
|
|
"""Create default tasks for an asset."""
|
|
created_tasks = []
|
|
|
|
for task_type in task_types:
|
|
# Create task name based on type
|
|
task_name = f"{asset.name} - {task_type.title()}"
|
|
|
|
# Create the task
|
|
db_task = Task(
|
|
project_id=asset.project_id,
|
|
asset_id=asset.id,
|
|
task_type=task_type,
|
|
name=task_name,
|
|
description=f"Default {task_type} task for {asset.name}",
|
|
status="not_started"
|
|
)
|
|
|
|
db.add(db_task)
|
|
created_tasks.append(db_task)
|
|
|
|
return created_tasks
|
|
|
|
|
|
@router.get("/categories", response_model=List[str])
|
|
async def list_asset_categories():
|
|
"""List all available asset categories"""
|
|
return [category.value for category in AssetCategory]
|
|
|
|
|
|
@router.get("/default-tasks/{category}", response_model=List[str])
|
|
async def get_default_tasks_for_category(
|
|
category: AssetCategory,
|
|
project_id: int = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_with_db)
|
|
):
|
|
"""Get default task types for an asset category (includes custom types if project_id provided)"""
|
|
task_types = get_default_asset_task_types(category)
|
|
|
|
# If project_id is provided, include custom task types
|
|
if project_id:
|
|
all_types = get_all_asset_task_types(project_id, db)
|
|
# Return only the types that are relevant for this category
|
|
# For now, return all available types (standard + custom)
|
|
return all_types
|
|
|
|
return task_types # task_types are already strings, no need for .value
|
|
|
|
|
|
@router.get("/", response_model=List[AssetListResponse])
|
|
async def list_assets(
|
|
project_id: int = None,
|
|
category: AssetCategory = None,
|
|
task_status_filter: str = None,
|
|
sort_by: str = None,
|
|
sort_direction: str = "asc",
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_with_db)
|
|
):
|
|
"""List assets with optional filtering by project and category"""
|
|
from sqlalchemy.orm import joinedload, selectinload
|
|
|
|
# Build base query for assets (exclude soft deleted)
|
|
base_query = db.query(Asset).filter(Asset.deleted_at.is_(None))
|
|
|
|
# Filter by project if specified
|
|
if project_id:
|
|
check_project_access(project_id, current_user, db)
|
|
base_query = base_query.filter(Asset.project_id == project_id)
|
|
else:
|
|
# If no project specified, filter by user's accessible projects for artists
|
|
if current_user.role == UserRole.ARTIST:
|
|
accessible_projects = db.query(ProjectMember.project_id).filter(
|
|
ProjectMember.user_id == current_user.id
|
|
).subquery()
|
|
base_query = base_query.filter(Asset.project_id.in_(accessible_projects))
|
|
|
|
# Filter by category if specified
|
|
if category:
|
|
base_query = base_query.filter(Asset.category == category)
|
|
|
|
# Apply sorting if specified (for non-task-status fields)
|
|
if sort_by and not sort_by.endswith('_status'):
|
|
if sort_by in ['name', 'category', 'status', 'created_at', 'updated_at']:
|
|
sort_column = getattr(Asset, sort_by)
|
|
if sort_direction.lower() == 'desc':
|
|
base_query = base_query.order_by(sort_column.desc())
|
|
else:
|
|
base_query = base_query.order_by(sort_column.asc())
|
|
|
|
# OPTIMIZATION: Use single query with optimized JOIN to fetch assets and their tasks
|
|
# This replaces the N+1 query pattern with a single database operation
|
|
assets_with_tasks = (
|
|
base_query
|
|
.outerjoin(Task, (Task.asset_id == Asset.id) & (Task.deleted_at.is_(None)))
|
|
.options(
|
|
joinedload(Asset.project), # Eager load project
|
|
selectinload(Asset.tasks).options( # Use selectinload for better performance with tasks
|
|
selectinload(Task.assigned_user) # Eager load assigned users
|
|
)
|
|
)
|
|
.add_columns(
|
|
Task.id.label('task_id'),
|
|
Task.task_type,
|
|
Task.status.label('task_status'),
|
|
Task.assigned_user_id,
|
|
Task.updated_at.label('task_updated_at') # Include task update time for better tracking
|
|
)
|
|
.offset(skip)
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
# OPTIMIZATION: Pre-fetch all project data and task types in a single query
|
|
# This eliminates the need for repeated project queries
|
|
project_ids = set()
|
|
for row in assets_with_tasks:
|
|
asset = row[0]
|
|
if asset.project_id not in project_ids:
|
|
project_ids.add(asset.project_id)
|
|
|
|
# Get all projects with their custom task types in one optimized query
|
|
project_data = {}
|
|
if project_ids:
|
|
projects = (
|
|
db.query(Project)
|
|
.filter(Project.id.in_(project_ids))
|
|
.all()
|
|
)
|
|
for project in projects:
|
|
custom_types = project.custom_asset_task_types or []
|
|
project_data[project.id] = {
|
|
'task_types': STANDARD_ASSET_TASK_TYPES + custom_types,
|
|
'custom_statuses': get_project_custom_statuses(project.id, db)
|
|
}
|
|
|
|
# OPTIMIZATION: Group results by asset and aggregate task data efficiently
|
|
assets_dict = {}
|
|
for row in assets_with_tasks:
|
|
asset = row[0] # Asset object
|
|
task_id = row[1] # task_id
|
|
task_type = row[2] # task_type
|
|
task_status = row[3] # task_status
|
|
assigned_user_id = row[4] # assigned_user_id
|
|
task_updated_at = row[5] # task_updated_at
|
|
|
|
if asset.id not in assets_dict:
|
|
# Initialize asset data with pre-fetched project data
|
|
project_info = project_data.get(asset.project_id, {
|
|
'task_types': STANDARD_ASSET_TASK_TYPES,
|
|
'custom_statuses': []
|
|
})
|
|
|
|
assets_dict[asset.id] = {
|
|
'asset': asset,
|
|
'tasks': [],
|
|
'task_status': {},
|
|
'task_details': [],
|
|
'project_info': project_info
|
|
}
|
|
|
|
# Initialize all task types as not started using pre-fetched data
|
|
for task_type_init in project_info['task_types']:
|
|
assets_dict[asset.id]['task_status'][task_type_init] = "not_started"
|
|
|
|
# Add task data if task exists
|
|
if task_id is not None:
|
|
assets_dict[asset.id]['tasks'].append({
|
|
'task_id': task_id,
|
|
'task_type': task_type,
|
|
'status': task_status,
|
|
'assigned_user_id': assigned_user_id,
|
|
'updated_at': task_updated_at
|
|
})
|
|
|
|
# Update task status
|
|
assets_dict[asset.id]['task_status'][task_type] = task_status
|
|
|
|
# Add to task details with enhanced information
|
|
assets_dict[asset.id]['task_details'].append(TaskStatusInfo(
|
|
task_type=task_type,
|
|
status=task_status,
|
|
task_id=task_id,
|
|
assigned_user_id=assigned_user_id
|
|
))
|
|
|
|
# Build response list efficiently
|
|
result = []
|
|
for asset_data in assets_dict.values():
|
|
asset = asset_data['asset']
|
|
|
|
# Create asset response with optimized data
|
|
asset_response = AssetListResponse.model_validate(asset)
|
|
asset_response.task_count = len(asset_data['tasks'])
|
|
asset_response.task_status = asset_data['task_status']
|
|
asset_response.task_details = asset_data['task_details']
|
|
|
|
result.append(asset_response)
|
|
|
|
# Apply task status filtering if specified
|
|
if task_status_filter:
|
|
try:
|
|
# Parse task status filter (format: "task_type:status")
|
|
task_type, status = task_status_filter.split(":")
|
|
filter_status = status # Use string directly instead of enum
|
|
result = [
|
|
asset for asset in result
|
|
if asset.task_status.get(task_type) == filter_status
|
|
]
|
|
except (ValueError, KeyError):
|
|
# Invalid filter format, ignore
|
|
pass
|
|
|
|
# Apply task status sorting if specified
|
|
if sort_by and sort_by.endswith('_status'):
|
|
task_type = sort_by.replace('_status', '')
|
|
|
|
# Get custom statuses for proper sorting using pre-fetched data
|
|
def get_status_order(asset):
|
|
status = asset.task_status.get(task_type, "not_started")
|
|
# Use pre-fetched custom statuses from project_data
|
|
asset_project_data = project_data.get(getattr(asset, 'project_id', None), {})
|
|
custom_statuses = asset_project_data.get('custom_statuses', [])
|
|
return get_status_sort_order(status, custom_statuses)
|
|
|
|
reverse = sort_direction.lower() == 'desc'
|
|
result.sort(key=get_status_order, reverse=reverse)
|
|
|
|
return result
|
|
|
|
|
|
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_asset(
|
|
asset: AssetCreate,
|
|
project_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Create a new asset in a project with optional default tasks"""
|
|
# Check project access
|
|
check_project_access(project_id, current_user, db)
|
|
|
|
# Check if asset name already exists in project (exclude soft deleted)
|
|
existing_asset = db.query(Asset).filter(
|
|
Asset.project_id == project_id,
|
|
Asset.name == asset.name,
|
|
Asset.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if existing_asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Asset with this name already exists in the project"
|
|
)
|
|
|
|
# Create new asset (exclude fields that don't belong to Asset model)
|
|
asset_data = asset.model_dump(exclude={'create_default_tasks', 'selected_task_types'})
|
|
db_asset = Asset(
|
|
project_id=project_id,
|
|
**asset_data
|
|
)
|
|
|
|
db.add(db_asset)
|
|
db.flush() # Flush to get the asset ID
|
|
|
|
# Create default tasks if requested
|
|
task_count = 0
|
|
if asset.create_default_tasks:
|
|
# Determine which task types to create
|
|
if asset.selected_task_types:
|
|
# Use the selected task types (already strings, can include custom types)
|
|
task_types = asset.selected_task_types
|
|
|
|
# Validate that all selected task types are valid (standard or custom)
|
|
all_valid_types = get_all_asset_task_types(project_id, db)
|
|
invalid_types = [t for t in task_types if t not in all_valid_types]
|
|
if invalid_types:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid task types: {', '.join(invalid_types)}"
|
|
)
|
|
else:
|
|
# Use default task types for the asset category
|
|
task_types = get_default_asset_task_types(asset.category)
|
|
|
|
# Create the tasks
|
|
created_tasks = create_default_tasks_for_asset(db_asset, task_types, db)
|
|
task_count = len(created_tasks)
|
|
|
|
db.commit()
|
|
db.refresh(db_asset)
|
|
|
|
# Add task count
|
|
asset_data = AssetResponse.model_validate(db_asset)
|
|
asset_data.task_count = task_count
|
|
|
|
return asset_data
|
|
|
|
|
|
@router.get("/{asset_id}", response_model=AssetResponse)
|
|
async def get_asset(
|
|
asset_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_with_db)
|
|
):
|
|
"""Get a specific asset by ID"""
|
|
from sqlalchemy.orm import joinedload, selectinload
|
|
|
|
# OPTIMIZATION: Use single query with optimized JOINs to fetch asset and all related data
|
|
# This replaces separate queries with a single database operation
|
|
asset_query = (
|
|
db.query(Asset)
|
|
.options(
|
|
joinedload(Asset.project), # Eager load project
|
|
selectinload(Asset.tasks).options( # Use selectinload for better performance with tasks
|
|
selectinload(Task.assigned_user) # Eager load assigned users if needed
|
|
)
|
|
)
|
|
.filter(Asset.id == asset_id, Asset.deleted_at.is_(None))
|
|
)
|
|
|
|
asset = asset_query.first()
|
|
|
|
if not asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Asset not found"
|
|
)
|
|
|
|
# Check project access
|
|
check_project_access(asset.project_id, current_user, db)
|
|
|
|
# OPTIMIZATION: Count tasks from the already loaded relationship
|
|
# This avoids a separate COUNT query
|
|
active_tasks = [task for task in asset.tasks if task.deleted_at is None]
|
|
task_count = len(active_tasks)
|
|
|
|
asset_data = AssetResponse.model_validate(asset)
|
|
asset_data.task_count = task_count
|
|
|
|
return asset_data
|
|
|
|
|
|
@router.get("/{asset_id}/task-status", response_model=List[TaskStatusInfo])
|
|
async def get_asset_task_status(
|
|
asset_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_with_db)
|
|
):
|
|
"""Get detailed task status for a specific asset"""
|
|
# Exclude soft deleted assets
|
|
asset = db.query(Asset).filter(
|
|
Asset.id == asset_id,
|
|
Asset.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if not asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Asset not found"
|
|
)
|
|
|
|
# Check project access
|
|
check_project_access(asset.project_id, current_user, db)
|
|
|
|
# Get all active tasks for this asset (exclude soft deleted)
|
|
tasks = db.query(Task).filter(
|
|
Task.asset_id == asset.id,
|
|
Task.deleted_at.is_(None)
|
|
).all()
|
|
|
|
# Build detailed task status information
|
|
task_details = []
|
|
for task in tasks:
|
|
task_details.append(TaskStatusInfo(
|
|
task_type=task.task_type,
|
|
status=task.status,
|
|
task_id=task.id,
|
|
assigned_user_id=task.assigned_user_id
|
|
))
|
|
|
|
return task_details
|
|
|
|
|
|
@router.post("/{asset_id}/tasks", response_model=TaskStatusInfo, status_code=status.HTTP_201_CREATED)
|
|
async def create_asset_task(
|
|
asset_id: int,
|
|
task_type: str, # Changed from TaskType enum to str
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Create a new task for an asset"""
|
|
# Exclude soft deleted assets
|
|
asset = db.query(Asset).filter(
|
|
Asset.id == asset_id,
|
|
Asset.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if not asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Asset not found"
|
|
)
|
|
|
|
# Check project access
|
|
check_project_access(asset.project_id, current_user, db)
|
|
|
|
# Check if task already exists (exclude soft deleted)
|
|
existing_task = db.query(Task).filter(
|
|
Task.asset_id == asset_id,
|
|
Task.task_type == task_type,
|
|
Task.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if existing_task:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Task already exists for this asset and task type"
|
|
)
|
|
|
|
# Create the task
|
|
task_name = f"{asset.name} - {task_type.title()}"
|
|
db_task = Task(
|
|
project_id=asset.project_id,
|
|
asset_id=asset.id,
|
|
task_type=task_type,
|
|
name=task_name,
|
|
description=f"{task_type.title()} task for {asset.name}",
|
|
status="not_started"
|
|
)
|
|
|
|
db.add(db_task)
|
|
db.commit()
|
|
db.refresh(db_task)
|
|
|
|
return TaskStatusInfo(
|
|
task_type=db_task.task_type,
|
|
status=db_task.status,
|
|
task_id=db_task.id,
|
|
assigned_user_id=db_task.assigned_user_id
|
|
)
|
|
|
|
|
|
@router.put("/{asset_id}", response_model=AssetResponse)
|
|
async def update_asset(
|
|
asset_id: int,
|
|
asset_update: AssetUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Update an asset"""
|
|
# Exclude soft deleted assets
|
|
db_asset = db.query(Asset).filter(
|
|
Asset.id == asset_id,
|
|
Asset.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if not db_asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Asset not found"
|
|
)
|
|
|
|
# Check project access
|
|
check_project_access(db_asset.project_id, current_user, db)
|
|
|
|
# Check if new name conflicts with existing assets in the same project
|
|
if asset_update.name and asset_update.name != db_asset.name:
|
|
existing_asset = db.query(Asset).filter(
|
|
Asset.project_id == db_asset.project_id,
|
|
Asset.name == asset_update.name,
|
|
Asset.id != asset_id,
|
|
Asset.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if existing_asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Asset with this name already exists in the project"
|
|
)
|
|
|
|
# Update only provided fields
|
|
update_data = asset_update.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(db_asset, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(db_asset)
|
|
|
|
# Add task count (exclude soft deleted tasks)
|
|
task_count = db.query(Task).filter(
|
|
Task.asset_id == db_asset.id,
|
|
Task.deleted_at.is_(None)
|
|
).count()
|
|
asset_data = AssetResponse.model_validate(db_asset)
|
|
asset_data.task_count = task_count
|
|
|
|
return asset_data
|
|
|
|
|
|
@router.get("/{asset_id}/deletion-info")
|
|
async def get_asset_deletion_info(
|
|
asset_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Get information about what will be deleted when deleting an asset"""
|
|
# Exclude soft deleted assets
|
|
db_asset = db.query(Asset).filter(
|
|
Asset.id == asset_id,
|
|
Asset.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if not db_asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Asset not found"
|
|
)
|
|
|
|
# Check project access
|
|
check_project_access(db_asset.project_id, current_user, db)
|
|
|
|
# Use the soft deletion service to get comprehensive deletion info
|
|
deletion_service = AssetSoftDeletionService()
|
|
deletion_info = deletion_service.get_deletion_info(asset_id, db)
|
|
|
|
if not deletion_info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Asset not found"
|
|
)
|
|
|
|
return {
|
|
"asset_id": deletion_info.asset_id,
|
|
"asset_name": deletion_info.asset_name,
|
|
"asset_category": deletion_info.asset_category,
|
|
"project_name": deletion_info.project_name,
|
|
"task_count": deletion_info.task_count,
|
|
"submission_count": deletion_info.submission_count,
|
|
"attachment_count": deletion_info.attachment_count,
|
|
"note_count": deletion_info.note_count,
|
|
"review_count": deletion_info.review_count,
|
|
"total_file_size": deletion_info.total_file_size,
|
|
"file_count": deletion_info.file_count,
|
|
"affected_users": deletion_info.affected_users,
|
|
"last_activity_date": deletion_info.last_activity_date,
|
|
"created_at": deletion_info.created_at
|
|
}
|
|
|
|
|
|
@router.delete("/{asset_id}")
|
|
async def delete_asset(
|
|
asset_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Soft delete an asset and all its associated data"""
|
|
# Exclude soft deleted assets
|
|
db_asset = db.query(Asset).filter(
|
|
Asset.id == asset_id,
|
|
Asset.deleted_at.is_(None)
|
|
).first()
|
|
|
|
if not db_asset:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Asset not found"
|
|
)
|
|
|
|
# Check project access
|
|
check_project_access(db_asset.project_id, current_user, db)
|
|
|
|
# Use the soft deletion service to perform cascading soft deletion
|
|
deletion_service = AssetSoftDeletionService()
|
|
result = deletion_service.soft_delete_asset_cascade(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 delete asset",
|
|
"errors": result.errors
|
|
}
|
|
)
|
|
|
|
# Commit the transaction
|
|
db.commit()
|
|
|
|
return {
|
|
"message": f"Asset '{result.asset_name}' and all related data have been deleted",
|
|
"asset_id": result.asset_id,
|
|
"asset_name": result.asset_name,
|
|
"deleted_at": result.deleted_at,
|
|
"deleted_by": result.deleted_by,
|
|
"marked_deleted_tasks": result.marked_deleted_tasks,
|
|
"marked_deleted_submissions": result.marked_deleted_submissions,
|
|
"marked_deleted_attachments": result.marked_deleted_attachments,
|
|
"marked_deleted_notes": result.marked_deleted_notes,
|
|
"marked_deleted_reviews": result.marked_deleted_reviews,
|
|
"operation_duration": result.operation_duration
|
|
} |