- Introduced a new template for listing scenarios associated with a project. - Added metrics for total, active, draft, and archived scenarios. - Implemented quick actions for creating new scenarios and reviewing project overview. - Enhanced navigation with breadcrumbs for better user experience. refactor: update Opex and Profitability templates for consistency - Changed titles and button labels for clarity in Opex and Profitability templates. - Updated form IDs and action URLs for better alignment with new naming conventions. - Improved navigation links to include scenario and project overviews. test: add integration tests for Opex calculations - Created new tests for Opex calculation HTML and JSON flows. - Validated successful calculations and ensured correct data persistence. - Implemented tests for currency mismatch and unsupported frequency scenarios. test: enhance project and scenario route tests - Added tests to verify scenario list rendering and calculator shortcuts. - Ensured scenario detail pages link back to the portfolio correctly. - Validated project detail pages show associated scenarios accurately.
171 lines
4.9 KiB
Python
171 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
from io import BytesIO
|
|
|
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
|
from fastapi import Request
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from dependencies import (
|
|
get_import_ingestion_service,
|
|
require_roles,
|
|
require_roles_html,
|
|
)
|
|
from models import User
|
|
from schemas.imports import (
|
|
ImportCommitRequest,
|
|
ProjectImportCommitResponse,
|
|
ProjectImportPreviewResponse,
|
|
ScenarioImportCommitResponse,
|
|
ScenarioImportPreviewResponse,
|
|
)
|
|
from services.importers import ImportIngestionService, UnsupportedImportFormat
|
|
from routes.template_filters import create_templates
|
|
|
|
router = APIRouter(prefix="/imports", tags=["Imports"])
|
|
templates = create_templates()
|
|
|
|
MANAGE_ROLES = ("project_manager", "admin")
|
|
|
|
|
|
@router.get(
|
|
"/ui",
|
|
response_class=HTMLResponse,
|
|
include_in_schema=False,
|
|
name="imports.ui",
|
|
)
|
|
def import_dashboard(
|
|
request: Request,
|
|
_: User = Depends(require_roles_html(*MANAGE_ROLES)),
|
|
) -> HTMLResponse:
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"imports/ui.html",
|
|
{
|
|
"title": "Imports",
|
|
},
|
|
)
|
|
|
|
|
|
async def _read_upload_file(upload: UploadFile) -> BytesIO:
|
|
content = await upload.read()
|
|
if not content:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Uploaded file is empty.",
|
|
)
|
|
return BytesIO(content)
|
|
|
|
|
|
@router.post(
|
|
"/projects/preview",
|
|
response_model=ProjectImportPreviewResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
)
|
|
async def preview_project_import(
|
|
file: UploadFile = File(...,
|
|
description="Project import file (CSV or Excel)"),
|
|
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
|
ingestion_service: ImportIngestionService = Depends(
|
|
get_import_ingestion_service),
|
|
) -> ProjectImportPreviewResponse:
|
|
if not file.filename:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Filename is required for import.",
|
|
)
|
|
|
|
stream = await _read_upload_file(file)
|
|
|
|
try:
|
|
preview = ingestion_service.preview_projects(stream, file.filename)
|
|
except UnsupportedImportFormat as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=str(exc),
|
|
) from exc
|
|
|
|
return ProjectImportPreviewResponse.model_validate(preview)
|
|
|
|
|
|
@router.post(
|
|
"/scenarios/preview",
|
|
response_model=ScenarioImportPreviewResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
)
|
|
async def preview_scenario_import(
|
|
file: UploadFile = File(...,
|
|
description="Scenario import file (CSV or Excel)"),
|
|
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
|
ingestion_service: ImportIngestionService = Depends(
|
|
get_import_ingestion_service),
|
|
) -> ScenarioImportPreviewResponse:
|
|
if not file.filename:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Filename is required for import.",
|
|
)
|
|
|
|
stream = await _read_upload_file(file)
|
|
|
|
try:
|
|
preview = ingestion_service.preview_scenarios(stream, file.filename)
|
|
except UnsupportedImportFormat as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=str(exc),
|
|
) from exc
|
|
|
|
return ScenarioImportPreviewResponse.model_validate(preview)
|
|
|
|
|
|
def _value_error_status(exc: ValueError) -> int:
|
|
detail = str(exc)
|
|
if detail.lower().startswith("unknown"):
|
|
return status.HTTP_404_NOT_FOUND
|
|
return status.HTTP_400_BAD_REQUEST
|
|
|
|
|
|
@router.post(
|
|
"/projects/commit",
|
|
response_model=ProjectImportCommitResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
)
|
|
async def commit_project_import_endpoint(
|
|
payload: ImportCommitRequest,
|
|
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
|
ingestion_service: ImportIngestionService = Depends(
|
|
get_import_ingestion_service),
|
|
) -> ProjectImportCommitResponse:
|
|
try:
|
|
result = ingestion_service.commit_project_import(payload.token)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=_value_error_status(exc),
|
|
detail=str(exc),
|
|
) from exc
|
|
|
|
return ProjectImportCommitResponse.model_validate(result)
|
|
|
|
|
|
@router.post(
|
|
"/scenarios/commit",
|
|
response_model=ScenarioImportCommitResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
)
|
|
async def commit_scenario_import_endpoint(
|
|
payload: ImportCommitRequest,
|
|
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
|
ingestion_service: ImportIngestionService = Depends(
|
|
get_import_ingestion_service),
|
|
) -> ScenarioImportCommitResponse:
|
|
try:
|
|
result = ingestion_service.commit_scenario_import(payload.token)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=_value_error_status(exc),
|
|
detail=str(exc),
|
|
) from exc
|
|
|
|
return ScenarioImportCommitResponse.model_validate(result)
|