feat: Enhance project and scenario creation with monitoring metrics
- Added monitoring metrics for project creation success and error handling in `ProjectRepository`. - Implemented similar monitoring for scenario creation in `ScenarioRepository`. - Refactored `run_monte_carlo` function in `simulation.py` to include timing and success/error metrics. - Introduced new CSS styles for headers, alerts, and navigation buttons in `main.css` and `projects.css`. - Created a new JavaScript file for navigation logic to handle chevron buttons. - Updated HTML templates to include new navigation buttons and improved styling for buttons. - Added tests for reporting service and routes to ensure proper functionality and access control. - Removed unused imports and optimized existing test files for better clarity and performance.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
@@ -12,7 +12,6 @@ from dependencies import (
|
||||
get_unit_of_work,
|
||||
require_any_role,
|
||||
require_project_resource,
|
||||
require_roles,
|
||||
require_scenario_resource,
|
||||
)
|
||||
from models import Project, Scenario, User
|
||||
@@ -30,6 +29,93 @@ from services.unit_of_work import UnitOfWork
|
||||
router = APIRouter(prefix="/reports", tags=["Reports"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
# Add custom Jinja2 filters
|
||||
|
||||
|
||||
def format_datetime(value):
|
||||
"""Format a datetime object for display in templates."""
|
||||
if not isinstance(value, datetime):
|
||||
return ""
|
||||
if value.tzinfo is None:
|
||||
# Assume UTC if no timezone
|
||||
from datetime import timezone
|
||||
value = value.replace(tzinfo=timezone.utc)
|
||||
# Format as readable date/time
|
||||
return value.strftime("%Y-%m-%d %H:%M UTC")
|
||||
|
||||
|
||||
def currency_display(value, currency_code):
|
||||
"""Format a numeric value with currency symbol/code."""
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
# Format the number
|
||||
if isinstance(value, (int, float)):
|
||||
formatted_value = f"{value:,.2f}"
|
||||
else:
|
||||
formatted_value = str(value)
|
||||
|
||||
# Add currency code
|
||||
if currency_code:
|
||||
return f"{currency_code} {formatted_value}"
|
||||
return formatted_value
|
||||
|
||||
|
||||
def format_metric(value, metric_name, currency_code=None):
|
||||
"""Format metric values appropriately based on metric type."""
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
# For currency-related metrics, use currency formatting
|
||||
currency_metrics = {'npv', 'inflows', 'outflows',
|
||||
'net', 'total_inflows', 'total_outflows', 'total_net'}
|
||||
if metric_name in currency_metrics and currency_code:
|
||||
return currency_display(value, currency_code)
|
||||
|
||||
# For percentage metrics
|
||||
percentage_metrics = {'irr', 'payback_period'}
|
||||
if metric_name in percentage_metrics:
|
||||
if isinstance(value, (int, float)):
|
||||
return f"{value:.2f}%"
|
||||
return f"{value}%"
|
||||
|
||||
# Default numeric formatting
|
||||
if isinstance(value, (int, float)):
|
||||
return f"{value:,.2f}"
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
def percentage_display(value):
|
||||
"""Format a value as a percentage."""
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
if isinstance(value, (int, float)):
|
||||
return f"{value:.2f}%"
|
||||
|
||||
return f"{value}%"
|
||||
|
||||
|
||||
def period_display(value):
|
||||
"""Format a period value (like payback period)."""
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
if isinstance(value, (int, float)):
|
||||
if value == int(value):
|
||||
return f"{int(value)} years"
|
||||
return f"{value:.1f} years"
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
templates.env.filters['format_datetime'] = format_datetime
|
||||
templates.env.filters['currency_display'] = currency_display
|
||||
templates.env.filters['format_metric'] = format_metric
|
||||
templates.env.filters['percentage_display'] = percentage_display
|
||||
templates.env.filters['period_display'] = period_display
|
||||
|
||||
READ_ROLES = ("viewer", "analyst", "project_manager", "admin")
|
||||
MANAGE_ROLES = ("project_manager", "admin")
|
||||
|
||||
@@ -296,35 +382,9 @@ def project_summary_page(
|
||||
)
|
||||
|
||||
service = ReportingService(uow)
|
||||
report = service.project_summary(
|
||||
project,
|
||||
filters=scenario_filter,
|
||||
include=include_options,
|
||||
iterations=iterations or DEFAULT_ITERATIONS,
|
||||
percentiles=percentile_values,
|
||||
context = service.build_project_summary_context(
|
||||
project, scenario_filter, include_options, iterations or DEFAULT_ITERATIONS, percentile_values, request
|
||||
)
|
||||
context = {
|
||||
"request": request,
|
||||
"project": report["project"],
|
||||
"scenario_count": report["scenario_count"],
|
||||
"aggregates": report["aggregates"],
|
||||
"scenarios": report["scenarios"],
|
||||
"filters": report["filters"],
|
||||
"include_options": include_options,
|
||||
"iterations": iterations or DEFAULT_ITERATIONS,
|
||||
"percentiles": percentile_values,
|
||||
"title": f"Project Summary · {project.name}",
|
||||
"subtitle": "Aggregated financial and simulation insights across scenarios.",
|
||||
"actions": [
|
||||
{
|
||||
"href": request.url_for(
|
||||
"reports.project_summary",
|
||||
project_id=project.id,
|
||||
),
|
||||
"label": "Download JSON",
|
||||
}
|
||||
],
|
||||
}
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"reports/project_summary.html",
|
||||
@@ -399,40 +459,9 @@ def project_scenario_comparison_page(
|
||||
)
|
||||
|
||||
service = ReportingService(uow)
|
||||
report = service.scenario_comparison(
|
||||
project,
|
||||
scenarios,
|
||||
include=include_options,
|
||||
iterations=iterations or DEFAULT_ITERATIONS,
|
||||
percentiles=percentile_values,
|
||||
context = service.build_scenario_comparison_context(
|
||||
project, scenarios, include_options, iterations or DEFAULT_ITERATIONS, percentile_values, request
|
||||
)
|
||||
comparison_json_url = request.url_for(
|
||||
"reports.project_scenario_comparison",
|
||||
project_id=project.id,
|
||||
)
|
||||
comparison_query = urlencode(
|
||||
[("scenario_ids", str(identifier)) for identifier in unique_ids]
|
||||
)
|
||||
if comparison_query:
|
||||
comparison_json_url = f"{comparison_json_url}?{comparison_query}"
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"project": report["project"],
|
||||
"scenarios": report["scenarios"],
|
||||
"comparison": report["comparison"],
|
||||
"include_options": include_options,
|
||||
"iterations": iterations or DEFAULT_ITERATIONS,
|
||||
"percentiles": percentile_values,
|
||||
"title": f"Scenario Comparison · {project.name}",
|
||||
"subtitle": "Evaluate deterministic metrics and Monte Carlo trends side by side.",
|
||||
"actions": [
|
||||
{
|
||||
"href": comparison_json_url,
|
||||
"label": "Download JSON",
|
||||
}
|
||||
],
|
||||
}
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"reports/scenario_comparison.html",
|
||||
@@ -478,33 +507,9 @@ def scenario_distribution_page(
|
||||
) from exc
|
||||
|
||||
service = ReportingService(uow)
|
||||
report = service.scenario_distribution(
|
||||
scenario,
|
||||
include=include_options,
|
||||
iterations=iterations or DEFAULT_ITERATIONS,
|
||||
percentiles=percentile_values,
|
||||
context = service.build_scenario_distribution_context(
|
||||
scenario, include_options, iterations or DEFAULT_ITERATIONS, percentile_values, request
|
||||
)
|
||||
context = {
|
||||
"request": request,
|
||||
"scenario": report["scenario"],
|
||||
"summary": report["summary"],
|
||||
"metrics": report["metrics"],
|
||||
"monte_carlo": report["monte_carlo"],
|
||||
"include_options": include_options,
|
||||
"iterations": iterations or DEFAULT_ITERATIONS,
|
||||
"percentiles": percentile_values,
|
||||
"title": f"Scenario Distribution · {scenario.name}",
|
||||
"subtitle": "Deterministic and simulated distributions for a single scenario.",
|
||||
"actions": [
|
||||
{
|
||||
"href": request.url_for(
|
||||
"reports.scenario_distribution",
|
||||
scenario_id=scenario.id,
|
||||
),
|
||||
"label": "Download JSON",
|
||||
}
|
||||
],
|
||||
}
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"reports/scenario_distribution.html",
|
||||
|
||||
Reference in New Issue
Block a user