from __future__ import annotations from typing import List from fastapi import APIRouter, Depends, Form, HTTPException, Request, status from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from dependencies import get_unit_of_work from models import MiningOperationType, Project from schemas.project import ProjectCreate, ProjectRead, ProjectUpdate from services.exceptions import EntityConflictError, EntityNotFoundError from services.unit_of_work import UnitOfWork router = APIRouter(prefix="/projects", tags=["Projects"]) templates = Jinja2Templates(directory="templates") def _to_read_model(project: Project) -> ProjectRead: return ProjectRead.model_validate(project) def _operation_type_choices() -> list[tuple[str, str]]: return [ (op.value, op.name.replace("_", " ").title()) for op in MiningOperationType ] @router.get("", response_model=List[ProjectRead]) def list_projects(uow: UnitOfWork = Depends(get_unit_of_work)) -> List[ProjectRead]: projects = uow.projects.list() return [_to_read_model(project) for project in projects] @router.post("", response_model=ProjectRead, status_code=status.HTTP_201_CREATED) def create_project( payload: ProjectCreate, uow: UnitOfWork = Depends(get_unit_of_work) ) -> ProjectRead: project = Project(**payload.model_dump()) try: created = uow.projects.create(project) except EntityConflictError as exc: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=str(exc) ) from exc return _to_read_model(created) @router.get("/{project_id}", response_model=ProjectRead) def get_project(project_id: int, uow: UnitOfWork = Depends(get_unit_of_work)) -> ProjectRead: try: project = uow.projects.get(project_id) except EntityNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc return _to_read_model(project) @router.put("/{project_id}", response_model=ProjectRead) def update_project( project_id: int, payload: ProjectUpdate, uow: UnitOfWork = Depends(get_unit_of_work), ) -> ProjectRead: try: project = uow.projects.get(project_id) except EntityNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc update_data = payload.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(project, field, value) uow.flush() return _to_read_model(project) @router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_project(project_id: int, uow: UnitOfWork = Depends(get_unit_of_work)) -> None: try: uow.projects.delete(project_id) except EntityNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc @router.get( "/ui", response_class=HTMLResponse, include_in_schema=False, name="projects.project_list_page", ) def project_list_page( request: Request, uow: UnitOfWork = Depends(get_unit_of_work) ) -> HTMLResponse: projects = uow.projects.list(with_children=True) for project in projects: setattr(project, "scenario_count", len(project.scenarios)) return templates.TemplateResponse( "projects/list.html", { "request": request, "projects": projects, }, ) @router.get( "/create", response_class=HTMLResponse, include_in_schema=False, name="projects.create_project_form", ) def create_project_form(request: Request) -> HTMLResponse: return templates.TemplateResponse( "projects/form.html", { "request": request, "project": None, "operation_types": _operation_type_choices(), "form_action": request.url_for("projects.create_project_submit"), "cancel_url": request.url_for("projects.project_list_page"), }, ) @router.post( "/create", include_in_schema=False, name="projects.create_project_submit", ) def create_project_submit( request: Request, name: str = Form(...), location: str | None = Form(None), operation_type: str = Form(...), description: str | None = Form(None), uow: UnitOfWork = Depends(get_unit_of_work), ): def _normalise(value: str | None) -> str | None: if value is None: return None value = value.strip() return value or None try: op_type = MiningOperationType(operation_type) except ValueError as exc: return templates.TemplateResponse( "projects/form.html", { "request": request, "project": None, "operation_types": _operation_type_choices(), "form_action": request.url_for("projects.create_project_submit"), "cancel_url": request.url_for("projects.project_list_page"), "error": "Invalid operation type.", }, status_code=status.HTTP_400_BAD_REQUEST, ) project = Project( name=name.strip(), location=_normalise(location), operation_type=op_type, description=_normalise(description), ) try: uow.projects.create(project) except EntityConflictError as exc: return templates.TemplateResponse( "projects/form.html", { "request": request, "project": project, "operation_types": _operation_type_choices(), "form_action": request.url_for("projects.create_project_submit"), "cancel_url": request.url_for("projects.project_list_page"), "error": "Project with this name already exists.", }, status_code=status.HTTP_409_CONFLICT, ) return RedirectResponse( request.url_for("projects.project_list_page"), status_code=status.HTTP_303_SEE_OTHER, ) @router.get( "/{project_id}/view", response_class=HTMLResponse, include_in_schema=False, name="projects.view_project", ) def view_project( project_id: int, request: Request, uow: UnitOfWork = Depends(get_unit_of_work) ) -> HTMLResponse: try: project = uow.projects.get(project_id, with_children=True) except EntityNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) ) from exc scenarios = sorted(project.scenarios, key=lambda s: s.created_at) return templates.TemplateResponse( "projects/detail.html", { "request": request, "project": project, "scenarios": scenarios, }, ) @router.get( "/{project_id}/edit", response_class=HTMLResponse, include_in_schema=False, name="projects.edit_project_form", ) def edit_project_form( project_id: int, request: Request, uow: UnitOfWork = Depends(get_unit_of_work) ) -> HTMLResponse: try: project = uow.projects.get(project_id) except EntityNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) ) from exc return templates.TemplateResponse( "projects/form.html", { "request": request, "project": project, "operation_types": _operation_type_choices(), "form_action": request.url_for( "projects.edit_project_submit", project_id=project_id ), "cancel_url": request.url_for( "projects.view_project", project_id=project_id ), }, ) @router.post( "/{project_id}/edit", include_in_schema=False, name="projects.edit_project_submit", ) def edit_project_submit( project_id: int, request: Request, name: str = Form(...), location: str | None = Form(None), operation_type: str | None = Form(None), description: str | None = Form(None), uow: UnitOfWork = Depends(get_unit_of_work), ): try: project = uow.projects.get(project_id) except EntityNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) ) from exc def _normalise(value: str | None) -> str | None: if value is None: return None value = value.strip() return value or None project.name = name.strip() project.location = _normalise(location) if operation_type: try: project.operation_type = MiningOperationType(operation_type) except ValueError as exc: return templates.TemplateResponse( "projects/form.html", { "request": request, "project": project, "operation_types": _operation_type_choices(), "form_action": request.url_for( "projects.edit_project_submit", project_id=project_id ), "cancel_url": request.url_for( "projects.view_project", project_id=project_id ), "error": "Invalid operation type.", }, status_code=status.HTTP_400_BAD_REQUEST, ) project.description = _normalise(description) uow.flush() return RedirectResponse( request.url_for("projects.view_project", project_id=project_id), status_code=status.HTTP_303_SEE_OTHER, )