"""Router per l'export del CSV Canva. Endpoint: - GET /api/export/{job_id}/csv — scarica CSV originale con Content-Disposition attachment - POST /api/export/{job_id}/csv — accetta dati modificati inline e rigenera il CSV """ from __future__ import annotations import logging from pathlib import Path from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse, Response from pydantic import BaseModel from backend.config import OUTPUTS_PATH from backend.schemas.generate import PostResult from backend.services.csv_builder import CSVBuilder from backend.services.generation_pipeline import GenerationPipeline logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/export", tags=["export"]) _csv_builder = CSVBuilder() # --------------------------------------------------------------------------- # Request schema per POST con dati modificati # --------------------------------------------------------------------------- class ExportWithEditsRequest(BaseModel): """Richiesta POST per rigenerare il CSV con dati modificati dall'utente.""" results: list[PostResult] # --------------------------------------------------------------------------- # Endpoint # --------------------------------------------------------------------------- @router.get("/{job_id}/csv") async def download_csv_original(job_id: str) -> FileResponse: """Scarica il CSV originale generato per un job. Cerca il file CSV in OUTPUTS_PATH/{job_id}.csv e lo serve come attachment. Args: job_id: Identificatore del job (da POST /api/generate/bulk). Returns: File CSV con Content-Disposition: attachment per il download automatico. Raises: HTTPException 404: Se il file CSV non esiste (job non completato o non trovato). """ csv_path = OUTPUTS_PATH / f"{job_id}.csv" if not csv_path.exists(): raise HTTPException( status_code=404, detail=f"File CSV per job '{job_id}' non trovato. " "Assicurati che la generazione sia completata.", ) return FileResponse( path=str(csv_path), media_type="text/csv; charset=utf-8", headers={ "Content-Disposition": f'attachment; filename="postgenerator_{job_id}.csv"', }, ) @router.post("/{job_id}/csv") async def download_csv_with_edits( job_id: str, request: ExportWithEditsRequest, ) -> Response: """Rigenera il CSV con i dati modificati inline dall'utente. Accetta i PostResult aggiornati dal frontend (con modifiche al testo delle slide, titoli, caption, etc.) e produce un nuovo CSV aggiornato. Il file viene salvato come OUTPUTS_PATH/{job_id}_edited.csv. Args: job_id: Identificatore del job originale. request: Lista di PostResult con i dati aggiornati dall'utente. Returns: File CSV rigenerato con le modifiche inline. Raises: HTTPException 404: Se il job originale (e quindi il calendario) non esiste. """ # Carica il calendario originale dal job per i metadati degli slot job_path = OUTPUTS_PATH / f"{job_id}.json" if not job_path.exists(): raise HTTPException( status_code=404, detail=f"Job '{job_id}' non trovato. " "Impossibile rigenerare il CSV senza i metadati originali.", ) # Carica il calendario dal JSON del job import json from backend.schemas.calendar import CalendarResponse try: with open(job_path, "r", encoding="utf-8") as f: job_data = json.load(f) calendar = CalendarResponse.model_validate(job_data["calendar"]) except Exception as e: raise HTTPException( status_code=500, detail=f"Errore nel caricamento del job: {str(e)}", ) # Genera il CSV con i dati modificati csv_content = _csv_builder.build_csv_content( posts=request.results, calendar=calendar, job_id=job_id, ) # Salva anche su disco come versione edited edited_path = OUTPUTS_PATH / f"{job_id}_edited.csv" edited_path.parent.mkdir(parents=True, exist_ok=True) with open(edited_path, "w", newline="", encoding="utf-8-sig") as f: f.write(csv_content) logger.info( "CSV con modifiche generato | job_id=%s | righe=%d | path=%s", job_id, len([r for r in request.results if r.status == "success"]), edited_path, ) # Ritorna il CSV come response con Content-Disposition attachment # Nota: build_csv_content include già il BOM (\ufeff), usiamo encode("utf-8") # per non raddoppiare il BOM nel body return Response( content=csv_content.encode("utf-8"), media_type="text/csv; charset=utf-8", headers={ "Content-Disposition": f'attachment; filename="postgenerator_{job_id}_edited.csv"', }, )