feat: Add application-level settings for CSS color management
Some checks failed
Run Tests / test (push) Failing after 1m51s
Some checks failed
Run Tests / test (push) Failing after 1m51s
- Introduced a new table `application_setting` to store configurable application options. - Implemented functions to manage CSS color settings, including loading, updating, and reading environment overrides. - Added a new settings view to render and manage theme colors. - Updated UI to include a settings page with theme color management and environment overrides display. - Enhanced CSS styles for the settings page and sidebar navigation. - Created unit and end-to-end tests for the new settings functionality and CSS management.
This commit is contained in:
85
routes/settings.py
Normal file
85
routes/settings.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from routes.dependencies import get_db
|
||||
from services.settings import (
|
||||
CSS_COLOR_DEFAULTS,
|
||||
get_css_color_settings,
|
||||
list_css_env_override_rows,
|
||||
read_css_color_env_overrides,
|
||||
update_css_color_settings,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["Settings"])
|
||||
|
||||
|
||||
class CSSSettingsPayload(BaseModel):
|
||||
variables: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_allowed_keys(self) -> "CSSSettingsPayload":
|
||||
invalid = set(self.variables.keys()) - set(CSS_COLOR_DEFAULTS.keys())
|
||||
if invalid:
|
||||
invalid_keys = ", ".join(sorted(invalid))
|
||||
raise ValueError(
|
||||
f"Unsupported CSS variables: {invalid_keys}."
|
||||
" Accepted keys align with the default theme variables."
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class EnvOverride(BaseModel):
|
||||
css_key: str
|
||||
env_var: str
|
||||
value: str
|
||||
|
||||
|
||||
class CSSSettingsResponse(BaseModel):
|
||||
variables: Dict[str, str]
|
||||
env_overrides: Dict[str, str] = Field(default_factory=dict)
|
||||
env_sources: List[EnvOverride] = Field(default_factory=list)
|
||||
|
||||
|
||||
@router.get("/css", response_model=CSSSettingsResponse)
|
||||
def read_css_settings(db: Session = Depends(get_db)) -> CSSSettingsResponse:
|
||||
try:
|
||||
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()
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
return CSSSettingsResponse(
|
||||
variables=values,
|
||||
env_overrides=env_overrides,
|
||||
env_sources=env_sources,
|
||||
)
|
||||
|
||||
|
||||
@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()
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
return CSSSettingsResponse(
|
||||
variables=values,
|
||||
env_overrides=env_overrides,
|
||||
env_sources=env_sources,
|
||||
)
|
||||
27
routes/ui.py
27
routes/ui.py
@@ -20,6 +20,12 @@ from routes.dependencies import get_db
|
||||
from services.reporting import generate_report
|
||||
from models.currency import Currency
|
||||
from routes.currencies import DEFAULT_CURRENCY_CODE, _ensure_default_currency
|
||||
from services.settings import (
|
||||
CSS_COLOR_DEFAULTS,
|
||||
get_css_color_settings,
|
||||
list_css_env_override_rows,
|
||||
read_css_color_env_overrides,
|
||||
)
|
||||
|
||||
|
||||
CURRENCY_CHOICES: list[Dict[str, Any]] = [
|
||||
@@ -186,6 +192,20 @@ def _load_currency_settings(db: Session) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _load_css_settings(db: Session) -> Dict[str, Any]:
|
||||
variables = get_css_color_settings(db)
|
||||
env_overrides = read_css_color_env_overrides()
|
||||
env_rows = list_css_env_override_rows()
|
||||
env_meta = {row["css_key"]: row for row in env_rows}
|
||||
return {
|
||||
"css_variables": variables,
|
||||
"css_defaults": CSS_COLOR_DEFAULTS,
|
||||
"css_env_overrides": env_overrides,
|
||||
"css_env_override_rows": env_rows,
|
||||
"css_env_override_meta": env_meta,
|
||||
}
|
||||
|
||||
|
||||
def _load_consumption(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for record in (
|
||||
@@ -672,6 +692,13 @@ async def reporting_view(request: Request, db: Session = Depends(get_db)):
|
||||
return _render(request, "reporting.html", _load_reporting(db))
|
||||
|
||||
|
||||
@router.get("/ui/settings", response_class=HTMLResponse)
|
||||
async def settings_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the settings landing page."""
|
||||
context = _load_css_settings(db)
|
||||
return _render(request, "settings.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/currencies", response_class=HTMLResponse)
|
||||
async def currencies_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the currency administration page with full currency context."""
|
||||
|
||||
Reference in New Issue
Block a user