Release govoplan-reporting v0.1.21: preserve calculated measure bindings
This commit is contained in:
+2
-2
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-reporting"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
description = "GovOPlaN governed reporting and semantic BI module."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = [
|
||||
"govoplan-core>=0.1.37",
|
||||
"govoplan-core>=0.1.45",
|
||||
"govoplan-access>=0.1.18",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""GovOPlaN Reporting module."""
|
||||
|
||||
__version__ = "0.1.20"
|
||||
__version__ = "0.1.21"
|
||||
|
||||
@@ -83,7 +83,7 @@ from govoplan_reporting.backend.search_source import create_reporting_search_sou
|
||||
|
||||
MODULE_ID = "reporting"
|
||||
MODULE_NAME = "Reporting"
|
||||
MODULE_VERSION = "0.1.20"
|
||||
MODULE_VERSION = "0.1.21"
|
||||
|
||||
|
||||
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
||||
@@ -603,8 +603,10 @@ manifest = ModuleManifest(
|
||||
"aggregations, typed expressions, filters, pivots, saved views, chart models, "
|
||||
"schedules, exports, and publication providers replace unchecked SQL in the "
|
||||
"presentation layer. PostgreSQL executes bounded semantic plans when available. "
|
||||
"Calculated measure keys may contain the documented dots and hyphens, including in nested references; generated bind names remain internal and values remain parameters, not SQL fragments. "
|
||||
"Signed drill contexts reauthorize contributor rows, and Files/Mail publication "
|
||||
"adapters retain idempotent evidence. A dataset may pin one successful published Dataflow run, which is read from its exact Datasource materialization after both source boundaries reauthorize the current principal. Dataflow and module read models remain source owners."
|
||||
"adapters retain idempotent evidence. A dataset may pin one successful published Dataflow run, which is read from its exact Datasource materialization after both source boundaries reauthorize the current principal. Dataflow and module read models remain source owners. "
|
||||
"The contributor drill-down action stays in a shared action column at the right edge of horizontally scrolled results; opening it still reauthorizes every contributor."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("admin", "user"),
|
||||
@@ -663,12 +665,13 @@ manifest = ModuleManifest(
|
||||
"Filter, Pivotierungen, gespeicherte Ansichten, Diagrammmodelle, Zeitpläne, "
|
||||
"Exporte und Veröffentlichungsanbieter ersetzen ungeprüftes SQL in der "
|
||||
"Darstellungsschicht. PostgreSQL führt begrenzte semantische Pläne aus, sofern "
|
||||
"verfügbar. Signierte Drilldown-Kontexte autorisieren beitragende Zeilen erneut; "
|
||||
"verfügbar. Kennungen berechneter Kennzahlen dürfen auch in verschachtelten Verweisen die vorgesehenen Punkte und Bindestriche enthalten; "
|
||||
"erzeugte Bindungsnamen bleiben intern, und Werte bleiben Parameter statt SQL-Fragmente. Signierte Drilldown-Kontexte autorisieren beitragende Zeilen erneut; "
|
||||
"Adapter für Dateien und Mail bewahren idempotente Nachweise. Ein Datensatz kann "
|
||||
"genau eine erfolgreiche veröffentlichte Dataflow-Ausführung fixieren, die nach "
|
||||
"erneuter Autorisierung beider Quellgrenzen aus ihrer exakten Datasource-"
|
||||
"Materialisierung gelesen wird. Dataflow und die Lesemodelle der Module bleiben "
|
||||
"führende Quellen."
|
||||
"führende Quellen. Die Aktion zum Aufschlüsseln beitragender Zeilen bleibt in einer gemeinsamen Aktionsspalte am rechten Rand horizontal gescrollter Ergebnisse; beim Öffnen wird jeder Beitrag erneut autorisiert."
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -344,7 +344,9 @@ def _calculated_sql(
|
||||
return _calculated_sql(
|
||||
target.expression,
|
||||
parameters,
|
||||
prefix + "_" + reference,
|
||||
# The expression position already makes this prefix unique. Model
|
||||
# keys may contain dots or hyphens, which are not SQL bind names.
|
||||
prefix + "_ref",
|
||||
measures=measures,
|
||||
stack=(*stack, reference),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from govoplan_reporting.backend.postgres_planner import compile_postgres_query
|
||||
from govoplan_reporting.backend.schemas import (
|
||||
DatasetDefinition,
|
||||
ReportQuery,
|
||||
SemanticModelDefinition,
|
||||
)
|
||||
|
||||
|
||||
class PostgresBindNameTests(unittest.TestCase):
|
||||
def test_allowed_measure_key_punctuation_never_becomes_bind_parameter_syntax(
|
||||
self,
|
||||
) -> None:
|
||||
dataset = DatasetDefinition(
|
||||
source_kind="static",
|
||||
source_ref="fixture",
|
||||
static_rows=[{"value": 10}],
|
||||
purpose="Bound parameter fixture",
|
||||
)
|
||||
semantic = SemanticModelDefinition.model_validate(
|
||||
{
|
||||
"dataset_id": "fixture",
|
||||
"dataset_revision": 1,
|
||||
"measures": [
|
||||
{
|
||||
"key": "base",
|
||||
"label": "Base",
|
||||
"aggregation": "sum",
|
||||
"field": "value",
|
||||
},
|
||||
{
|
||||
"key": "extra-cost",
|
||||
"label": "Extra",
|
||||
"aggregation": "calculated",
|
||||
"expression": {
|
||||
"op": "add",
|
||||
"args": [
|
||||
{"op": "measure", "ref": "base"},
|
||||
{"op": "literal", "value": 5},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "tax.factor",
|
||||
"label": "Tax",
|
||||
"aggregation": "calculated",
|
||||
"expression": {
|
||||
"op": "multiply",
|
||||
"args": [
|
||||
{"op": "measure", "ref": "base"},
|
||||
{"op": "literal", "value": 9},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "grand-total",
|
||||
"label": "Total",
|
||||
"aggregation": "calculated",
|
||||
"expression": {
|
||||
"op": "add",
|
||||
"args": [
|
||||
{"op": "measure", "ref": "extra-cost"},
|
||||
{"op": "measure", "ref": "tax.factor"},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
plan = compile_postgres_query(
|
||||
dataset,
|
||||
semantic,
|
||||
ReportQuery(measures=["extra-cost", "tax.factor", "grand-total"]),
|
||||
)
|
||||
binds = text(plan.sql).compile(dialect=postgresql.dialect()).params
|
||||
self.assertEqual(
|
||||
set(plan.parameters) | {"rows_json", "result_limit", "result_offset"},
|
||||
set(binds),
|
||||
)
|
||||
self.assertTrue(
|
||||
all("-" not in name and "." not in name for name in plan.parameters)
|
||||
)
|
||||
self.assertIn('AS "extra-cost"', plan.sql)
|
||||
self.assertIn('AS "tax.factor"', plan.sql)
|
||||
self.assertEqual(2, list(plan.parameters.values()).count(5))
|
||||
self.assertEqual(2, list(plan.parameters.values()).count(9))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/reporting-webui",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -17,7 +17,7 @@
|
||||
"test:interface-pattern": "node scripts/test-interface-pattern.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@govoplan/core-webui": "^0.1.45",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
|
||||
@@ -10,6 +10,8 @@ assert.ok(page.includes("PageScrollViewport"), "Reporting owns bounded catalogue
|
||||
assert.ok(page.includes("DataGrid"), "Tabular report results use the shared grid");
|
||||
assert.ok(page.includes("<Dialog"), "Save and schedule operations use shared dialogs");
|
||||
assert.ok(page.includes("createDrillContext"), "Aggregate detail uses an actor-bound drill context");
|
||||
assert.match(page, /id: "drill",\s*header: "Detail",\s*columnType: "actions",\s*sticky: "end"/, "Result drill-down controls use the shared pinned action-column contract");
|
||||
assert.match(page, /<TableActionGroup actions=\{\[\{\s*id: "drill"/, "Drill-down renders the shared measurable action surface");
|
||||
assert.ok(page.includes("AccessExplanation"), "Policy-hidden fields, rows, and actions are explained");
|
||||
assert.ok(page.includes("PublishDialog"), "Publication targets use the shared dialog surface");
|
||||
assert.ok(provider.includes("disabledReason={runDisabledReason}"), "Governed report blockers remain keyboard-explainable");
|
||||
|
||||
@@ -34,6 +34,7 @@ import { FormGrid,
|
||||
SelectionListItem,
|
||||
SelectionListItemContent,
|
||||
StatusBadge,
|
||||
TableActionGroup,
|
||||
ToggleSwitch,
|
||||
hasScope,
|
||||
WorkspaceActionBar,
|
||||
@@ -543,7 +544,7 @@ function ReportTable({ execution, onDrill }: { execution: ReportExecution; onDri
|
||||
const [page, setPage] = useState(0);
|
||||
useEffect(() => setPage(0), [execution.execution_id]);
|
||||
const columns = useMemo<DataGridColumn<Record<string, unknown>>[]>(() => {
|
||||
const result = execution.schema.map((field) => ({
|
||||
const result: DataGridColumn<Record<string, unknown>>[] = execution.schema.map((field) => ({
|
||||
id: field.name,
|
||||
header: humanize(field.name),
|
||||
width: "1fr",
|
||||
@@ -559,16 +560,20 @@ function ReportTable({ execution, onDrill }: { execution: ReportExecution; onDri
|
||||
result.push({
|
||||
id: "drill",
|
||||
header: "Detail",
|
||||
columnType: "actions",
|
||||
sticky: "end",
|
||||
resizable: false,
|
||||
align: "right",
|
||||
width: 74,
|
||||
minWidth: 74,
|
||||
maxWidth: 74,
|
||||
render: (row) => (
|
||||
<IconButton
|
||||
label="Show authorized contributing rows"
|
||||
icon={<ChevronRight size={16} />}
|
||||
variant="ghost"
|
||||
onClick={() => onDrill(row)}
|
||||
/>
|
||||
<TableActionGroup actions={[{
|
||||
id: "drill",
|
||||
label: "Show authorized contributing rows",
|
||||
icon: <ChevronRight size={16} aria-hidden="true" />,
|
||||
onClick: () => onDrill(row)
|
||||
}]} />
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user