feat: connect project and scenario routers to new Jinja2 views with forms and error handling
This commit is contained in:
@@ -7,3 +7,4 @@
|
|||||||
- Introduced repository and unit-of-work helpers for projects, scenarios, financial inputs, and simulation parameters to support service-layer operations.
|
- Introduced repository and unit-of-work helpers for projects, scenarios, financial inputs, and simulation parameters to support service-layer operations.
|
||||||
- Added SQLite-backed pytest coverage for repository and unit-of-work behaviours to validate persistence interactions.
|
- Added SQLite-backed pytest coverage for repository and unit-of-work behaviours to validate persistence interactions.
|
||||||
- Exposed project and scenario CRUD APIs with validated schemas and integrated them into the FastAPI application.
|
- Exposed project and scenario CRUD APIs with validated schemas and integrated them into the FastAPI application.
|
||||||
|
- Connected project and scenario routers to new Jinja2 list/detail/edit views with HTML forms and redirects.
|
||||||
|
|||||||
@@ -2,21 +2,30 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
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 dependencies import get_unit_of_work
|
||||||
from models import Project
|
from models import MiningOperationType, Project
|
||||||
from schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
|
from schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
|
||||||
from services.exceptions import EntityConflictError, EntityNotFoundError
|
from services.exceptions import EntityConflictError, EntityNotFoundError
|
||||||
from services.unit_of_work import UnitOfWork
|
from services.unit_of_work import UnitOfWork
|
||||||
|
|
||||||
router = APIRouter(prefix="/projects", tags=["Projects"])
|
router = APIRouter(prefix="/projects", tags=["Projects"])
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
def _to_read_model(project: Project) -> ProjectRead:
|
def _to_read_model(project: Project) -> ProjectRead:
|
||||||
return ProjectRead.model_validate(project)
|
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])
|
@router.get("", response_model=List[ProjectRead])
|
||||||
def list_projects(uow: UnitOfWork = Depends(get_unit_of_work)) -> List[ProjectRead]:
|
def list_projects(uow: UnitOfWork = Depends(get_unit_of_work)) -> List[ProjectRead]:
|
||||||
projects = uow.projects.list()
|
projects = uow.projects.list()
|
||||||
@@ -74,3 +83,224 @@ def delete_project(project_id: int, uow: UnitOfWork = Depends(get_unit_of_work))
|
|||||||
except EntityNotFoundError as exc:
|
except EntityNotFoundError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
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 dependencies import get_unit_of_work
|
||||||
from models import Scenario
|
from models import ResourceType, Scenario, ScenarioStatus
|
||||||
from schemas.scenario import ScenarioCreate, ScenarioRead, ScenarioUpdate
|
from schemas.scenario import ScenarioCreate, ScenarioRead, ScenarioUpdate
|
||||||
from services.exceptions import EntityConflictError, EntityNotFoundError
|
from services.exceptions import EntityConflictError, EntityNotFoundError
|
||||||
from services.unit_of_work import UnitOfWork
|
from services.unit_of_work import UnitOfWork
|
||||||
|
|
||||||
router = APIRouter(tags=["Scenarios"])
|
router = APIRouter(tags=["Scenarios"])
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
def _to_read_model(scenario: Scenario) -> ScenarioRead:
|
def _to_read_model(scenario: Scenario) -> ScenarioRead:
|
||||||
return ScenarioRead.model_validate(scenario)
|
return ScenarioRead.model_validate(scenario)
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_type_choices() -> list[tuple[str, str]]:
|
||||||
|
return [
|
||||||
|
(resource.value, resource.value.replace("_", " ").title())
|
||||||
|
for resource in ResourceType
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _scenario_status_choices() -> list[tuple[str, str]]:
|
||||||
|
return [
|
||||||
|
(status.value, status.value.title()) for status in ScenarioStatus
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/projects/{project_id}/scenarios",
|
"/projects/{project_id}/scenarios",
|
||||||
response_model=List[ScenarioRead],
|
response_model=List[ScenarioRead],
|
||||||
@@ -101,3 +118,270 @@ def delete_scenario(
|
|||||||
except EntityNotFoundError as exc:
|
except EntityNotFoundError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_date(value: str | None) -> date | None:
|
||||||
|
value = _normalise(value)
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
return date.fromisoformat(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_discount_rate(value: str | None) -> float | None:
|
||||||
|
value = _normalise(value)
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/projects/{project_id}/scenarios/new",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
include_in_schema=False,
|
||||||
|
name="scenarios.create_scenario_form",
|
||||||
|
)
|
||||||
|
def create_scenario_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(
|
||||||
|
"scenarios/form.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"project": project,
|
||||||
|
"scenario": None,
|
||||||
|
"scenario_statuses": _scenario_status_choices(),
|
||||||
|
"resource_types": _resource_type_choices(),
|
||||||
|
"form_action": request.url_for(
|
||||||
|
"scenarios.create_scenario_submit", project_id=project_id
|
||||||
|
),
|
||||||
|
"cancel_url": request.url_for(
|
||||||
|
"projects.view_project", project_id=project_id
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/projects/{project_id}/scenarios/new",
|
||||||
|
include_in_schema=False,
|
||||||
|
name="scenarios.create_scenario_submit",
|
||||||
|
)
|
||||||
|
def create_scenario_submit(
|
||||||
|
project_id: int,
|
||||||
|
request: Request,
|
||||||
|
name: str = Form(...),
|
||||||
|
description: str | None = Form(None),
|
||||||
|
status_value: str = Form(ScenarioStatus.DRAFT.value),
|
||||||
|
start_date: str | None = Form(None),
|
||||||
|
end_date: str | None = Form(None),
|
||||||
|
discount_rate: str | None = Form(None),
|
||||||
|
currency: str | None = Form(None),
|
||||||
|
primary_resource: 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
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_enum = ScenarioStatus(status_value)
|
||||||
|
except ValueError:
|
||||||
|
status_enum = ScenarioStatus.DRAFT
|
||||||
|
|
||||||
|
resource_enum = None
|
||||||
|
if primary_resource:
|
||||||
|
try:
|
||||||
|
resource_enum = ResourceType(primary_resource)
|
||||||
|
except ValueError:
|
||||||
|
resource_enum = None
|
||||||
|
|
||||||
|
currency_value = _normalise(currency)
|
||||||
|
currency_value = currency_value.upper() if currency_value else None
|
||||||
|
|
||||||
|
scenario = Scenario(
|
||||||
|
project_id=project_id,
|
||||||
|
name=name.strip(),
|
||||||
|
description=_normalise(description),
|
||||||
|
status=status_enum,
|
||||||
|
start_date=_parse_date(start_date),
|
||||||
|
end_date=_parse_date(end_date),
|
||||||
|
discount_rate=_parse_discount_rate(discount_rate),
|
||||||
|
currency=currency_value,
|
||||||
|
primary_resource=resource_enum,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
uow.scenarios.create(scenario)
|
||||||
|
except EntityConflictError as exc:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"scenarios/form.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"project": project,
|
||||||
|
"scenario": scenario,
|
||||||
|
"scenario_statuses": _scenario_status_choices(),
|
||||||
|
"resource_types": _resource_type_choices(),
|
||||||
|
"form_action": request.url_for(
|
||||||
|
"scenarios.create_scenario_submit", project_id=project_id
|
||||||
|
),
|
||||||
|
"cancel_url": request.url_for(
|
||||||
|
"projects.view_project", project_id=project_id
|
||||||
|
),
|
||||||
|
"error": "Scenario could not be created.",
|
||||||
|
},
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse(
|
||||||
|
request.url_for("projects.view_project", project_id=project_id),
|
||||||
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/scenarios/{scenario_id}/view",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
include_in_schema=False,
|
||||||
|
name="scenarios.view_scenario",
|
||||||
|
)
|
||||||
|
def view_scenario(
|
||||||
|
scenario_id: int, request: Request, uow: UnitOfWork = Depends(get_unit_of_work)
|
||||||
|
) -> HTMLResponse:
|
||||||
|
try:
|
||||||
|
scenario = uow.scenarios.get(scenario_id, with_children=True)
|
||||||
|
except EntityNotFoundError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
project = uow.projects.get(scenario.project_id)
|
||||||
|
financial_inputs = sorted(
|
||||||
|
scenario.financial_inputs, key=lambda item: item.created_at
|
||||||
|
)
|
||||||
|
simulation_parameters = sorted(
|
||||||
|
scenario.simulation_parameters, key=lambda item: item.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"scenarios/detail.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"project": project,
|
||||||
|
"scenario": scenario,
|
||||||
|
"financial_inputs": financial_inputs,
|
||||||
|
"simulation_parameters": simulation_parameters,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/scenarios/{scenario_id}/edit",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
include_in_schema=False,
|
||||||
|
name="scenarios.edit_scenario_form",
|
||||||
|
)
|
||||||
|
def edit_scenario_form(
|
||||||
|
scenario_id: int, request: Request, uow: UnitOfWork = Depends(get_unit_of_work)
|
||||||
|
) -> HTMLResponse:
|
||||||
|
try:
|
||||||
|
scenario = uow.scenarios.get(scenario_id)
|
||||||
|
except EntityNotFoundError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
project = uow.projects.get(scenario.project_id)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"scenarios/form.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"project": project,
|
||||||
|
"scenario": scenario,
|
||||||
|
"scenario_statuses": _scenario_status_choices(),
|
||||||
|
"resource_types": _resource_type_choices(),
|
||||||
|
"form_action": request.url_for(
|
||||||
|
"scenarios.edit_scenario_submit", scenario_id=scenario_id
|
||||||
|
),
|
||||||
|
"cancel_url": request.url_for(
|
||||||
|
"scenarios.view_scenario", scenario_id=scenario_id
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/scenarios/{scenario_id}/edit",
|
||||||
|
include_in_schema=False,
|
||||||
|
name="scenarios.edit_scenario_submit",
|
||||||
|
)
|
||||||
|
def edit_scenario_submit(
|
||||||
|
scenario_id: int,
|
||||||
|
request: Request,
|
||||||
|
name: str = Form(...),
|
||||||
|
description: str | None = Form(None),
|
||||||
|
status_value: str = Form(ScenarioStatus.DRAFT.value),
|
||||||
|
start_date: str | None = Form(None),
|
||||||
|
end_date: str | None = Form(None),
|
||||||
|
discount_rate: str | None = Form(None),
|
||||||
|
currency: str | None = Form(None),
|
||||||
|
primary_resource: str | None = Form(None),
|
||||||
|
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
scenario = uow.scenarios.get(scenario_id)
|
||||||
|
except EntityNotFoundError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
project = uow.projects.get(scenario.project_id)
|
||||||
|
|
||||||
|
scenario.name = name.strip()
|
||||||
|
scenario.description = _normalise(description)
|
||||||
|
try:
|
||||||
|
scenario.status = ScenarioStatus(status_value)
|
||||||
|
except ValueError:
|
||||||
|
scenario.status = ScenarioStatus.DRAFT
|
||||||
|
scenario.start_date = _parse_date(start_date)
|
||||||
|
scenario.end_date = _parse_date(end_date)
|
||||||
|
|
||||||
|
scenario.discount_rate = _parse_discount_rate(discount_rate)
|
||||||
|
|
||||||
|
currency_value = _normalise(currency)
|
||||||
|
scenario.currency = currency_value.upper() if currency_value else None
|
||||||
|
|
||||||
|
resource_enum = None
|
||||||
|
if primary_resource:
|
||||||
|
try:
|
||||||
|
resource_enum = ResourceType(primary_resource)
|
||||||
|
except ValueError:
|
||||||
|
resource_enum = None
|
||||||
|
scenario.primary_resource = resource_enum
|
||||||
|
|
||||||
|
uow.flush()
|
||||||
|
|
||||||
|
return RedirectResponse(
|
||||||
|
request.url_for("scenarios.view_scenario", scenario_id=scenario_id),
|
||||||
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Sequence
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||||
|
|
||||||
from models import FinancialInput, Project, Scenario, SimulationParameter
|
from models import FinancialInput, Project, Scenario, SimulationParameter
|
||||||
from services.exceptions import EntityConflictError, EntityNotFoundError
|
from services.exceptions import EntityConflictError, EntityNotFoundError
|
||||||
@@ -17,8 +17,10 @@ class ProjectRepository:
|
|||||||
def __init__(self, session: Session) -> None:
|
def __init__(self, session: Session) -> None:
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
def list(self) -> Sequence[Project]:
|
def list(self, *, with_children: bool = False) -> Sequence[Project]:
|
||||||
stmt = select(Project).order_by(Project.created_at)
|
stmt = select(Project).order_by(Project.created_at)
|
||||||
|
if with_children:
|
||||||
|
stmt = stmt.options(selectinload(Project.scenarios))
|
||||||
return self.session.execute(stmt).scalars().all()
|
return self.session.execute(stmt).scalars().all()
|
||||||
|
|
||||||
def get(self, project_id: int, *, with_children: bool = False) -> Project:
|
def get(self, project_id: int, *, with_children: bool = False) -> Project:
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form class="form" method="post" action="{{ form_action }}">
|
<form class="form" method="post" action="{{ form_action }}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form class="form" method="post" action="{{ form_action }}">
|
<form class="form" method="post" action="{{ form_action }}">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user