| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- import logging
- from fastapi import APIRouter, Depends, HTTPException
- from sqlalchemy.ext.asyncio import AsyncSession
- from sqlalchemy import select, func
- from backend.app.core.database import get_db
- from backend.app.models.project import Project
- from backend.app.models.archive import PrintArchive
- from backend.app.models.print_queue import PrintQueueItem
- from backend.app.schemas.project import (
- ProjectCreate,
- ProjectUpdate,
- ProjectResponse,
- ProjectListResponse,
- ProjectStats,
- BatchAddArchives,
- BatchAddQueueItems,
- ArchivePreview,
- )
- logger = logging.getLogger(__name__)
- router = APIRouter(prefix="/projects", tags=["projects"])
- async def compute_project_stats(
- db: AsyncSession, project_id: int, target_count: int | None = None
- ) -> ProjectStats:
- """Compute statistics for a project."""
- # Count total archives
- total_result = await db.execute(
- select(func.count(PrintArchive.id)).where(PrintArchive.project_id == project_id)
- )
- total_archives = total_result.scalar() or 0
- # Count completed archives
- completed_result = await db.execute(
- select(func.count(PrintArchive.id)).where(
- PrintArchive.project_id == project_id,
- PrintArchive.status == "completed"
- )
- )
- completed_prints = completed_result.scalar() or 0
- # Count failed archives
- failed_result = await db.execute(
- select(func.count(PrintArchive.id)).where(
- PrintArchive.project_id == project_id,
- PrintArchive.status == "failed"
- )
- )
- failed_prints = failed_result.scalar() or 0
- # Sum print time and filament
- sums_result = await db.execute(
- select(
- func.coalesce(func.sum(PrintArchive.print_time_seconds), 0).label("total_time"),
- func.coalesce(func.sum(PrintArchive.filament_used_grams), 0).label("total_filament"),
- ).where(PrintArchive.project_id == project_id)
- )
- sums = sums_result.first()
- # Count queued items
- queued_result = await db.execute(
- select(func.count(PrintQueueItem.id)).where(
- PrintQueueItem.project_id == project_id,
- PrintQueueItem.status == "pending"
- )
- )
- queued_prints = queued_result.scalar() or 0
- # Count in-progress items
- in_progress_result = await db.execute(
- select(func.count(PrintQueueItem.id)).where(
- PrintQueueItem.project_id == project_id,
- PrintQueueItem.status == "printing"
- )
- )
- in_progress_prints = in_progress_result.scalar() or 0
- # Calculate progress
- progress_percent = None
- if target_count and target_count > 0:
- progress_percent = round((completed_prints / target_count) * 100, 1)
- return ProjectStats(
- total_archives=total_archives,
- completed_prints=completed_prints,
- failed_prints=failed_prints,
- queued_prints=queued_prints,
- in_progress_prints=in_progress_prints,
- total_print_time_hours=round((sums.total_time or 0) / 3600, 2),
- total_filament_grams=round(sums.total_filament or 0, 2),
- progress_percent=progress_percent,
- )
- @router.get("", response_model=list[ProjectListResponse])
- @router.get("/", response_model=list[ProjectListResponse])
- async def list_projects(
- status: str | None = None,
- db: AsyncSession = Depends(get_db),
- ):
- """List all projects with basic stats."""
- query = select(Project)
- if status:
- query = query.where(Project.status == status)
- query = query.order_by(Project.updated_at.desc())
- result = await db.execute(query)
- projects = result.scalars().all()
- # Compute quick stats for each project
- response = []
- for project in projects:
- # Get archive count
- archive_count_result = await db.execute(
- select(func.count(PrintArchive.id)).where(
- PrintArchive.project_id == project.id
- )
- )
- archive_count = archive_count_result.scalar() or 0
- # Get queue count
- queue_count_result = await db.execute(
- select(func.count(PrintQueueItem.id)).where(
- PrintQueueItem.project_id == project.id,
- PrintQueueItem.status.in_(["pending", "printing"]),
- )
- )
- queue_count = queue_count_result.scalar() or 0
- # Get completed count for progress
- completed_result = await db.execute(
- select(func.count(PrintArchive.id)).where(
- PrintArchive.project_id == project.id,
- PrintArchive.status == "completed",
- )
- )
- completed_count = completed_result.scalar() or 0
- progress_percent = None
- if project.target_count and project.target_count > 0:
- progress_percent = round((completed_count / project.target_count) * 100, 1)
- # Get archive previews (up to 6 most recent)
- archives_result = await db.execute(
- select(PrintArchive)
- .where(PrintArchive.project_id == project.id)
- .order_by(PrintArchive.created_at.desc())
- .limit(6)
- )
- archives = archives_result.scalars().all()
- archive_previews = [
- ArchivePreview(
- id=a.id,
- print_name=a.print_name,
- thumbnail_path=a.thumbnail_path,
- status=a.status,
- )
- for a in archives
- ]
- response.append(
- ProjectListResponse(
- id=project.id,
- name=project.name,
- description=project.description,
- color=project.color,
- status=project.status,
- target_count=project.target_count,
- created_at=project.created_at,
- archive_count=archive_count,
- queue_count=queue_count,
- progress_percent=progress_percent,
- archives=archive_previews,
- )
- )
- return response
- @router.post("/", response_model=ProjectResponse)
- async def create_project(
- data: ProjectCreate,
- db: AsyncSession = Depends(get_db),
- ):
- """Create a new project."""
- project = Project(
- name=data.name,
- description=data.description,
- color=data.color,
- target_count=data.target_count,
- )
- db.add(project)
- await db.flush()
- await db.refresh(project)
- stats = await compute_project_stats(db, project.id, project.target_count)
- return ProjectResponse(
- id=project.id,
- name=project.name,
- description=project.description,
- color=project.color,
- status=project.status,
- target_count=project.target_count,
- created_at=project.created_at,
- updated_at=project.updated_at,
- stats=stats,
- )
- @router.get("/{project_id}", response_model=ProjectResponse)
- async def get_project(
- project_id: int,
- db: AsyncSession = Depends(get_db),
- ):
- """Get a project by ID with detailed stats."""
- result = await db.execute(select(Project).where(Project.id == project_id))
- project = result.scalar_one_or_none()
- if not project:
- raise HTTPException(status_code=404, detail="Project not found")
- stats = await compute_project_stats(db, project.id, project.target_count)
- return ProjectResponse(
- id=project.id,
- name=project.name,
- description=project.description,
- color=project.color,
- status=project.status,
- target_count=project.target_count,
- created_at=project.created_at,
- updated_at=project.updated_at,
- stats=stats,
- )
- @router.patch("/{project_id}", response_model=ProjectResponse)
- async def update_project(
- project_id: int,
- data: ProjectUpdate,
- db: AsyncSession = Depends(get_db),
- ):
- """Update a project."""
- result = await db.execute(select(Project).where(Project.id == project_id))
- project = result.scalar_one_or_none()
- if not project:
- raise HTTPException(status_code=404, detail="Project not found")
- # Update fields if provided
- if data.name is not None:
- project.name = data.name
- if data.description is not None:
- project.description = data.description
- if data.color is not None:
- project.color = data.color
- if data.status is not None:
- if data.status not in ["active", "completed", "archived"]:
- raise HTTPException(status_code=400, detail="Invalid status")
- project.status = data.status
- if data.target_count is not None:
- project.target_count = data.target_count
- await db.flush()
- await db.refresh(project)
- stats = await compute_project_stats(db, project.id, project.target_count)
- return ProjectResponse(
- id=project.id,
- name=project.name,
- description=project.description,
- color=project.color,
- status=project.status,
- target_count=project.target_count,
- created_at=project.created_at,
- updated_at=project.updated_at,
- stats=stats,
- )
- @router.delete("/{project_id}")
- async def delete_project(
- project_id: int,
- db: AsyncSession = Depends(get_db),
- ):
- """Delete a project. Archives and queue items will have project_id set to NULL."""
- result = await db.execute(select(Project).where(Project.id == project_id))
- project = result.scalar_one_or_none()
- if not project:
- raise HTTPException(status_code=404, detail="Project not found")
- await db.delete(project)
- return {"message": "Project deleted"}
- @router.get("/{project_id}/archives")
- async def list_project_archives(
- project_id: int,
- limit: int = 100,
- offset: int = 0,
- db: AsyncSession = Depends(get_db),
- ):
- """List archives in a project."""
- # Verify project exists
- result = await db.execute(select(Project).where(Project.id == project_id))
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=404, detail="Project not found")
- # Get archives
- query = (
- select(PrintArchive)
- .where(PrintArchive.project_id == project_id)
- .order_by(PrintArchive.created_at.desc())
- .limit(limit)
- .offset(offset)
- )
- result = await db.execute(query)
- archives = result.scalars().all()
- # Import the response converter from archives module
- from backend.app.api.routes.archives import archive_to_response
- return [archive_to_response(a) for a in archives]
- @router.get("/{project_id}/queue")
- async def list_project_queue(
- project_id: int,
- db: AsyncSession = Depends(get_db),
- ):
- """List queue items in a project."""
- # Verify project exists
- result = await db.execute(select(Project).where(Project.id == project_id))
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=404, detail="Project not found")
- # Get queue items
- query = (
- select(PrintQueueItem)
- .where(PrintQueueItem.project_id == project_id)
- .order_by(PrintQueueItem.position)
- )
- result = await db.execute(query)
- items = result.scalars().all()
- return items
- @router.post("/{project_id}/add-archives")
- async def add_archives_to_project(
- project_id: int,
- data: BatchAddArchives,
- db: AsyncSession = Depends(get_db),
- ):
- """Batch add archives to a project."""
- # Verify project exists
- result = await db.execute(select(Project).where(Project.id == project_id))
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=404, detail="Project not found")
- # Update archives
- updated = 0
- for archive_id in data.archive_ids:
- result = await db.execute(
- select(PrintArchive).where(PrintArchive.id == archive_id)
- )
- archive = result.scalar_one_or_none()
- if archive:
- archive.project_id = project_id
- updated += 1
- return {"message": f"Added {updated} archives to project"}
- @router.post("/{project_id}/add-queue")
- async def add_queue_items_to_project(
- project_id: int,
- data: BatchAddQueueItems,
- db: AsyncSession = Depends(get_db),
- ):
- """Batch add queue items to a project."""
- # Verify project exists
- result = await db.execute(select(Project).where(Project.id == project_id))
- if not result.scalar_one_or_none():
- raise HTTPException(status_code=404, detail="Project not found")
- # Update queue items
- updated = 0
- for item_id in data.queue_item_ids:
- result = await db.execute(
- select(PrintQueueItem).where(PrintQueueItem.id == item_id)
- )
- item = result.scalar_one_or_none()
- if item:
- item.project_id = project_id
- updated += 1
- return {"message": f"Added {updated} queue items to project"}
- @router.post("/{project_id}/remove-archives")
- async def remove_archives_from_project(
- project_id: int,
- data: BatchAddArchives,
- db: AsyncSession = Depends(get_db),
- ):
- """Remove archives from a project (sets project_id to NULL)."""
- updated = 0
- for archive_id in data.archive_ids:
- result = await db.execute(
- select(PrintArchive).where(
- PrintArchive.id == archive_id,
- PrintArchive.project_id == project_id,
- )
- )
- archive = result.scalar_one_or_none()
- if archive:
- archive.project_id = None
- updated += 1
- return {"message": f"Removed {updated} archives from project"}
|