Small fixes

This commit is contained in:
Beto Dealmeida
2026-04-16 18:55:48 -04:00
parent 5d6ce82651
commit 0d09ecaae1
7 changed files with 5097 additions and 5405 deletions

View File

@@ -105,7 +105,13 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
"SEMANTIC_LAYERS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,9 @@ class GetCombinedDatasourceListCommand(BaseCommand):
source_type, name_filter, sql_filter, type_filter = self._parse_filters(filters)
source_type = self._resolve_source_type(source_type, sql_filter, type_filter)
if source_type == "empty":
return {"count": 0, "result": []}
ds_q = DatasourceDAO.build_dataset_query(name_filter, sql_filter)
sv_q = DatasourceDAO.build_semantic_view_query(name_filter)
@@ -108,8 +111,18 @@ class GetCombinedDatasourceListCommand(BaseCommand):
sql_filter: bool | None,
type_filter: str | None,
) -> str:
"""Narrow source_type based on access flags, sql filter, and type filter."""
"""Narrow source_type based on access flags, sql filter, and type filter.
Returns one of: "database", "semantic_layer", "all", or "empty".
"empty" signals that the caller should short-circuit and return no results
(used when the user explicitly requests semantic views but lacks access).
"""
if not self._can_read_semantic_views:
# If the user explicitly asked for semantic views but cannot read them,
# return "empty" so the caller yields zero results rather than silently
# falling back to the full dataset list.
if source_type == "semantic_layer" or type_filter == "semantic_view":
return "empty"
return "database"
if not self._can_read_datasets:
return "semantic_layer"

View File

@@ -22,18 +22,20 @@ from __future__ import annotations
from typing import Any
from sqlalchemy.exc import StatementError
from superset_core.semantic_layers.daos import (
AbstractSemanticLayerDAO,
AbstractSemanticViewDAO,
)
from superset.daos.base import BaseDAO
from superset.extensions import db
from superset.semantic_layers.models import SemanticLayer, SemanticView
from superset.utils import json
class SemanticLayerDAO(AbstractSemanticLayerDAO):
class SemanticLayerDAO(BaseDAO[SemanticLayer], AbstractSemanticLayerDAO):
# SemanticLayer uses uuid as the primary key
id_column_name = "uuid"
"""
Data Access Object for SemanticLayer model.
"""
@@ -112,7 +114,7 @@ class SemanticLayerDAO(AbstractSemanticLayerDAO):
)
class SemanticViewDAO(AbstractSemanticViewDAO):
class SemanticViewDAO(BaseDAO[SemanticView], AbstractSemanticViewDAO):
"""Data Access Object for SemanticView model."""
model_cls = SemanticView

View File

@@ -56,7 +56,7 @@ from superset.commands.semantic_layer.update import (
UpdateSemanticViewCommand,
)
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.daos.semantic_layer import SemanticLayerDAO, SemanticViewDAO
from superset.daos.semantic_layer import SemanticLayerDAO
from superset.datasets.schemas import get_delete_ids_schema
from superset.models.core import Database
from superset.semantic_layers.models import SemanticLayer, SemanticView
@@ -175,6 +175,11 @@ class SemanticViewRestApi(BaseSupersetModelRestApi):
class_permission_name = "SemanticView"
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
include_route_methods = {"put", "post", "delete", "bulk_delete"}
# SemanticViewRestApi exposes only write endpoints, but can_read must be
# declared explicitly so that FAB registers the permission. It is used by
# DatasourceRestApi.combined_list to gate access to semantic views in the
# combined datasource list.
base_permissions = ["can_read", "can_write"]
edit_model_schema = SemanticViewPutSchema()
@@ -635,7 +640,7 @@ class SemanticLayerRestApi(BaseSupersetApi):
)
# Check which views already exist with the same runtime config
existing = SemanticViewDAO.find_by_semantic_layer(str(layer.uuid))
existing = SemanticLayerDAO.get_semantic_views(str(layer.uuid))
existing_keys: set[tuple[str, str]] = set()
for v in existing:
config = v.configuration

View File

@@ -291,11 +291,7 @@ def map_query_object(query_object: ValidatedQueryObject) -> list[SemanticQuery]:
metrics = [all_metrics[metric] for metric in (query_object.metrics or [])]
grain = (
_convert_time_grain(query_object.extras["time_grain_sqla"])
if "time_grain_sqla" in query_object.extras
else None
)
grain = _convert_time_grain(query_object.extras.get("time_grain_sqla"))
dimensions = [
dimension
for dimension in semantic_view.dimensions
@@ -740,13 +736,17 @@ def _get_group_limit_filters(
return filters if filters else None
def _convert_time_grain(time_grain: str) -> Grain | None:
def _convert_time_grain(time_grain: str | None) -> Grain | None:
"""
Convert a time grain string (ISO 8601 duration) to a Grain instance.
Returns None when ``time_grain`` is None or empty (no grain selected).
"""
if not time_grain:
return None
try:
return Grains.get(time_grain)
except (ValueError, isodate.ISO8601Error):
except (TypeError, ValueError, isodate.ISO8601Error):
return None

View File

@@ -1796,8 +1796,7 @@ def test_get_views(
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = mock_layer
mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO")
mock_sv_dao.find_by_semantic_layer.return_value = []
mock_dao.get_semantic_views.return_value = []
response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/views",
@@ -1834,8 +1833,7 @@ def test_get_views_with_existing(
existing_view.name = "Existing View"
existing_view.configuration = '{"database": "mydb"}'
mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO")
mock_sv_dao.find_by_semantic_layer.return_value = [existing_view]
mock_dao.get_semantic_views.return_value = [existing_view]
response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/views",
@@ -1915,8 +1913,7 @@ def test_get_views_existing_dict_config(
existing_view.name = "View X"
existing_view.configuration = {"key": "val"} # dict, not string
mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO")
mock_sv_dao.find_by_semantic_layer.return_value = [existing_view]
mock_dao.get_semantic_views.return_value = [existing_view]
response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/views",