feat: implement export functionality for projects and scenarios with CSV and Excel support
This commit is contained in:
132
routes/exports.py
Normal file
132
routes/exports.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from dependencies import get_unit_of_work, require_any_role
|
||||
from schemas.exports import (
|
||||
ExportFormat,
|
||||
ProjectExportRequest,
|
||||
ScenarioExportRequest,
|
||||
)
|
||||
from services.export_serializers import (
|
||||
export_projects_to_excel,
|
||||
export_scenarios_to_excel,
|
||||
stream_projects_to_csv,
|
||||
stream_scenarios_to_csv,
|
||||
)
|
||||
from services.unit_of_work import UnitOfWork
|
||||
|
||||
router = APIRouter(prefix="/exports", tags=["exports"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/modal/{dataset}",
|
||||
response_model=None,
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
name="exports.modal",
|
||||
)
|
||||
async def export_modal(
|
||||
dataset: str,
|
||||
request: Request,
|
||||
) -> HTMLResponse:
|
||||
dataset = dataset.lower()
|
||||
if dataset not in {"projects", "scenarios"}:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Unknown dataset")
|
||||
|
||||
submit_url = request.url_for(
|
||||
"export_projects" if dataset == "projects" else "export_scenarios"
|
||||
)
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"exports/modal.html",
|
||||
{
|
||||
"dataset": dataset,
|
||||
"submit_url": submit_url,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _timestamp_suffix() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
|
||||
def _ensure_repository(repo, name: str):
|
||||
if repo is None:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"{name} repository unavailable")
|
||||
return repo
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_class=StreamingResponse,
|
||||
dependencies=[Depends(require_any_role(
|
||||
"admin", "project_manager", "analyst"))],
|
||||
)
|
||||
async def export_projects(
|
||||
request: ProjectExportRequest,
|
||||
uow: Annotated[UnitOfWork, Depends(get_unit_of_work)],
|
||||
) -> Response:
|
||||
project_repo = _ensure_repository(
|
||||
getattr(uow, "projects", None), "Project")
|
||||
projects = project_repo.filtered_for_export(request.filters)
|
||||
|
||||
filename = f"projects-{_timestamp_suffix()}"
|
||||
|
||||
if request.format == ExportFormat.CSV:
|
||||
stream = stream_projects_to_csv(projects)
|
||||
response = StreamingResponse(stream, media_type="text/csv")
|
||||
response.headers["Content-Disposition"] = f"attachment; filename={filename}.csv"
|
||||
return response
|
||||
|
||||
data = export_projects_to_excel(projects)
|
||||
return StreamingResponse(
|
||||
iter([data]),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}.xlsx",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/scenarios",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_class=StreamingResponse,
|
||||
dependencies=[Depends(require_any_role(
|
||||
"admin", "project_manager", "analyst"))],
|
||||
)
|
||||
async def export_scenarios(
|
||||
request: ScenarioExportRequest,
|
||||
uow: Annotated[UnitOfWork, Depends(get_unit_of_work)],
|
||||
) -> Response:
|
||||
scenario_repo = _ensure_repository(
|
||||
getattr(uow, "scenarios", None), "Scenario")
|
||||
scenarios = scenario_repo.filtered_for_export(
|
||||
request.filters, include_project=True)
|
||||
|
||||
filename = f"scenarios-{_timestamp_suffix()}"
|
||||
|
||||
if request.format == ExportFormat.CSV:
|
||||
stream = stream_scenarios_to_csv(scenarios)
|
||||
response = StreamingResponse(stream, media_type="text/csv")
|
||||
response.headers["Content-Disposition"] = f"attachment; filename={filename}.csv"
|
||||
return response
|
||||
|
||||
data = export_scenarios_to_excel(scenarios)
|
||||
return StreamingResponse(
|
||||
iter([data]),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}.xlsx",
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user