- 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.
231 lines
6.9 KiB
Python
231 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from typing import Dict, Mapping
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from models.application_setting import ApplicationSetting
|
|
from models.theme_setting import ThemeSetting # Import ThemeSetting model
|
|
|
|
CSS_COLOR_CATEGORY = "theme"
|
|
CSS_COLOR_VALUE_TYPE = "color"
|
|
CSS_ENV_PREFIX = "CALMINER_THEME_"
|
|
|
|
CSS_COLOR_DEFAULTS: Dict[str, str] = {
|
|
"--color-background": "#f4f5f7",
|
|
"--color-surface": "#ffffff",
|
|
"--color-text-primary": "#2a1f33",
|
|
"--color-text-secondary": "#624769",
|
|
"--color-text-muted": "#64748b",
|
|
"--color-text-subtle": "#94a3b8",
|
|
"--color-text-invert": "#ffffff",
|
|
"--color-text-dark": "#0f172a",
|
|
"--color-text-strong": "#111827",
|
|
"--color-primary": "#5f320d",
|
|
"--color-primary-strong": "#7e4c13",
|
|
"--color-primary-stronger": "#837c15",
|
|
"--color-accent": "#bff838",
|
|
"--color-border": "#e2e8f0",
|
|
"--color-border-strong": "#cbd5e1",
|
|
"--color-highlight": "#eef2ff",
|
|
"--color-panel-shadow": "rgba(15, 23, 42, 0.08)",
|
|
"--color-panel-shadow-deep": "rgba(15, 23, 42, 0.12)",
|
|
"--color-surface-alt": "#f8fafc",
|
|
"--color-success": "#047857",
|
|
"--color-error": "#b91c1c",
|
|
}
|
|
|
|
_COLOR_VALUE_PATTERN = re.compile(
|
|
r"^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\([^)]+\)|hsla?\([^)]+\))$",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def ensure_css_color_settings(db: Session) -> Dict[str, ApplicationSetting]:
|
|
"""Ensure the CSS color defaults exist in the settings table."""
|
|
|
|
existing = (
|
|
db.query(ApplicationSetting)
|
|
.filter(ApplicationSetting.key.in_(CSS_COLOR_DEFAULTS.keys()))
|
|
.all()
|
|
)
|
|
by_key = {setting.key: setting for setting in existing}
|
|
|
|
created = False
|
|
for key, default_value in CSS_COLOR_DEFAULTS.items():
|
|
if key in by_key:
|
|
continue
|
|
setting = ApplicationSetting(
|
|
key=key,
|
|
value=default_value,
|
|
value_type=CSS_COLOR_VALUE_TYPE,
|
|
category=CSS_COLOR_CATEGORY,
|
|
description=f"CSS variable {key}",
|
|
is_editable=True,
|
|
)
|
|
db.add(setting)
|
|
by_key[key] = setting
|
|
created = True
|
|
|
|
if created:
|
|
db.commit()
|
|
for key, setting in by_key.items():
|
|
db.refresh(setting)
|
|
|
|
return by_key
|
|
|
|
|
|
def get_css_color_settings(db: Session) -> Dict[str, str]:
|
|
"""Return CSS color variables, filling missing values with defaults."""
|
|
|
|
settings = ensure_css_color_settings(db)
|
|
values: Dict[str, str] = {
|
|
key: settings[key].value if key in settings else default
|
|
for key, default in CSS_COLOR_DEFAULTS.items()
|
|
}
|
|
|
|
env_overrides = read_css_color_env_overrides(os.environ)
|
|
if env_overrides:
|
|
values.update(env_overrides)
|
|
|
|
return values
|
|
|
|
|
|
def update_css_color_settings(
|
|
db: Session, updates: Mapping[str, str]
|
|
) -> Dict[str, str]:
|
|
"""Persist provided CSS color overrides and return the final values."""
|
|
|
|
if not updates:
|
|
return get_css_color_settings(db)
|
|
|
|
invalid_keys = sorted(set(updates.keys()) - set(CSS_COLOR_DEFAULTS.keys()))
|
|
if invalid_keys:
|
|
invalid_list = ", ".join(invalid_keys)
|
|
raise ValueError(f"Unsupported CSS variables: {invalid_list}")
|
|
|
|
normalized: Dict[str, str] = {}
|
|
for key, value in updates.items():
|
|
normalized[key] = _normalize_color_value(value)
|
|
|
|
settings = ensure_css_color_settings(db)
|
|
changed = False
|
|
|
|
for key, value in normalized.items():
|
|
setting = settings[key]
|
|
if setting.value != value:
|
|
setting.value = value
|
|
changed = True
|
|
if setting.value_type != CSS_COLOR_VALUE_TYPE:
|
|
setting.value_type = CSS_COLOR_VALUE_TYPE
|
|
changed = True
|
|
if setting.category != CSS_COLOR_CATEGORY:
|
|
setting.category = CSS_COLOR_CATEGORY
|
|
changed = True
|
|
if not setting.is_editable:
|
|
setting.is_editable = True
|
|
changed = True
|
|
|
|
if changed:
|
|
db.commit()
|
|
for key in normalized.keys():
|
|
db.refresh(settings[key])
|
|
|
|
return get_css_color_settings(db)
|
|
|
|
|
|
def read_css_color_env_overrides(
|
|
env: Mapping[str, str] | None = None,
|
|
) -> Dict[str, str]:
|
|
"""Return validated CSS overrides sourced from environment variables."""
|
|
|
|
if env is None:
|
|
env = os.environ
|
|
|
|
overrides: Dict[str, str] = {}
|
|
for css_key in CSS_COLOR_DEFAULTS.keys():
|
|
env_name = css_key_to_env_var(css_key)
|
|
raw_value = env.get(env_name)
|
|
if raw_value is None:
|
|
continue
|
|
overrides[css_key] = _normalize_color_value(raw_value)
|
|
|
|
return overrides
|
|
|
|
|
|
def _normalize_color_value(value: str) -> str:
|
|
if not isinstance(value, str):
|
|
raise ValueError("Color value must be a string")
|
|
trimmed = value.strip()
|
|
if not trimmed:
|
|
raise ValueError("Color value cannot be empty")
|
|
if not _COLOR_VALUE_PATTERN.match(trimmed):
|
|
raise ValueError(
|
|
"Color value must be a hex code or an rgb/rgba/hsl/hsla expression"
|
|
)
|
|
_validate_functional_color(trimmed)
|
|
return trimmed
|
|
|
|
|
|
def _validate_functional_color(value: str) -> None:
|
|
lowered = value.lower()
|
|
if lowered.startswith("rgb(") or lowered.startswith("hsl("):
|
|
_ensure_component_count(value, expected=3)
|
|
elif lowered.startswith("rgba(") or lowered.startswith("hsla("):
|
|
_ensure_component_count(value, expected=4)
|
|
|
|
|
|
def _ensure_component_count(value: str, expected: int) -> None:
|
|
if not value.endswith(")"):
|
|
raise ValueError(
|
|
"Color function expressions must end with a closing parenthesis"
|
|
)
|
|
inner = value[value.index("(") + 1: -1]
|
|
parts = [segment.strip() for segment in inner.split(",")]
|
|
if len(parts) != expected:
|
|
raise ValueError(
|
|
"Color function expressions must provide the expected number of components"
|
|
)
|
|
if any(not component for component in parts):
|
|
raise ValueError("Color function components cannot be empty")
|
|
|
|
|
|
def css_key_to_env_var(css_key: str) -> str:
|
|
sanitized = css_key.lstrip("-").replace("-", "_").upper()
|
|
return f"{CSS_ENV_PREFIX}{sanitized}"
|
|
|
|
|
|
def list_css_env_override_rows(
|
|
env: Mapping[str, str] | None = None,
|
|
) -> list[Dict[str, str]]:
|
|
overrides = read_css_color_env_overrides(env)
|
|
rows: list[Dict[str, str]] = []
|
|
for css_key, value in overrides.items():
|
|
rows.append(
|
|
{
|
|
"css_key": css_key,
|
|
"env_var": css_key_to_env_var(css_key),
|
|
"value": value,
|
|
}
|
|
)
|
|
return rows
|
|
|
|
|
|
def save_theme_settings(db: Session, theme_data: dict):
|
|
theme = db.query(ThemeSetting).first() or ThemeSetting()
|
|
for key, value in theme_data.items():
|
|
setattr(theme, key, value)
|
|
db.add(theme)
|
|
db.commit()
|
|
db.refresh(theme)
|
|
return theme
|
|
|
|
|
|
def get_theme_settings(db: Session):
|
|
theme = db.query(ThemeSetting).first()
|
|
if theme:
|
|
return {c.name: getattr(theme, c.name) for c in theme.__table__.columns}
|
|
return {}
|