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:
@@ -34,6 +34,7 @@ TestingSessionLocal = sessionmaker(
|
||||
def setup_database() -> Generator[None, None, None]:
|
||||
# Ensure all model metadata is registered before creating tables
|
||||
from models import (
|
||||
application_setting,
|
||||
capex,
|
||||
consumption,
|
||||
distribution,
|
||||
@@ -52,6 +53,7 @@ def setup_database() -> Generator[None, None, None]:
|
||||
distribution,
|
||||
equipment,
|
||||
maintenance,
|
||||
application_setting,
|
||||
opex,
|
||||
parameters,
|
||||
production_output,
|
||||
|
||||
53
tests/unit/test_settings_routes.py
Normal file
53
tests/unit/test_settings_routes.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from services import settings as settings_service
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("db_session")
|
||||
def test_read_css_settings_reflects_env_overrides(
|
||||
api_client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
env_var = settings_service.css_key_to_env_var("--color-background")
|
||||
monkeypatch.setenv(env_var, "#123456")
|
||||
|
||||
response = api_client.get("/api/settings/css")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
|
||||
assert body["variables"]["--color-background"] == "#123456"
|
||||
assert body["env_overrides"]["--color-background"] == "#123456"
|
||||
assert any(
|
||||
source["env_var"] == env_var and source["value"] == "#123456"
|
||||
for source in body["env_sources"]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("db_session")
|
||||
def test_update_css_settings_persists_changes(
|
||||
api_client: TestClient, db_session: Session
|
||||
) -> None:
|
||||
payload = {"variables": {"--color-primary": "#112233"}}
|
||||
|
||||
response = api_client.put("/api/settings/css", json=payload)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
|
||||
assert body["variables"]["--color-primary"] == "#112233"
|
||||
|
||||
persisted = settings_service.get_css_color_settings(db_session)
|
||||
assert persisted["--color-primary"] == "#112233"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("db_session")
|
||||
def test_update_css_settings_invalid_value_returns_422(
|
||||
api_client: TestClient
|
||||
) -> None:
|
||||
response = api_client.put(
|
||||
"/api/settings/css",
|
||||
json={"variables": {"--color-primary": "not-a-color"}},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
body = response.json()
|
||||
assert "color" in body["detail"].lower()
|
||||
137
tests/unit/test_settings_service.py
Normal file
137
tests/unit/test_settings_service.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from types import SimpleNamespace
|
||||
from typing import Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.application_setting import ApplicationSetting
|
||||
from services import settings as settings_service
|
||||
from services.settings import CSS_COLOR_DEFAULTS
|
||||
|
||||
|
||||
@pytest.fixture(name="clean_env")
|
||||
def fixture_clean_env(monkeypatch: pytest.MonkeyPatch) -> Dict[str, str]:
|
||||
"""Provide an isolated environment mapping for tests."""
|
||||
|
||||
env: Dict[str, str] = {}
|
||||
monkeypatch.setattr(settings_service, "os", SimpleNamespace(environ=env))
|
||||
return env
|
||||
|
||||
|
||||
def test_css_key_to_env_var_formatting():
|
||||
assert settings_service.css_key_to_env_var("--color-background") == "CALMINER_THEME_COLOR_BACKGROUND"
|
||||
assert settings_service.css_key_to_env_var("--color-primary-stronger") == "CALMINER_THEME_COLOR_PRIMARY_STRONGER"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"env_key,env_value",
|
||||
[
|
||||
("--color-background", "#ffffff"),
|
||||
("--color-primary", "rgb(10, 20, 30)"),
|
||||
("--color-accent", "rgba(1,2,3,0.5)"),
|
||||
("--color-text-secondary", "hsla(210, 40%, 40%, 1)"),
|
||||
],
|
||||
)
|
||||
def test_read_css_color_env_overrides_valid_values(clean_env, env_key, env_value):
|
||||
env_var = settings_service.css_key_to_env_var(env_key)
|
||||
clean_env[env_var] = env_value
|
||||
|
||||
overrides = settings_service.read_css_color_env_overrides(clean_env)
|
||||
assert overrides[env_key] == env_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_value",
|
||||
[
|
||||
"", # empty
|
||||
"not-a-color", # arbitrary string
|
||||
"#12", # short hex
|
||||
"rgb(1,2)", # malformed rgb
|
||||
],
|
||||
)
|
||||
def test_read_css_color_env_overrides_invalid_values_raise(clean_env, invalid_value):
|
||||
env_var = settings_service.css_key_to_env_var("--color-background")
|
||||
clean_env[env_var] = invalid_value
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
settings_service.read_css_color_env_overrides(clean_env)
|
||||
|
||||
|
||||
def test_read_css_color_env_overrides_ignores_missing(clean_env):
|
||||
overrides = settings_service.read_css_color_env_overrides(clean_env)
|
||||
assert overrides == {}
|
||||
|
||||
|
||||
def test_list_css_env_override_rows_returns_structured_data(clean_env):
|
||||
clean_env[settings_service.css_key_to_env_var("--color-primary")] = "#123456"
|
||||
rows = settings_service.list_css_env_override_rows(clean_env)
|
||||
assert rows == [
|
||||
{
|
||||
"css_key": "--color-primary",
|
||||
"env_var": settings_service.css_key_to_env_var("--color-primary"),
|
||||
"value": "#123456",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_normalize_color_value_strips_and_validates():
|
||||
assert settings_service._normalize_color_value(" #abcdef ") == "#abcdef"
|
||||
with pytest.raises(ValueError):
|
||||
settings_service._normalize_color_value(123) # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
settings_service._normalize_color_value(" ")
|
||||
with pytest.raises(ValueError):
|
||||
settings_service._normalize_color_value("#12")
|
||||
|
||||
|
||||
def test_ensure_css_color_settings_creates_defaults(db_session: Session):
|
||||
settings_service.ensure_css_color_settings(db_session)
|
||||
|
||||
stored = {
|
||||
record.key: record.value
|
||||
for record in db_session.query(ApplicationSetting).all()
|
||||
}
|
||||
assert set(stored.keys()) == set(CSS_COLOR_DEFAULTS.keys())
|
||||
assert stored == CSS_COLOR_DEFAULTS
|
||||
|
||||
|
||||
def test_update_css_color_settings_persists_changes(db_session: Session):
|
||||
settings_service.ensure_css_color_settings(db_session)
|
||||
|
||||
updated = settings_service.update_css_color_settings(
|
||||
db_session,
|
||||
{"--color-background": "#000000", "--color-accent": "#abcdef"},
|
||||
)
|
||||
|
||||
assert updated["--color-background"] == "#000000"
|
||||
assert updated["--color-accent"] == "#abcdef"
|
||||
|
||||
stored = {
|
||||
record.key: record.value
|
||||
for record in db_session.query(ApplicationSetting).all()
|
||||
}
|
||||
assert stored["--color-background"] == "#000000"
|
||||
assert stored["--color-accent"] == "#abcdef"
|
||||
|
||||
|
||||
def test_get_css_color_settings_respects_env_overrides(
|
||||
db_session: Session, clean_env: Dict[str, str]
|
||||
):
|
||||
settings_service.ensure_css_color_settings(db_session)
|
||||
override_value = "#112233"
|
||||
clean_env[settings_service.css_key_to_env_var("--color-background")] = (
|
||||
override_value
|
||||
)
|
||||
|
||||
values = settings_service.get_css_color_settings(db_session)
|
||||
|
||||
assert values["--color-background"] == override_value
|
||||
|
||||
db_value = (
|
||||
db_session.query(ApplicationSetting)
|
||||
.filter_by(key="--color-background")
|
||||
.one()
|
||||
.value
|
||||
)
|
||||
assert db_value != override_value
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from models.scenario import Scenario
|
||||
from services import settings as settings_service
|
||||
|
||||
|
||||
def test_dashboard_route_provides_summary(
|
||||
@@ -129,3 +130,36 @@ def test_additional_ui_routes_render_templates(
|
||||
|
||||
context = cast(Dict[str, Any], getattr(response, "context", {}))
|
||||
assert context
|
||||
|
||||
|
||||
def test_settings_route_provides_css_context(
|
||||
api_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_var = settings_service.css_key_to_env_var("--color-accent")
|
||||
monkeypatch.setenv(env_var, "#abcdef")
|
||||
|
||||
response = api_client.get("/ui/settings")
|
||||
assert response.status_code == 200
|
||||
|
||||
template = getattr(response, "template", None)
|
||||
assert template is not None
|
||||
assert template.name == "settings.html"
|
||||
|
||||
context = cast(Dict[str, Any], getattr(response, "context", {}))
|
||||
assert "css_variables" in context
|
||||
assert "css_defaults" in context
|
||||
assert "css_env_overrides" in context
|
||||
assert "css_env_override_rows" in context
|
||||
assert "css_env_override_meta" in context
|
||||
|
||||
assert context["css_variables"]["--color-accent"] == "#abcdef"
|
||||
assert context["css_defaults"]["--color-accent"] == settings_service.CSS_COLOR_DEFAULTS["--color-accent"]
|
||||
assert context["css_env_overrides"]["--color-accent"] == "#abcdef"
|
||||
|
||||
override_rows = context["css_env_override_rows"]
|
||||
assert any(row["env_var"] == env_var for row in override_rows)
|
||||
|
||||
meta = context["css_env_override_meta"]["--color-accent"]
|
||||
assert meta["value"] == "#abcdef"
|
||||
assert meta["env_var"] == env_var
|
||||
|
||||
Reference in New Issue
Block a user