Feature: print files directly from project view (closes #930) (#932)

* feat: print files directly from project view (closes #930)

Show printable files from linked library folders directly in the project
detail page, with Print Now and Add to Queue buttons per file. Removes
the detour through the File Manager for common reprint workflows.
This commit is contained in:
lietschaend
2026-04-10 12:54:27 +02:00
committed by GitHub
parent b453385d22
commit f95b2acd7d
21 changed files with 566 additions and 31 deletions
+14 -1
View File
@@ -1044,14 +1044,16 @@ async def scan_external_folder(
async def list_files(
response: Response,
folder_id: int | None = None,
project_id: int | None = None,
include_root: bool = True,
db: AsyncSession = Depends(get_db),
_: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)),
):
"""List files, optionally filtered by folder.
"""List files, optionally filtered by folder or project.
Args:
folder_id: Filter by folder ID. If None and include_root=True, returns root files.
project_id: Return all files across folders linked to this project (bulk fetch, avoids N+1).
include_root: If True and folder_id is None, returns files at root level.
If False and folder_id is None, returns all files.
"""
@@ -1059,6 +1061,10 @@ async def list_files(
if folder_id is not None:
query = query.where(LibraryFile.folder_id == folder_id)
elif project_id is not None:
# Single join instead of one query per folder (avoids N+1 pattern)
query = query.join(LibraryFolder, LibraryFile.folder_id == LibraryFolder.id)
query = query.where(LibraryFolder.project_id == project_id)
elif include_root:
query = query.where(LibraryFile.folder_id.is_(None))
@@ -2210,6 +2216,12 @@ async def print_library_file(
if not printer_manager.is_connected(printer_id):
raise HTTPException(status_code=400, detail="Printer is not connected")
# Validate project exists before dispatching so a bogus ID yields 404, not a FK-constraint 500
if body.project_id is not None:
project_result = await db.execute(select(Project).where(Project.id == body.project_id))
if not project_result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="Project not found")
plate_name = body.plate_name
if not plate_name and body.plate_id is not None:
plate_name = f"Plate {body.plate_id}"
@@ -2225,6 +2237,7 @@ async def print_library_file(
printer_id=printer_id,
printer_name=printer.name,
options=body.model_dump(exclude_none=True),
project_id=body.project_id,
requested_by_user_id=None,
requested_by_username=None,
)
+8
View File
@@ -21,6 +21,7 @@ from backend.app.models.library import LibraryFile
from backend.app.models.print_batch import PrintBatch
from backend.app.models.print_queue import PrintQueueItem
from backend.app.models.printer import Printer
from backend.app.models.project import Project
from backend.app.models.user import User
from backend.app.schemas.print_queue import (
PrintBatchResponse,
@@ -484,6 +485,12 @@ async def add_to_queue(
if plate_time is not None:
cached_print_time = plate_time
# Validate project exists before insert so a bogus ID yields 404, not an FK-constraint 500
if data.project_id is not None:
project_result = await db.execute(select(Project).where(Project.id == data.project_id))
if not project_result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="Project not found")
ams_mapping_json = json.dumps(data.ams_mapping) if data.ams_mapping else None
items = []
for i in range(quantity):
@@ -508,6 +515,7 @@ async def add_to_queue(
timelapse=data.timelapse,
use_ams=data.use_ams,
gcode_injection=data.gcode_injection,
project_id=data.project_id,
position=max_pos + 1 + i,
status="pending",
created_by_id=current_user.id if current_user else None,