diff --git a/superset/charts/api.py b/superset/charts/api.py index 5abead3f6af..a33c0bc943d 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -45,6 +45,7 @@ from superset.charts.filters import ( from superset.charts.schemas import ( CHART_SCHEMAS, ChartCacheWarmUpRequestSchema, + ChartGetResponseSchema, ChartPostSchema, ChartPutSchema, get_delete_ids_schema, @@ -131,34 +132,7 @@ class ChartRestApi(BaseSupersetModelRestApi): } class_permission_name = "Chart" method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP - show_columns = [ - "cache_timeout", - "certified_by", - "certification_details", - "changed_on_delta_humanized", - "dashboards.dashboard_title", - "dashboards.id", - "dashboards.json_metadata", - "description", - "id", - "owners.first_name", - "owners.id", - "owners.last_name", - "dashboards.id", - "dashboards.dashboard_title", - "params", - "slice_name", - "thumbnail_url", - "url", - "viz_type", - "query_context", - "is_managed_externally", - "tags.id", - "tags.name", - "tags.type", - ] - show_select_columns = show_columns + ["table.id"] list_columns = [ "is_managed_externally", "certified_by", @@ -230,6 +204,7 @@ class ChartRestApi(BaseSupersetModelRestApi): "datasource_type", "description", "id", + "uuid", "owners", "dashboards", "slice_name", @@ -255,6 +230,7 @@ class ChartRestApi(BaseSupersetModelRestApi): add_model_schema = ChartPostSchema() edit_model_schema = ChartPutSchema() + chart_get_response_schema = ChartGetResponseSchema() openapi_spec_tag = "Charts" """ Override the name set for this collection of endpoints """ @@ -287,6 +263,53 @@ class ChartRestApi(BaseSupersetModelRestApi): allowed_rel_fields = {"owners", "created_by", "changed_by"} + @expose("/", methods=["GET"]) + @protect() + @safe + @statsd_metrics + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get", + log_to_statsd=False, + ) + def get(self, id_or_uuid: str) -> Response: + """Gets a chart + --- + get: + description: >- + Get a chart + parameters: + - in: path + schema: + type: string + name: id_or_uuid + description: Either the id of the chart, or its uuid + responses: + 200: + description: Chart + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/ChartGetResponseSchema' + 302: + description: Redirects to the current digest + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + """ + # pylint: disable=arguments-differ + try: + dash = ChartDAO.get_by_id_or_uuid(id_or_uuid) + result = self.chart_get_response_schema.dump(dash) + return self.response(200, result=result) + except ChartNotFoundError: + return self.response_404() + @expose("/", methods=("POST",)) @protect() @safe diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py index f1f7a7a19e7..3a05c3ff176 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -27,6 +27,7 @@ from marshmallow.validate import Length, Range from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.db_engine_specs.base import builtin_time_grains +from superset.tags.models import TagType from superset.utils import pandas_postprocessing, schema as utils from superset.utils.core import ( AnnotationType, @@ -241,6 +242,7 @@ class ChartPostSchema(Schema): ) is_managed_externally = fields.Boolean(allow_none=True, dump_default=False) external_url = fields.String(allow_none=True) + uuid = fields.UUID(allow_none=True) class ChartPutSchema(Schema): @@ -297,6 +299,7 @@ class ChartPutSchema(Schema): is_managed_externally = fields.Boolean(allow_none=True, dump_default=False) external_url = fields.String(allow_none=True) tags = fields.List(fields.Integer(metadata={"description": tags_description})) + uuid = fields.UUID(allow_none=True) class ChartGetDatasourceObjectDataResponseSchema(Schema): @@ -1617,6 +1620,49 @@ class ChartCacheWarmUpResponseSchema(Schema): ) +class TagSchema(Schema): + id = fields.Int() + name = fields.String() + type = fields.Enum(TagType, by_value=True) + + +class UserSchema(Schema): + id = fields.Int() + first_name = fields.String() + last_name = fields.String() + + +class DashboardSchema(Schema): + id = fields.Int() + dashboard_title = fields.String() + json_metadata = fields.String() + + +class ChartGetResponseSchema(Schema): + id = fields.Int(description=id_description) + url = fields.String() + cache_timeout = fields.String() + certified_by = fields.String() + certification_details = fields.String() + changed_on_humanized = fields.String(data_key="changed_on_delta_humanized") + description = fields.String() + params = fields.String() + slice_name = fields.String() + thumbnail_url = fields.String() + viz_type = fields.String() + query_context = fields.String() + is_managed_externally = fields.Boolean() + tags = fields.Nested(TagSchema, many=True) + owners = fields.List(fields.Nested(UserSchema)) + dashboards = fields.List(fields.Nested(DashboardSchema)) + uuid = fields.UUID() + datasource_id = fields.Int() + datasource_name_text = fields.Function(lambda obj: obj.datasource_name_text()) + datasource_type = fields.String() + datasource_url = fields.Function(lambda obj: obj.datasource_url()) + datasource_uuid = fields.UUID(attribute="table.uuid") + + CHART_SCHEMAS = ( ChartCacheWarmUpRequestSchema, ChartCacheWarmUpResponseSchema, @@ -1640,6 +1686,7 @@ CHART_SCHEMAS = ( ChartDataGeodeticParseOptionsSchema, ChartEntityResponseSchema, ChartGetDatasourceResponseSchema, + ChartGetResponseSchema, ChartCacheScreenshotResponseSchema, GetFavStarIdsSchema, ) diff --git a/superset/daos/chart.py b/superset/daos/chart.py index 35afb7f7a91..adca95b8a62 100644 --- a/superset/daos/chart.py +++ b/superset/daos/chart.py @@ -20,11 +20,14 @@ import logging from datetime import datetime from typing import TYPE_CHECKING +from flask_appbuilder.models.sqla.interface import SQLAInterface + from superset.charts.filters import ChartFilter +from superset.commands.chart.exceptions import ChartNotFoundError from superset.daos.base import BaseDAO from superset.extensions import db from superset.models.core import FavStar, FavStarClassName -from superset.models.slice import Slice +from superset.models.slice import id_or_uuid_filter, Slice from superset.utils.core import get_user_id if TYPE_CHECKING: @@ -36,6 +39,16 @@ logger = logging.getLogger(__name__) class ChartDAO(BaseDAO[Slice]): base_filter = ChartFilter + @staticmethod + def get_by_id_or_uuid(id_or_uuid: str) -> Slice: + query = db.session.query(Slice).filter(id_or_uuid_filter(id_or_uuid)) + # Apply chart base filters + query = ChartFilter("id", SQLAInterface(Slice, db.session)).apply(query, None) + chart = query.one_or_none() + if not chart: + raise ChartNotFoundError() + return chart + @staticmethod def favorited_ids(charts: list[Slice]) -> list[FavStar]: ids = [chart.id for chart in charts] diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 6d8fd0da555..54e05df8b75 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -191,6 +191,7 @@ class DashboardRestApi(BaseSupersetModelRestApi): list_columns = [ "id", + "uuid", "published", "status", "slug", @@ -251,6 +252,7 @@ class DashboardRestApi(BaseSupersetModelRestApi): "changed_by", "dashboard_title", "id", + "uuid", "owners", "published", "roles", diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py index bdfcb94fc7a..df6c1dd04ba 100644 --- a/superset/dashboards/schemas.py +++ b/superset/dashboards/schemas.py @@ -238,6 +238,7 @@ class DashboardGetResponseSchema(Schema): changed_on_humanized = fields.String(data_key="changed_on_delta_humanized") created_on_humanized = fields.String(data_key="created_on_delta_humanized") is_managed_externally = fields.Boolean(allow_none=True, dump_default=False) + uuid = fields.UUID(allow_none=True) # pylint: disable=unused-argument @post_dump() @@ -365,6 +366,7 @@ class DashboardPostSchema(BaseDashboardSchema): ) is_managed_externally = fields.Boolean(allow_none=True, dump_default=False) external_url = fields.String(allow_none=True) + uuid = fields.UUID(allow_none=True) class DashboardCopySchema(Schema): @@ -431,6 +433,7 @@ class DashboardPutSchema(BaseDashboardSchema): tags = fields.List( fields.Integer(metadata={"description": tags_description}, allow_none=True) ) + uuid = fields.UUID(allow_none=True) class DashboardNativeFiltersConfigUpdateSchema(BaseDashboardSchema): diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 0af6ecca4bd..2afd0411c28 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -117,8 +117,10 @@ class DatasetRestApi(BaseSupersetModelRestApi): } list_columns = [ "id", + "uuid", "database.id", "database.database_name", + "database.uuid", "changed_by_name", "changed_by.first_name", "changed_by.last_name", @@ -153,6 +155,7 @@ class DatasetRestApi(BaseSupersetModelRestApi): "id", "database.database_name", "database.id", + "database.uuid", "table_name", "sql", "filter_select_enabled", @@ -222,6 +225,7 @@ class DatasetRestApi(BaseSupersetModelRestApi): "columns.advanced_data_type", "is_managed_externally", "uid", + "uuid", "datasource_name", "name", "column_formats", @@ -273,6 +277,7 @@ class DatasetRestApi(BaseSupersetModelRestApi): } search_columns = [ "id", + "uuid", "database", "owners", "catalog", diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py index 3b781f4b1c1..a0d5747dd73 100644 --- a/superset/datasets/schemas.py +++ b/superset/datasets/schemas.py @@ -150,6 +150,7 @@ class DatasetPostSchema(Schema): normalize_columns = fields.Boolean(load_default=False) always_filter_main_dttm = fields.Boolean(load_default=False) template_params = fields.String(allow_none=True) + uuid = fields.UUID(allow_none=True) class DatasetPutSchema(Schema): @@ -176,6 +177,7 @@ class DatasetPutSchema(Schema): extra = fields.String(allow_none=True) is_managed_externally = fields.Boolean(allow_none=True, dump_default=False) external_url = fields.String(allow_none=True) + uuid = fields.UUID(allow_none=True) def handle_error( self, diff --git a/superset/models/slice.py b/superset/models/slice.py index 2469db90ad0..f47f424fc3e 100644 --- a/superset/models/slice.py +++ b/superset/models/slice.py @@ -37,6 +37,7 @@ from sqlalchemy import ( from sqlalchemy.engine.base import Connection from sqlalchemy.orm import relationship from sqlalchemy.orm.mapper import Mapper +from sqlalchemy.sql.elements import BinaryExpression from superset import db, is_feature_enabled, security_manager from superset.legacy import update_time_range @@ -361,11 +362,17 @@ class Slice( # pylint: disable=too-many-public-methods return self.query_context_factory @classmethod - def get(cls, id_: int) -> Slice: - qry = db.session.query(Slice).filter_by(id=id_) + def get(cls, id_or_uuid: str) -> Slice: + qry = db.session.query(Slice).filter_by(id_or_uuid_filter(id_or_uuid)) return qry.one_or_none() +def id_or_uuid_filter(id_or_uuid: str) -> BinaryExpression: + if id_or_uuid.isdigit(): + return Slice.id == int(id_or_uuid) + return Slice.uuid == id_or_uuid + + def set_related_perm(_mapper: Mapper, _connection: Connection, target: Slice) -> None: src_class = target.cls_model if id_ := target.datasource_id: diff --git a/superset/tasks/thumbnails.py b/superset/tasks/thumbnails.py index 5c3e2e412dc..9ff5ccfedf8 100644 --- a/superset/tasks/thumbnails.py +++ b/superset/tasks/thumbnails.py @@ -37,7 +37,7 @@ logger = logging.getLogger(__name__) @celery_app.task(name="cache_chart_thumbnail", soft_time_limit=300) def cache_chart_thumbnail( current_user: Optional[str], - chart_id: int, + chart_id: str, force: bool, window_size: Optional[WindowSize] = None, thumb_size: Optional[WindowSize] = None, diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py index 89fb23fc393..1b95ed1f639 100644 --- a/tests/integration_tests/charts/api_tests.py +++ b/tests/integration_tests/charts/api_tests.py @@ -1055,6 +1055,12 @@ class TestChartApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCase): "id", "thumbnail_url", "url", + "uuid", + "datasource_id", + "datasource_name_text", + "datasource_type", + "datasource_url", + "datasource_uuid", ): assert value == expected_result[key] db.session.delete(chart) diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index 45aad249ecb..43da49abbbe 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -530,6 +530,7 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas "last_name": "user", }, "id": dashboard.id, + "uuid": str(dashboard.uuid), "css": "", "dashboard_title": "title", "datasources": [], diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index 91bada22759..2316414a12c 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -400,6 +400,7 @@ class TestDatasetApi(SupersetTestCase): "backend": main_db.backend, "database_name": "examples", "id": 1, + "uuid": ANY, }, "default_endpoint": None, "description": "Energy consumption",