Compare commits

..
7 changed files with 36 additions and 50 deletions
+2 -9
View File
@@ -18,8 +18,6 @@ import logging
from functools import partial
from typing import Any
from jinja2.exceptions import TemplateError
from superset import security_manager
from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.tag.exceptions import TagCreateFailedError, TagInvalidError
@@ -99,14 +97,9 @@ class CreateCustomTagCommand(CreateMixin, BaseCommand):
f"Access validation not supported for {object_type}"
)
)
except (SupersetSecurityException, TemplateError):
# A TemplateError can surface when authorizing a saved query whose
# Jinja-templated SQL must be parsed to resolve table references; a
# malformed template is a validation failure, not an unhandled 500.
except SupersetSecurityException:
exceptions.append(
TagCreateFailedError(
f"Could not validate access for {object_type} {object_id}"
)
TagCreateFailedError(f"Access denied for {object_type} {object_id}")
)
+3 -1
View File
@@ -213,7 +213,9 @@ def orderby_from_form_data(
# The drag-and-drop "sort by" control persists a list; the frontend unwraps it
# with ``ensureIsArray(...)[0]`` (``plugin-chart-table/src/buildQuery.ts:67``).
# Read raw, a list would nest inside ``orderby`` and fail the query.
raw_sort_metric = form_data.get("timeseries_limit_metric")
raw_sort_metric = form_data.get("series_limit_metric") or form_data.get(
"timeseries_limit_metric"
)
sort_metric = (
next(iter(as_list(raw_sort_metric)), None) if raw_sort_metric else None
) or (metrics[0] if form_data.get("sort_by_metric") else None)
+4 -4
View File
@@ -290,7 +290,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
groupby=["name"],
adhoc_filters=[gen_filter("gender", "girl")],
row_limit=50,
timeseries_limit_metric=metric,
series_limit_metric=metric,
metrics=[metric],
),
editors=[],
@@ -321,7 +321,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
groupby=["name"],
adhoc_filters=[gen_filter("gender", "boy")],
row_limit=50,
timeseries_limit_metric=metric,
series_limit_metric=metric,
metrics=[metric],
),
editors=[],
@@ -498,7 +498,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
viz_type="echarts_timeseries_line",
granularity_sqla="ds",
groupby=["name"],
timeseries_limit_metric={
series_limit_metric={
"expressionType": "SIMPLE",
"column": {
"column_name": "num_california",
@@ -522,7 +522,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
metrics=metrics,
groupby=["name"],
row_limit=50,
timeseries_limit_metric={
series_limit_metric={
"expressionType": "SIMPLE",
"column": {
"column_name": "num_california",
@@ -36,8 +36,8 @@ params:
metrics:
- sum__num
row_limit: 50
series_limit_metric: sum__num
time_range: '100 years ago : now'
timeseries_limit_metric: sum__num
viz_type: table
query_context: null
slice_name: Boys
@@ -36,8 +36,8 @@ params:
metrics:
- sum__num
row_limit: 50
series_limit_metric: sum__num
time_range: '100 years ago : now'
timeseries_limit_metric: sum__num
viz_type: table
query_context: null
slice_name: Girls
@@ -218,6 +218,31 @@ def test_orderby_uses_timeseries_limit_metric_and_order_desc() -> None:
assert query["orderby"] == [["revenue", True]]
def test_orderby_uses_series_limit_metric_and_order_desc() -> None:
# series_limit_metric is the current field name; timeseries_limit_metric is
# the deprecated alias kept above for back-compat with old saved charts.
form_data = {
"metrics": ["count"],
"groupby": ["c"],
"series_limit_metric": "revenue",
"order_desc": False,
}
query = build_query_context_from_form_data(form_data, DATASOURCE)["queries"][0]
assert query["orderby"] == [["revenue", True]]
def test_orderby_prefers_series_limit_metric_over_deprecated_alias() -> None:
form_data = {
"metrics": ["count"],
"groupby": ["c"],
"series_limit_metric": "revenue",
"timeseries_limit_metric": "profit",
"order_desc": False,
}
query = build_query_context_from_form_data(form_data, DATASOURCE)["queries"][0]
assert query["orderby"] == [["revenue", True]]
def test_orderby_pie_sort_by_metric() -> None:
form_data = {"metric": "count", "groupby": ["c"], "sort_by_metric": True}
query = build_query_context_from_form_data(form_data, DATASOURCE, viz_type="pie")[
@@ -108,40 +108,6 @@ def test_create_command_success(session_with_data: Session, mocker: MockerFixtur
)
def test_validate_object_access_query_malformed_jinja(
session_with_data: Session, mocker: MockerFixture
):
"""A saved query whose Jinja-templated SQL fails to parse during access
checks must surface as a validation error, not an unhandled
``jinja2.TemplateError`` escaping as a 500.
When ``raise_for_access(query=...)`` authorizes a saved query via
per-table permissions it parses the query's Jinja SQL (e.g. an unclosed
``{% if %}`` block raises ``TemplateSyntaxError``). Mock that call to raise
the ``TemplateError`` directly so the test stays hermetic and does not open
a live DB connection to introspect table-level perms.
"""
from jinja2.exceptions import TemplateError
from superset.commands.tag.create import CreateCustomTagCommand
from superset.commands.tag.exceptions import TagInvalidError
from superset.models.sql_lab import SavedQuery
from superset.tags.models import ObjectType
query = db.session.query(SavedQuery).first()
mocker.patch("superset.commands.tag.create.to_object_model", return_value=query)
mocker.patch(
"superset.commands.tag.create.security_manager.raise_for_access",
side_effect=TemplateError("unclosed {% if %}"),
)
command = CreateCustomTagCommand(ObjectType.query, query.id, ["tag"])
with pytest.raises(TagInvalidError):
command.validate()
def test_create_command_success_clear(
session_with_data: Session, mocker: MockerFixture
):