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]
|
[project]
|
||||||
name = "govoplan-reporting"
|
name = "govoplan-reporting"
|
||||||
version = "0.1.20"
|
version = "0.1.21"
|
||||||
description = "GovOPlaN governed reporting and semantic BI module."
|
description = "GovOPlaN governed reporting and semantic BI module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
license = { text = "AGPL-3.0-or-later" }
|
license = { text = "AGPL-3.0-or-later" }
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.37",
|
"govoplan-core>=0.1.45",
|
||||||
"govoplan-access>=0.1.18",
|
"govoplan-access>=0.1.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""GovOPlaN Reporting module."""
|
"""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_ID = "reporting"
|
||||||
MODULE_NAME = "Reporting"
|
MODULE_NAME = "Reporting"
|
||||||
MODULE_VERSION = "0.1.20"
|
MODULE_VERSION = "0.1.21"
|
||||||
|
|
||||||
|
|
||||||
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
||||||
@@ -603,8 +603,10 @@ manifest = ModuleManifest(
|
|||||||
"aggregations, typed expressions, filters, pivots, saved views, chart models, "
|
"aggregations, typed expressions, filters, pivots, saved views, chart models, "
|
||||||
"schedules, exports, and publication providers replace unchecked SQL in the "
|
"schedules, exports, and publication providers replace unchecked SQL in the "
|
||||||
"presentation layer. PostgreSQL executes bounded semantic plans when available. "
|
"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 "
|
"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",
|
layer="available",
|
||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
@@ -663,12 +665,13 @@ manifest = ModuleManifest(
|
|||||||
"Filter, Pivotierungen, gespeicherte Ansichten, Diagrammmodelle, Zeitpläne, "
|
"Filter, Pivotierungen, gespeicherte Ansichten, Diagrammmodelle, Zeitpläne, "
|
||||||
"Exporte und Veröffentlichungsanbieter ersetzen ungeprüftes SQL in der "
|
"Exporte und Veröffentlichungsanbieter ersetzen ungeprüftes SQL in der "
|
||||||
"Darstellungsschicht. PostgreSQL führt begrenzte semantische Pläne aus, sofern "
|
"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 "
|
"Adapter für Dateien und Mail bewahren idempotente Nachweise. Ein Datensatz kann "
|
||||||
"genau eine erfolgreiche veröffentlichte Dataflow-Ausführung fixieren, die nach "
|
"genau eine erfolgreiche veröffentlichte Dataflow-Ausführung fixieren, die nach "
|
||||||
"erneuter Autorisierung beider Quellgrenzen aus ihrer exakten Datasource-"
|
"erneuter Autorisierung beider Quellgrenzen aus ihrer exakten Datasource-"
|
||||||
"Materialisierung gelesen wird. Dataflow und die Lesemodelle der Module bleiben "
|
"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(
|
return _calculated_sql(
|
||||||
target.expression,
|
target.expression,
|
||||||
parameters,
|
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,
|
measures=measures,
|
||||||
stack=(*stack, reference),
|
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",
|
"name": "@govoplan/reporting-webui",
|
||||||
"version": "0.1.20",
|
"version": "0.1.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"test:interface-pattern": "node scripts/test-interface-pattern.mjs"
|
"test:interface-pattern": "node scripts/test-interface-pattern.mjs"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.45",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
"react-dom": ">=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("DataGrid"), "Tabular report results use the shared grid");
|
||||||
assert.ok(page.includes("<Dialog"), "Save and schedule operations use shared dialogs");
|
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.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("AccessExplanation"), "Policy-hidden fields, rows, and actions are explained");
|
||||||
assert.ok(page.includes("PublishDialog"), "Publication targets use the shared dialog surface");
|
assert.ok(page.includes("PublishDialog"), "Publication targets use the shared dialog surface");
|
||||||
assert.ok(provider.includes("disabledReason={runDisabledReason}"), "Governed report blockers remain keyboard-explainable");
|
assert.ok(provider.includes("disabledReason={runDisabledReason}"), "Governed report blockers remain keyboard-explainable");
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { FormGrid,
|
|||||||
SelectionListItem,
|
SelectionListItem,
|
||||||
SelectionListItemContent,
|
SelectionListItemContent,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
|
TableActionGroup,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
hasScope,
|
hasScope,
|
||||||
WorkspaceActionBar,
|
WorkspaceActionBar,
|
||||||
@@ -543,7 +544,7 @@ function ReportTable({ execution, onDrill }: { execution: ReportExecution; onDri
|
|||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
useEffect(() => setPage(0), [execution.execution_id]);
|
useEffect(() => setPage(0), [execution.execution_id]);
|
||||||
const columns = useMemo<DataGridColumn<Record<string, unknown>>[]>(() => {
|
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,
|
id: field.name,
|
||||||
header: humanize(field.name),
|
header: humanize(field.name),
|
||||||
width: "1fr",
|
width: "1fr",
|
||||||
@@ -559,16 +560,20 @@ function ReportTable({ execution, onDrill }: { execution: ReportExecution; onDri
|
|||||||
result.push({
|
result.push({
|
||||||
id: "drill",
|
id: "drill",
|
||||||
header: "Detail",
|
header: "Detail",
|
||||||
|
columnType: "actions",
|
||||||
|
sticky: "end",
|
||||||
|
resizable: false,
|
||||||
|
align: "right",
|
||||||
width: 74,
|
width: 74,
|
||||||
minWidth: 74,
|
minWidth: 74,
|
||||||
maxWidth: 74,
|
maxWidth: 74,
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<IconButton
|
<TableActionGroup actions={[{
|
||||||
label="Show authorized contributing rows"
|
id: "drill",
|
||||||
icon={<ChevronRight size={16} />}
|
label: "Show authorized contributing rows",
|
||||||
variant="ghost"
|
icon: <ChevronRight size={16} aria-hidden="true" />,
|
||||||
onClick={() => onDrill(row)}
|
onClick: () => onDrill(row)
|
||||||
/>
|
}]} />
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user