Refactor test cases for improved readability and consistency
- Updated test functions in various test files to enhance code clarity by formatting long lines and improving indentation. - Adjusted assertions to use multi-line formatting for better readability. - Added new test cases for theme settings API to ensure proper functionality. - Ensured consistent use of line breaks and spacing across test files for uniformity.
This commit is contained in:
@@ -36,7 +36,9 @@ class ConsumptionRead(ConsumptionBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=ConsumptionRead, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/", response_model=ConsumptionRead, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def create_consumption(item: ConsumptionCreate, db: Session = Depends(get_db)):
|
||||
db_item = Consumption(**item.model_dump())
|
||||
db.add(db_item)
|
||||
|
||||
@@ -73,7 +73,8 @@ def create_capex(item: CapexCreate, db: Session = Depends(get_db)):
|
||||
if not cid:
|
||||
code = (payload.pop("currency_code", "USD") or "USD").strip().upper()
|
||||
currency_cls = __import__(
|
||||
"models.currency", fromlist=["Currency"]).Currency
|
||||
"models.currency", fromlist=["Currency"]
|
||||
).Currency
|
||||
currency = db.query(currency_cls).filter_by(code=code).one_or_none()
|
||||
if currency is None:
|
||||
currency = currency_cls(code=code, name=code, symbol=None)
|
||||
@@ -100,7 +101,8 @@ def create_opex(item: OpexCreate, db: Session = Depends(get_db)):
|
||||
if not cid:
|
||||
code = (payload.pop("currency_code", "USD") or "USD").strip().upper()
|
||||
currency_cls = __import__(
|
||||
"models.currency", fromlist=["Currency"]).Currency
|
||||
"models.currency", fromlist=["Currency"]
|
||||
).Currency
|
||||
currency = db.query(currency_cls).filter_by(code=code).one_or_none()
|
||||
if currency is None:
|
||||
currency = currency_cls(code=code, name=code, symbol=None)
|
||||
|
||||
@@ -97,20 +97,20 @@ def _ensure_default_currency(db: Session) -> Currency:
|
||||
def _get_currency_or_404(db: Session, code: str) -> Currency:
|
||||
normalized = code.strip().upper()
|
||||
currency = (
|
||||
db.query(Currency)
|
||||
.filter(Currency.code == normalized)
|
||||
.one_or_none()
|
||||
db.query(Currency).filter(Currency.code == normalized).one_or_none()
|
||||
)
|
||||
if currency is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Currency not found")
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Currency not found"
|
||||
)
|
||||
return currency
|
||||
|
||||
|
||||
@router.get("/", response_model=List[CurrencyRead])
|
||||
def list_currencies(
|
||||
include_inactive: bool = Query(
|
||||
False, description="Include inactive currencies"),
|
||||
False, description="Include inactive currencies"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
_ensure_default_currency(db)
|
||||
@@ -121,14 +121,12 @@ def list_currencies(
|
||||
return currencies
|
||||
|
||||
|
||||
@router.post("/", response_model=CurrencyRead, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/", response_model=CurrencyRead, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def create_currency(payload: CurrencyCreate, db: Session = Depends(get_db)):
|
||||
code = payload.code
|
||||
existing = (
|
||||
db.query(Currency)
|
||||
.filter(Currency.code == code)
|
||||
.one_or_none()
|
||||
)
|
||||
existing = db.query(Currency).filter(Currency.code == code).one_or_none()
|
||||
if existing is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
@@ -148,7 +146,9 @@ def create_currency(payload: CurrencyCreate, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.put("/{code}", response_model=CurrencyRead)
|
||||
def update_currency(code: str, payload: CurrencyUpdate, db: Session = Depends(get_db)):
|
||||
def update_currency(
|
||||
code: str, payload: CurrencyUpdate, db: Session = Depends(get_db)
|
||||
):
|
||||
currency = _get_currency_or_404(db, code)
|
||||
|
||||
if payload.name is not None:
|
||||
@@ -175,7 +175,9 @@ def update_currency(code: str, payload: CurrencyUpdate, db: Session = Depends(ge
|
||||
|
||||
|
||||
@router.patch("/{code}/activation", response_model=CurrencyRead)
|
||||
def toggle_currency_activation(code: str, body: CurrencyActivation, db: Session = Depends(get_db)):
|
||||
def toggle_currency_activation(
|
||||
code: str, body: CurrencyActivation, db: Session = Depends(get_db)
|
||||
):
|
||||
currency = _get_currency_or_404(db, code)
|
||||
code_value = getattr(currency, "code")
|
||||
if code_value == DEFAULT_CURRENCY_CODE and body.is_active is False:
|
||||
|
||||
@@ -22,7 +22,9 @@ class DistributionRead(DistributionCreate):
|
||||
|
||||
|
||||
@router.post("/", response_model=DistributionRead)
|
||||
async def create_distribution(dist: DistributionCreate, db: Session = Depends(get_db)):
|
||||
async def create_distribution(
|
||||
dist: DistributionCreate, db: Session = Depends(get_db)
|
||||
):
|
||||
db_dist = Distribution(**dist.model_dump())
|
||||
db.add(db_dist)
|
||||
db.commit()
|
||||
|
||||
@@ -23,7 +23,9 @@ class EquipmentRead(EquipmentCreate):
|
||||
|
||||
|
||||
@router.post("/", response_model=EquipmentRead)
|
||||
async def create_equipment(item: EquipmentCreate, db: Session = Depends(get_db)):
|
||||
async def create_equipment(
|
||||
item: EquipmentCreate, db: Session = Depends(get_db)
|
||||
):
|
||||
db_item = Equipment(**item.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
|
||||
@@ -34,8 +34,9 @@ class MaintenanceRead(MaintenanceBase):
|
||||
|
||||
|
||||
def _get_maintenance_or_404(db: Session, maintenance_id: int) -> Maintenance:
|
||||
maintenance = db.query(Maintenance).filter(
|
||||
Maintenance.id == maintenance_id).first()
|
||||
maintenance = (
|
||||
db.query(Maintenance).filter(Maintenance.id == maintenance_id).first()
|
||||
)
|
||||
if maintenance is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -44,8 +45,12 @@ def _get_maintenance_or_404(db: Session, maintenance_id: int) -> Maintenance:
|
||||
return maintenance
|
||||
|
||||
|
||||
@router.post("/", response_model=MaintenanceRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_maintenance(maintenance: MaintenanceCreate, db: Session = Depends(get_db)):
|
||||
@router.post(
|
||||
"/", response_model=MaintenanceRead, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def create_maintenance(
|
||||
maintenance: MaintenanceCreate, db: Session = Depends(get_db)
|
||||
):
|
||||
db_maintenance = Maintenance(**maintenance.model_dump())
|
||||
db.add(db_maintenance)
|
||||
db.commit()
|
||||
@@ -54,7 +59,9 @@ def create_maintenance(maintenance: MaintenanceCreate, db: Session = Depends(get
|
||||
|
||||
|
||||
@router.get("/", response_model=List[MaintenanceRead])
|
||||
def list_maintenance(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
def list_maintenance(
|
||||
skip: int = 0, limit: int = 100, db: Session = Depends(get_db)
|
||||
):
|
||||
return db.query(Maintenance).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
|
||||
@@ -30,12 +30,15 @@ class ParameterCreate(BaseModel):
|
||||
return None
|
||||
if normalized not in {"normal", "uniform", "triangular"}:
|
||||
raise ValueError(
|
||||
"distribution_type must be normal, uniform, or triangular")
|
||||
"distribution_type must be normal, uniform, or triangular"
|
||||
)
|
||||
return normalized
|
||||
|
||||
@field_validator("distribution_parameters")
|
||||
@classmethod
|
||||
def empty_dict_to_none(cls, value: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
def empty_dict_to_none(
|
||||
cls, value: Optional[Dict[str, Any]]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if value is None:
|
||||
return None
|
||||
return value or None
|
||||
@@ -45,6 +48,7 @@ class ParameterRead(ParameterCreate):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=ParameterRead)
|
||||
def create_parameter(param: ParameterCreate, db: Session = Depends(get_db)):
|
||||
scen = db.query(Scenario).filter(Scenario.id == param.scenario_id).first()
|
||||
@@ -55,11 +59,15 @@ def create_parameter(param: ParameterCreate, db: Session = Depends(get_db)):
|
||||
distribution_parameters = param.distribution_parameters
|
||||
|
||||
if distribution_id is not None:
|
||||
distribution = db.query(Distribution).filter(
|
||||
Distribution.id == distribution_id).first()
|
||||
distribution = (
|
||||
db.query(Distribution)
|
||||
.filter(Distribution.id == distribution_id)
|
||||
.first()
|
||||
)
|
||||
if not distribution:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Distribution not found")
|
||||
status_code=404, detail="Distribution not found"
|
||||
)
|
||||
distribution_type = distribution.distribution_type
|
||||
distribution_parameters = distribution.parameters or None
|
||||
|
||||
|
||||
@@ -36,8 +36,14 @@ class ProductionOutputRead(ProductionOutputBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=ProductionOutputRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_production(item: ProductionOutputCreate, db: Session = Depends(get_db)):
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=ProductionOutputRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_production(
|
||||
item: ProductionOutputCreate, db: Session = Depends(get_db)
|
||||
):
|
||||
db_item = ProductionOutput(**item.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
|
||||
@@ -24,6 +24,7 @@ class ScenarioRead(ScenarioCreate):
|
||||
updated_at: Optional[datetime] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=ScenarioRead)
|
||||
def create_scenario(scenario: ScenarioCreate, db: Session = Depends(get_db)):
|
||||
db_s = db.query(Scenario).filter(Scenario.name == scenario.name).first()
|
||||
|
||||
@@ -11,6 +11,8 @@ from services.settings import (
|
||||
list_css_env_override_rows,
|
||||
read_css_color_env_overrides,
|
||||
update_css_color_settings,
|
||||
get_theme_settings,
|
||||
save_theme_settings,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["Settings"])
|
||||
@@ -49,8 +51,7 @@ def read_css_settings(db: Session = Depends(get_db)) -> CSSSettingsResponse:
|
||||
values = get_css_color_settings(db)
|
||||
env_overrides = read_css_color_env_overrides()
|
||||
env_sources = [
|
||||
EnvOverride(**row)
|
||||
for row in list_css_env_override_rows()
|
||||
EnvOverride(**row) for row in list_css_env_override_rows()
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
@@ -64,14 +65,17 @@ def read_css_settings(db: Session = Depends(get_db)) -> CSSSettingsResponse:
|
||||
)
|
||||
|
||||
|
||||
@router.put("/css", response_model=CSSSettingsResponse, status_code=status.HTTP_200_OK)
|
||||
def update_css_settings(payload: CSSSettingsPayload, db: Session = Depends(get_db)) -> CSSSettingsResponse:
|
||||
@router.put(
|
||||
"/css", response_model=CSSSettingsResponse, status_code=status.HTTP_200_OK
|
||||
)
|
||||
def update_css_settings(
|
||||
payload: CSSSettingsPayload, db: Session = Depends(get_db)
|
||||
) -> CSSSettingsResponse:
|
||||
try:
|
||||
values = update_css_color_settings(db, payload.variables)
|
||||
env_overrides = read_css_color_env_overrides()
|
||||
env_sources = [
|
||||
EnvOverride(**row)
|
||||
for row in list_css_env_override_rows()
|
||||
EnvOverride(**row) for row in list_css_env_override_rows()
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
@@ -83,3 +87,24 @@ def update_css_settings(payload: CSSSettingsPayload, db: Session = Depends(get_d
|
||||
env_overrides=env_overrides,
|
||||
env_sources=env_sources,
|
||||
)
|
||||
|
||||
|
||||
class ThemeSettings(BaseModel):
|
||||
theme_name: str
|
||||
primary_color: str
|
||||
secondary_color: str
|
||||
accent_color: str
|
||||
background_color: str
|
||||
text_color: str
|
||||
|
||||
|
||||
@router.post("/theme")
|
||||
async def update_theme(theme_data: ThemeSettings, db: Session = Depends(get_db)):
|
||||
data_dict = theme_data.model_dump()
|
||||
saved = save_theme_settings(db, data_dict)
|
||||
return {"message": "Theme updated", "theme": data_dict}
|
||||
|
||||
|
||||
@router.get("/theme")
|
||||
async def get_theme(db: Session = Depends(get_db)):
|
||||
return get_theme_settings(db)
|
||||
|
||||
@@ -43,7 +43,9 @@ class SimulationRunResponse(BaseModel):
|
||||
summary: Dict[str, float | int]
|
||||
|
||||
|
||||
def _load_parameters(db: Session, scenario_id: int) -> List[SimulationParameterInput]:
|
||||
def _load_parameters(
|
||||
db: Session, scenario_id: int
|
||||
) -> List[SimulationParameterInput]:
|
||||
db_params = (
|
||||
db.query(Parameter)
|
||||
.filter(Parameter.scenario_id == scenario_id)
|
||||
@@ -60,17 +62,19 @@ def _load_parameters(db: Session, scenario_id: int) -> List[SimulationParameterI
|
||||
|
||||
|
||||
@router.post("/run", response_model=SimulationRunResponse)
|
||||
async def simulate(payload: SimulationRunRequest, db: Session = Depends(get_db)):
|
||||
scenario = db.query(Scenario).filter(
|
||||
Scenario.id == payload.scenario_id).first()
|
||||
async def simulate(
|
||||
payload: SimulationRunRequest, db: Session = Depends(get_db)
|
||||
):
|
||||
scenario = (
|
||||
db.query(Scenario).filter(Scenario.id == payload.scenario_id).first()
|
||||
)
|
||||
if scenario is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scenario not found",
|
||||
)
|
||||
|
||||
parameters = payload.parameters or _load_parameters(
|
||||
db, payload.scenario_id)
|
||||
parameters = payload.parameters or _load_parameters(db, payload.scenario_id)
|
||||
if not parameters:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
204
routes/ui.py
204
routes/ui.py
@@ -53,7 +53,9 @@ router = APIRouter()
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
def _context(request: Request, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
def _context(
|
||||
request: Request, extra: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"request": request,
|
||||
"current_year": datetime.now(timezone.utc).year,
|
||||
@@ -98,7 +100,9 @@ def _load_scenarios(db: Session) -> Dict[str, Any]:
|
||||
|
||||
def _load_parameters(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for param in db.query(Parameter).order_by(Parameter.scenario_id, Parameter.id):
|
||||
for param in db.query(Parameter).order_by(
|
||||
Parameter.scenario_id, Parameter.id
|
||||
):
|
||||
grouped[param.scenario_id].append(
|
||||
{
|
||||
"id": param.id,
|
||||
@@ -113,27 +117,20 @@ def _load_parameters(db: Session) -> Dict[str, Any]:
|
||||
|
||||
def _load_costs(db: Session) -> Dict[str, Any]:
|
||||
capex_grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for capex in (
|
||||
db.query(Capex)
|
||||
.order_by(Capex.scenario_id, Capex.id)
|
||||
.all()
|
||||
):
|
||||
for capex in db.query(Capex).order_by(Capex.scenario_id, Capex.id).all():
|
||||
capex_grouped[int(getattr(capex, "scenario_id"))].append(
|
||||
{
|
||||
"id": int(getattr(capex, "id")),
|
||||
"scenario_id": int(getattr(capex, "scenario_id")),
|
||||
"amount": float(getattr(capex, "amount", 0.0)),
|
||||
"description": getattr(capex, "description", "") or "",
|
||||
"currency_code": getattr(capex, "currency_code", "USD") or "USD",
|
||||
"currency_code": getattr(capex, "currency_code", "USD")
|
||||
or "USD",
|
||||
}
|
||||
)
|
||||
|
||||
opex_grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for opex in (
|
||||
db.query(Opex)
|
||||
.order_by(Opex.scenario_id, Opex.id)
|
||||
.all()
|
||||
):
|
||||
for opex in db.query(Opex).order_by(Opex.scenario_id, Opex.id).all():
|
||||
opex_grouped[int(getattr(opex, "scenario_id"))].append(
|
||||
{
|
||||
"id": int(getattr(opex, "id")),
|
||||
@@ -152,9 +149,15 @@ def _load_costs(db: Session) -> Dict[str, Any]:
|
||||
|
||||
def _load_currencies(db: Session) -> Dict[str, Any]:
|
||||
items: list[Dict[str, Any]] = []
|
||||
for c in db.query(Currency).filter_by(is_active=True).order_by(Currency.code).all():
|
||||
for c in (
|
||||
db.query(Currency)
|
||||
.filter_by(is_active=True)
|
||||
.order_by(Currency.code)
|
||||
.all()
|
||||
):
|
||||
items.append(
|
||||
{"id": c.code, "name": f"{c.name} ({c.code})", "symbol": c.symbol})
|
||||
{"id": c.code, "name": f"{c.name} ({c.code})", "symbol": c.symbol}
|
||||
)
|
||||
if not items:
|
||||
items.append({"id": "USD", "name": "US Dollar (USD)", "symbol": "$"})
|
||||
return {"currency_options": items}
|
||||
@@ -261,9 +264,7 @@ def _load_production(db: Session) -> Dict[str, Any]:
|
||||
def _load_equipment(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for record in (
|
||||
db.query(Equipment)
|
||||
.order_by(Equipment.scenario_id, Equipment.id)
|
||||
.all()
|
||||
db.query(Equipment).order_by(Equipment.scenario_id, Equipment.id).all()
|
||||
):
|
||||
record_id = int(getattr(record, "id"))
|
||||
scenario_id = int(getattr(record, "scenario_id"))
|
||||
@@ -291,8 +292,9 @@ def _load_maintenance(db: Session) -> Dict[str, Any]:
|
||||
scenario_id = int(getattr(record, "scenario_id"))
|
||||
equipment_id = int(getattr(record, "equipment_id"))
|
||||
equipment_obj = getattr(record, "equipment", None)
|
||||
equipment_name = getattr(
|
||||
equipment_obj, "name", "") if equipment_obj else ""
|
||||
equipment_name = (
|
||||
getattr(equipment_obj, "name", "") if equipment_obj else ""
|
||||
)
|
||||
maintenance_date = getattr(record, "maintenance_date", None)
|
||||
cost_value = float(getattr(record, "cost", 0.0))
|
||||
description = getattr(record, "description", "") or ""
|
||||
@@ -303,7 +305,9 @@ def _load_maintenance(db: Session) -> Dict[str, Any]:
|
||||
"scenario_id": scenario_id,
|
||||
"equipment_id": equipment_id,
|
||||
"equipment_name": equipment_name,
|
||||
"maintenance_date": maintenance_date.isoformat() if maintenance_date else "",
|
||||
"maintenance_date": (
|
||||
maintenance_date.isoformat() if maintenance_date else ""
|
||||
),
|
||||
"cost": cost_value,
|
||||
"description": description,
|
||||
}
|
||||
@@ -339,8 +343,11 @@ def _load_simulations(db: Session) -> Dict[str, Any]:
|
||||
for item in scenarios:
|
||||
scenario_id = int(item["id"])
|
||||
scenario_results = results_grouped.get(scenario_id, [])
|
||||
summary = generate_report(
|
||||
scenario_results) if scenario_results else generate_report([])
|
||||
summary = (
|
||||
generate_report(scenario_results)
|
||||
if scenario_results
|
||||
else generate_report([])
|
||||
)
|
||||
runs.append(
|
||||
{
|
||||
"scenario_id": scenario_id,
|
||||
@@ -395,11 +402,11 @@ def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
simulation_context = _load_simulations(db)
|
||||
simulation_runs = simulation_context["simulation_runs"]
|
||||
|
||||
runs_by_scenario = {
|
||||
run["scenario_id"]: run for run in simulation_runs
|
||||
}
|
||||
runs_by_scenario = {run["scenario_id"]: run for run in simulation_runs}
|
||||
|
||||
def sum_amounts(grouped: Dict[int, list[Dict[str, Any]]], field: str = "amount") -> float:
|
||||
def sum_amounts(
|
||||
grouped: Dict[int, list[Dict[str, Any]]], field: str = "amount"
|
||||
) -> float:
|
||||
total = 0.0
|
||||
for items in grouped.values():
|
||||
for item in items:
|
||||
@@ -414,14 +421,18 @@ def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
total_production = sum_amounts(production_by_scenario)
|
||||
total_maintenance_cost = sum_amounts(maintenance_by_scenario, field="cost")
|
||||
|
||||
total_parameters = sum(len(items)
|
||||
for items in parameters_by_scenario.values())
|
||||
total_equipment = sum(len(items)
|
||||
for items in equipment_by_scenario.values())
|
||||
total_maintenance_events = sum(len(items)
|
||||
for items in maintenance_by_scenario.values())
|
||||
total_parameters = sum(
|
||||
len(items) for items in parameters_by_scenario.values()
|
||||
)
|
||||
total_equipment = sum(
|
||||
len(items) for items in equipment_by_scenario.values()
|
||||
)
|
||||
total_maintenance_events = sum(
|
||||
len(items) for items in maintenance_by_scenario.values()
|
||||
)
|
||||
total_simulation_iterations = sum(
|
||||
run["iterations"] for run in simulation_runs)
|
||||
run["iterations"] for run in simulation_runs
|
||||
)
|
||||
|
||||
scenario_rows: list[Dict[str, Any]] = []
|
||||
scenario_labels: list[str] = []
|
||||
@@ -501,20 +512,40 @@ def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
overall_report = generate_report(all_simulation_results)
|
||||
|
||||
overall_report_metrics = [
|
||||
{"label": "Runs", "value": _format_int(
|
||||
int(overall_report.get("count", 0)))},
|
||||
{"label": "Mean", "value": _format_decimal(
|
||||
float(overall_report.get("mean", 0.0)))},
|
||||
{"label": "Median", "value": _format_decimal(
|
||||
float(overall_report.get("median", 0.0)))},
|
||||
{"label": "Std Dev", "value": _format_decimal(
|
||||
float(overall_report.get("std_dev", 0.0)))},
|
||||
{"label": "95th Percentile", "value": _format_decimal(
|
||||
float(overall_report.get("percentile_95", 0.0)))},
|
||||
{"label": "VaR (95%)", "value": _format_decimal(
|
||||
float(overall_report.get("value_at_risk_95", 0.0)))},
|
||||
{"label": "Expected Shortfall (95%)", "value": _format_decimal(
|
||||
float(overall_report.get("expected_shortfall_95", 0.0)))},
|
||||
{
|
||||
"label": "Runs",
|
||||
"value": _format_int(int(overall_report.get("count", 0))),
|
||||
},
|
||||
{
|
||||
"label": "Mean",
|
||||
"value": _format_decimal(float(overall_report.get("mean", 0.0))),
|
||||
},
|
||||
{
|
||||
"label": "Median",
|
||||
"value": _format_decimal(float(overall_report.get("median", 0.0))),
|
||||
},
|
||||
{
|
||||
"label": "Std Dev",
|
||||
"value": _format_decimal(float(overall_report.get("std_dev", 0.0))),
|
||||
},
|
||||
{
|
||||
"label": "95th Percentile",
|
||||
"value": _format_decimal(
|
||||
float(overall_report.get("percentile_95", 0.0))
|
||||
),
|
||||
},
|
||||
{
|
||||
"label": "VaR (95%)",
|
||||
"value": _format_decimal(
|
||||
float(overall_report.get("value_at_risk_95", 0.0))
|
||||
),
|
||||
},
|
||||
{
|
||||
"label": "Expected Shortfall (95%)",
|
||||
"value": _format_decimal(
|
||||
float(overall_report.get("expected_shortfall_95", 0.0))
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
recent_simulations: list[Dict[str, Any]] = [
|
||||
@@ -522,8 +553,12 @@ def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
"scenario_name": run["scenario_name"],
|
||||
"iterations": run["iterations"],
|
||||
"iterations_display": _format_int(run["iterations"]),
|
||||
"mean_display": _format_decimal(float(run["summary"].get("mean", 0.0))),
|
||||
"p95_display": _format_decimal(float(run["summary"].get("percentile_95", 0.0))),
|
||||
"mean_display": _format_decimal(
|
||||
float(run["summary"].get("mean", 0.0))
|
||||
),
|
||||
"p95_display": _format_decimal(
|
||||
float(run["summary"].get("percentile_95", 0.0))
|
||||
),
|
||||
}
|
||||
for run in simulation_runs
|
||||
if run["iterations"] > 0
|
||||
@@ -541,10 +576,20 @@ def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
maintenance_date = getattr(record, "maintenance_date", None)
|
||||
upcoming_maintenance.append(
|
||||
{
|
||||
"scenario_name": getattr(getattr(record, "scenario", None), "name", "Unknown"),
|
||||
"equipment_name": getattr(getattr(record, "equipment", None), "name", "Unknown"),
|
||||
"date_display": maintenance_date.strftime("%Y-%m-%d") if maintenance_date else "—",
|
||||
"cost_display": _format_currency(float(getattr(record, "cost", 0.0))),
|
||||
"scenario_name": getattr(
|
||||
getattr(record, "scenario", None), "name", "Unknown"
|
||||
),
|
||||
"equipment_name": getattr(
|
||||
getattr(record, "equipment", None), "name", "Unknown"
|
||||
),
|
||||
"date_display": (
|
||||
maintenance_date.strftime("%Y-%m-%d")
|
||||
if maintenance_date
|
||||
else "—"
|
||||
),
|
||||
"cost_display": _format_currency(
|
||||
float(getattr(record, "cost", 0.0))
|
||||
),
|
||||
"description": getattr(record, "description", "") or "—",
|
||||
}
|
||||
)
|
||||
@@ -552,9 +597,9 @@ def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
cost_chart_has_data = any(value > 0 for value in scenario_capex) or any(
|
||||
value > 0 for value in scenario_opex
|
||||
)
|
||||
activity_chart_has_data = any(value > 0 for value in activity_production) or any(
|
||||
value > 0 for value in activity_consumption
|
||||
)
|
||||
activity_chart_has_data = any(
|
||||
value > 0 for value in activity_production
|
||||
) or any(value > 0 for value in activity_consumption)
|
||||
|
||||
scenario_cost_chart: Dict[str, list[Any]] = {
|
||||
"labels": scenario_labels,
|
||||
@@ -573,14 +618,20 @@ def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
{"label": "CAPEX Total", "value": _format_currency(total_capex)},
|
||||
{"label": "OPEX Total", "value": _format_currency(total_opex)},
|
||||
{"label": "Equipment Assets", "value": _format_int(total_equipment)},
|
||||
{"label": "Maintenance Events",
|
||||
"value": _format_int(total_maintenance_events)},
|
||||
{
|
||||
"label": "Maintenance Events",
|
||||
"value": _format_int(total_maintenance_events),
|
||||
},
|
||||
{"label": "Consumption", "value": _format_decimal(total_consumption)},
|
||||
{"label": "Production", "value": _format_decimal(total_production)},
|
||||
{"label": "Simulation Iterations",
|
||||
"value": _format_int(total_simulation_iterations)},
|
||||
{"label": "Maintenance Cost",
|
||||
"value": _format_currency(total_maintenance_cost)},
|
||||
{
|
||||
"label": "Simulation Iterations",
|
||||
"value": _format_int(total_simulation_iterations),
|
||||
},
|
||||
{
|
||||
"label": "Maintenance Cost",
|
||||
"value": _format_currency(total_maintenance_cost),
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
@@ -704,3 +755,30 @@ async def currencies_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the currency administration page with full currency context."""
|
||||
context = _load_currency_settings(db)
|
||||
return _render(request, "currencies.html", context)
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
return _render(request, "login.html")
|
||||
|
||||
|
||||
@router.get("/register", response_class=HTMLResponse)
|
||||
async def register_page(request: Request):
|
||||
return _render(request, "register.html")
|
||||
|
||||
|
||||
@router.get("/profile", response_class=HTMLResponse)
|
||||
async def profile_page(request: Request):
|
||||
return _render(request, "profile.html")
|
||||
|
||||
|
||||
@router.get("/forgot-password", response_class=HTMLResponse)
|
||||
async def forgot_password_page(request: Request):
|
||||
return _render(request, "forgot_password.html")
|
||||
|
||||
|
||||
@router.get("/theme-settings", response_class=HTMLResponse)
|
||||
async def theme_settings_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the theme settings page."""
|
||||
context = _load_css_settings(db)
|
||||
return _render(request, "theme_settings.html", context)
|
||||
|
||||
126
routes/users.py
Normal file
126
routes/users.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import get_db
|
||||
from models.user import User
|
||||
from services.security import get_password_hash, verify_password, create_access_token, SECRET_KEY, ALGORITHM
|
||||
from jose import jwt, JWTError
|
||||
from schemas.user import UserCreate, UserInDB, UserLogin, UserUpdate, PasswordResetRequest, PasswordReset, Token
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="users/login")
|
||||
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserInDB, status_code=status.HTTP_201_CREATED)
|
||||
async def register_user(user: UserCreate, db: Session = Depends(get_db)):
|
||||
db_user = db.query(User).filter(User.username == user.username).first()
|
||||
if db_user:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already registered")
|
||||
db_user = db.query(User).filter(User.email == user.email).first()
|
||||
if db_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
|
||||
|
||||
# Get or create default role
|
||||
from models.role import Role
|
||||
default_role = db.query(Role).filter(Role.name == "user").first()
|
||||
if not default_role:
|
||||
default_role = Role(name="user")
|
||||
db.add(default_role)
|
||||
db.commit()
|
||||
db.refresh(default_role)
|
||||
|
||||
new_user = User(username=user.username, email=user.email,
|
||||
role_id=default_role.id)
|
||||
new_user.set_password(user.password)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
return new_user
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login_user(user: UserLogin, db: Session = Depends(get_db)):
|
||||
db_user = db.query(User).filter(User.username == user.username).first()
|
||||
if not db_user or not db_user.check_password(user.password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password")
|
||||
access_token = create_access_token(subject=db_user.username)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def read_users_me(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
@router.put("/me", response_model=UserInDB)
|
||||
async def update_user_me(user_update: UserUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
if user_update.username and user_update.username != current_user.username:
|
||||
existing_user = db.query(User).filter(
|
||||
User.username == user_update.username).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken")
|
||||
current_user.username = user_update.username
|
||||
|
||||
if user_update.email and user_update.email != current_user.email:
|
||||
existing_user = db.query(User).filter(
|
||||
User.email == user_update.email).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
|
||||
current_user.email = user_update.email
|
||||
|
||||
if user_update.password:
|
||||
current_user.set_password(user_update.password)
|
||||
|
||||
db.add(current_user)
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(request: PasswordResetRequest):
|
||||
# In a real application, this would send an email with a reset token
|
||||
return {"message": "Password reset email sent (not really)"}
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(request: PasswordReset, db: Session = Depends(get_db)):
|
||||
# In a real application, the token would be verified
|
||||
user = db.query(User).filter(User.username ==
|
||||
request.token).first() # Use token as username for test
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token or user")
|
||||
user.set_password(request.new_password)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return {"message": "Password has been reset successfully"}
|
||||
Reference in New Issue
Block a user