← Files ClaraARCHIVED FILE
modules/business-planning/scripts/planning_interaction.py
4.02 KB · Oct 6, 2026 · 06:02 UTC
"""Switch between author-selected, checked tables without browser arithmetic.
ID/shape checks and shared scales are mechanical; period comparability, scenario
meaning and the explanation remain the report author's professional judgment.
"""
from __future__ import annotations
import html
from pathlib import Path
from typing import Any, Callable
from planning_workflow import indexed, require
__all__ = ["validate_groups", "render_group", "interaction_assets"]
def validate_groups(presentation: dict[str, Any], tables: dict[str, Any]) -> None:
"""Require unambiguous views bound to existing monetary comparison tables."""
groups = indexed(presentation.get("comparison_groups", []), "comparison group")
assigned: set[str] = set()
for group in groups.values():
require(
set(group) == {"id", "title", "views"}, "Unexpected comparison group fields"
)
require(
isinstance(group["title"], str) and bool(group["title"].strip()),
"Comparison group needs a title",
)
views = group["views"]
require(
isinstance(views, list) and len(views) >= 2,
"Comparison group needs at least two views",
)
choices: set[tuple[str, str]] = set()
sections: set[str] = set()
for view in views:
require(
isinstance(view, dict)
and set(view) == {"table_id", "period", "scenario"},
"Unexpected comparison view fields",
)
require(
all(isinstance(view[k], str) and view[k].strip() for k in view),
"Comparison view labels must be nonempty text",
)
tid = view["table_id"]
require(
tid in tables and "comparison" in tables[tid],
"Comparison view needs a monetary comparison table",
)
require(tid not in assigned, "Comparison table is assigned more than once")
assigned.add(tid)
choice = (view["period"], view["scenario"])
require(choice not in choices, "Duplicate period and scenario choice")
choices.add(choice)
sections.add(tables[tid]["section"])
require(len(sections) == 1, "Comparison group must stay in one report section")
def render_group(
group: dict[str, Any],
tables: dict[str, Any],
lang: str,
render_table: Callable[[dict[str, Any], list[dict[str, Any]]], str],
rows_for: Callable[[dict[str, Any]], list[dict[str, Any]]],
) -> str:
"""Retain every checked view in HTML; JavaScript only selects visibility."""
from planning_presentation import label
e = html.escape
scales = [
row for view in group["views"] for row in rows_for(tables[view["table_id"]])
]
controls = []
for key, title in (("period", "Period"), ("scenario", "Comparison")):
values = dict.fromkeys(view[key] for view in group["views"])
options = "".join(f'<option value="{e(v)}">{e(v)}</option>' for v in values)
controls.append(
f"<label>{e(label(title, lang))}<select data-report-{key} disabled>{options}</select></label>"
)
panels = []
for view in group["views"]:
panels.append(
f'<div data-comparison-panel data-period="{e(view["period"])}" data-scenario="{e(view["scenario"])}">{render_table(tables[view["table_id"]], scales)}</div>'
)
return (
f'<div class="comparison-explorer" id="comparison-{e(group["id"])}" data-comparison-group>'
f'<h3>{e(group["title"])}</h3><div class="report-controls">{"".join(controls)}</div>'
f'<p class="comparison-selection" aria-live="polite"></p>{"".join(panels)}</div>'
)
def interaction_assets() -> tuple[str, str]:
"""Inline the packaged first-party assets for an offline-capable report."""
assets = Path(__file__).resolve().parents[1] / "assets"
return (
(assets / "report-interaction.css").read_text(encoding="utf-8"),
(assets / "report-interaction.js").read_text(encoding="utf-8"),
)
SHA-256: 3acf8c8bc7e22042291f8050205bf2c3fcf17c5a66c9d5e3621d61cd236d63bf