superset/views/core.py (754 lines of code) (raw):
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=invalid-name
from __future__ import annotations
import contextlib
import logging
from datetime import datetime
from typing import Any, Callable, cast
from urllib import parse
from flask import abort, flash, g, redirect, request, Response, url_for
from flask_appbuilder import expose
from flask_appbuilder.security.decorators import (
has_access,
has_access_api,
permission_name,
)
from flask_babel import gettext as __, lazy_gettext as _
from sqlalchemy.exc import SQLAlchemyError
from superset import (
app,
appbuilder,
conf,
db,
event_logger,
is_feature_enabled,
security_manager,
)
from superset.async_events.async_query_manager import AsyncQueryTokenException
from superset.commands.chart.exceptions import ChartNotFoundError
from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
from superset.commands.dashboard.exceptions import DashboardAccessDeniedError
from superset.commands.dashboard.permalink.get import GetDashboardPermalinkCommand
from superset.commands.dataset.exceptions import DatasetNotFoundError
from superset.commands.explore.form_data.create import CreateFormDataCommand
from superset.commands.explore.form_data.get import GetFormDataCommand
from superset.commands.explore.form_data.parameters import CommandParameters
from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
from superset.connectors.sqla.models import BaseDatasource, SqlaTable
from superset.daos.chart import ChartDAO
from superset.daos.datasource import DatasourceDAO
from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
from superset.exceptions import (
CacheLoadError,
SupersetException,
SupersetSecurityException,
)
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.extensions import async_query_manager, cache_manager
from superset.models.core import Database
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import Query
from superset.models.user_attributes import UserAttribute
from superset.superset_typing import FlaskResponse
from superset.utils import core as utils, json
from superset.utils.cache import etag_cache
from superset.utils.core import (
DatasourceType,
get_user_id,
ReservedUrlParameters,
)
from superset.views.base import (
api,
BaseSupersetView,
common_bootstrap_payload,
CsvResponse,
data_payload_response,
deprecated,
generate_download_headers,
json_error_response,
json_success,
)
from superset.views.error_handling import handle_api_exception
from superset.views.utils import (
bootstrap_user_data,
check_datasource_perms,
check_explore_cache_perms,
check_resource_permissions,
get_datasource_info,
get_form_data,
get_viz,
loads_request_json,
redirect_with_flash,
sanitize_datasource_data,
)
from superset.viz import BaseViz
config = app.config
SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = config["SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT"]
stats_logger = config["STATS_LOGGER"]
logger = logging.getLogger(__name__)
DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted")
USER_MISSING_ERR = __("The user seems to have been deleted")
PARAMETER_MISSING_ERR = __(
"Please check your template parameters for syntax errors and make sure "
"they match across your SQL query and Set Parameters. Then, try running "
"your query again."
)
SqlResults = dict[str, Any]
class Superset(BaseSupersetView):
"""The base views for Superset!"""
logger = logging.getLogger(__name__)
@has_access
@event_logger.log_this
@expose("/slice/<int:slice_id>/")
def slice(self, slice_id: int) -> FlaskResponse:
_, slc = get_form_data(slice_id, use_slice_data=True)
if not slc:
abort(404)
form_data = parse.quote(json.dumps({"slice_id": slice_id}))
endpoint_params = {"form_data": f"{form_data}"}
if ReservedUrlParameters.is_standalone_mode():
endpoint_params[ReservedUrlParameters.STANDALONE.value] = "true"
return redirect(url_for("ExploreView.root", **endpoint_params))
def get_query_string_response(self, viz_obj: BaseViz) -> FlaskResponse:
query = None
try:
if query_obj := viz_obj.query_obj():
query = viz_obj.datasource.get_query_str(query_obj)
except Exception as ex: # pylint: disable=broad-except
err_msg = utils.error_msg_from_exception(ex)
logger.exception(err_msg)
return json_error_response(err_msg)
if not query:
query = "No query."
return self.json_response(
{"query": query, "language": viz_obj.datasource.query_language}
)
def get_raw_results(self, viz_obj: BaseViz) -> FlaskResponse:
payload = viz_obj.get_df_payload()
if viz_obj.has_error(payload):
return json_error_response(payload=payload, status=400)
return self.json_response(
{
"data": payload["df"].to_dict("records"),
"colnames": payload.get("colnames"),
"coltypes": payload.get("coltypes"),
"rowcount": payload.get("rowcount"),
"sql_rowcount": payload.get("sql_rowcount"),
},
)
def get_samples(self, viz_obj: BaseViz) -> FlaskResponse:
return self.json_response(viz_obj.get_samples())
@staticmethod
def send_data_payload_response(viz_obj: BaseViz, payload: Any) -> FlaskResponse:
return data_payload_response(*viz_obj.payload_json_and_has_error(payload))
def generate_json(
self, viz_obj: BaseViz, response_type: str | None = None
) -> FlaskResponse:
if response_type == ChartDataResultFormat.CSV:
return CsvResponse(
viz_obj.get_csv(), headers=generate_download_headers("csv")
)
if response_type == ChartDataResultType.QUERY:
return self.get_query_string_response(viz_obj)
if response_type == ChartDataResultType.RESULTS:
return self.get_raw_results(viz_obj)
if response_type == ChartDataResultType.SAMPLES:
return self.get_samples(viz_obj)
payload = viz_obj.get_payload()
return self.send_data_payload_response(viz_obj, payload)
@event_logger.log_this
@api
@has_access_api
@handle_api_exception
@permission_name("explore_json")
@expose("/explore_json/data/<cache_key>", methods=("GET",))
@check_resource_permissions(check_explore_cache_perms)
@deprecated(eol_version="5.0.0")
def explore_json_data(self, cache_key: str) -> FlaskResponse:
"""Serves cached result data for async explore_json calls
`self.generate_json` receives this input and returns different
payloads based on the request args in the first block
TODO: form_data should not be loaded twice from cache
(also loaded in `check_explore_cache_perms`)
"""
try:
cached = cache_manager.cache.get(cache_key)
if not cached:
raise CacheLoadError("Cached data not found")
form_data = cached.get("form_data")
response_type = cached.get("response_type")
# Set form_data in Flask Global as it is used as a fallback
# for async queries with jinja context
g.form_data = form_data
datasource_id, datasource_type = get_datasource_info(None, None, form_data)
viz_obj = get_viz(
datasource_type=cast(str, datasource_type),
datasource_id=datasource_id,
form_data=form_data,
force_cached=True,
)
return self.generate_json(viz_obj, response_type)
except SupersetException as ex:
return json_error_response(utils.error_msg_from_exception(ex), 400)
@api
@has_access_api
@handle_api_exception
@event_logger.log_this
@expose(
"/explore_json/<datasource_type>/<int:datasource_id>/",
methods=(
"GET",
"POST",
),
)
@expose(
"/explore_json/",
methods=(
"GET",
"POST",
),
)
@etag_cache()
@check_resource_permissions(check_datasource_perms)
@deprecated(eol_version="5.0.0")
def explore_json(
self, datasource_type: str | None = None, datasource_id: int | None = None
) -> FlaskResponse:
"""Serves all request that GET or POST form_data
This endpoint evolved to be the entry point of many different
requests that GETs or POSTs a form_data.
`self.generate_json` receives this input and returns different
payloads based on the request args in the first block
TODO: break into one endpoint for each return shape"""
response_type = ChartDataResultFormat.JSON.value
responses: list[ChartDataResultFormat | ChartDataResultType] = list(
ChartDataResultFormat
)
responses.extend(list(ChartDataResultType))
for response_option in responses:
if request.args.get(response_option) == "true":
response_type = response_option
break
# Verify user has permission to export CSV file
if (
response_type == ChartDataResultFormat.CSV
and not security_manager.can_access("can_csv", "Superset")
):
return json_error_response(
_("You don't have the rights to download as csv"),
status=403,
)
form_data = get_form_data()[0]
try:
datasource_id, datasource_type = get_datasource_info(
datasource_id, datasource_type, form_data
)
force = request.args.get("force") == "true"
# TODO: support CSV, SQL query and other non-JSON types
if (
is_feature_enabled("GLOBAL_ASYNC_QUERIES")
and response_type == ChartDataResultFormat.JSON
):
# First, look for the chart query results in the cache.
with contextlib.suppress(CacheLoadError):
viz_obj = get_viz(
datasource_type=cast(str, datasource_type),
datasource_id=datasource_id,
form_data=form_data,
force_cached=True,
force=force,
)
payload = viz_obj.get_payload()
# If the chart query has already been cached, return it immediately.
if payload is not None:
return self.send_data_payload_response(viz_obj, payload)
# Otherwise, kick off a background job to run the chart query.
# Clients will either poll or be notified of query completion,
# at which point they will call the /explore_json/data/<cache_key>
# endpoint to retrieve the results.
try:
async_channel_id = (
async_query_manager.parse_channel_id_from_request(request)
)
job_metadata = async_query_manager.submit_explore_json_job(
async_channel_id, form_data, response_type, force, get_user_id()
)
except AsyncQueryTokenException:
return json_error_response("Not authorized", 401)
return json_success(json.dumps(job_metadata), status=202)
viz_obj = get_viz(
datasource_type=cast(str, datasource_type),
datasource_id=datasource_id,
form_data=form_data,
force=force,
)
return self.generate_json(viz_obj, response_type)
except SupersetException as ex:
return json_error_response(utils.error_msg_from_exception(ex), 400)
@staticmethod
def get_redirect_url() -> str:
"""Assembles the redirect URL to the new endpoint. It also replaces
the form_data param with a form_data_key by saving the original content
to the cache layer.
"""
redirect_url = request.url.replace("/superset/explore", "/explore")
form_data_key = None
if request_form_data := request.args.get("form_data"):
parsed_form_data = loads_request_json(request_form_data)
slice_id = parsed_form_data.get(
"slice_id", int(request.args.get("slice_id", 0))
)
if datasource := parsed_form_data.get("datasource"):
datasource_id, datasource_type = datasource.split("__")
parameters = CommandParameters(
datasource_id=datasource_id,
datasource_type=datasource_type,
chart_id=slice_id,
form_data=request_form_data,
)
form_data_key = CreateFormDataCommand(parameters).run()
if form_data_key:
url = parse.urlparse(redirect_url)
query = parse.parse_qs(url.query)
query.pop("form_data")
query["form_data_key"] = [form_data_key]
url = url._replace(query=parse.urlencode(query, True))
redirect_url = parse.urlunparse(url)
# Return a relative URL
url = parse.urlparse(redirect_url)
return f"{url.path}?{url.query}" if url.query else url.path
@has_access
@event_logger.log_this
@expose(
"/explore/<datasource_type>/<int:datasource_id>/",
methods=(
"GET",
"POST",
),
)
@expose(
"/explore/",
methods=(
"GET",
"POST",
),
)
@deprecated()
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
def explore( # noqa: C901
self,
datasource_type: str | None = None,
datasource_id: int | None = None,
key: str | None = None,
) -> FlaskResponse:
if request.method == "GET":
return redirect(Superset.get_redirect_url())
initial_form_data = {}
form_data_key = request.args.get("form_data_key")
if key is not None:
command = GetExplorePermalinkCommand(key)
try:
if permalink_value := command.run():
state = permalink_value["state"]
initial_form_data = state["formData"]
url_params = state.get("urlParams")
if url_params:
initial_form_data["url_params"] = dict(url_params)
else:
return json_error_response(
_("Error: permalink state not found"), status=404
)
except (ChartNotFoundError, ExplorePermalinkGetFailedError) as ex:
flash(__("Error: %(msg)s", msg=ex.message), "danger")
return redirect(url_for("SliceModelView.list"))
elif form_data_key:
parameters = CommandParameters(key=form_data_key)
value = GetFormDataCommand(parameters).run()
initial_form_data = json.loads(value) if value else {}
if not initial_form_data:
slice_id = request.args.get("slice_id")
dataset_id = request.args.get("dataset_id")
if slice_id:
initial_form_data["slice_id"] = slice_id
if form_data_key:
flash(
_("Form data not found in cache, reverting to chart metadata.")
)
elif dataset_id:
initial_form_data["datasource"] = f"{dataset_id}__table"
if form_data_key:
flash(
_(
"Form data not found in cache, reverting to dataset metadata." # noqa: E501
)
)
form_data, slc = get_form_data(
use_slice_data=True, initial_form_data=initial_form_data
)
query_context = request.form.get("query_context")
try:
datasource_id, datasource_type = get_datasource_info(
datasource_id, datasource_type, form_data
)
except SupersetException:
datasource_id = None
# fallback unknown datasource to table type
datasource_type = SqlaTable.type
datasource: BaseDatasource | None = None
if datasource_id is not None:
with contextlib.suppress(DatasetNotFoundError):
datasource = DatasourceDAO.get_datasource(
DatasourceType("table"),
datasource_id,
)
datasource_name = datasource.name if datasource else _("[Missing Dataset]")
viz_type = form_data.get("viz_type")
if not viz_type and datasource and datasource.default_endpoint:
return redirect(datasource.default_endpoint)
selectedColumns = [] # noqa: N806
if "selectedColumns" in form_data:
selectedColumns = form_data.pop("selectedColumns") # noqa: N806
if "viz_type" not in form_data:
form_data["viz_type"] = app.config["DEFAULT_VIZ_TYPE"]
if app.config["DEFAULT_VIZ_TYPE"] == "table":
all_columns = []
for x in selectedColumns:
all_columns.append(x["name"])
form_data["all_columns"] = all_columns
# slc perms
slice_add_perm = security_manager.can_access("can_write", "Chart")
slice_overwrite_perm = security_manager.is_owner(slc) if slc else False
slice_download_perm = security_manager.can_access("can_csv", "Superset")
form_data["datasource"] = str(datasource_id) + "__" + cast(str, datasource_type)
# On explore, merge legacy and extra filters into the form data
utils.convert_legacy_filters_into_adhoc(form_data)
utils.merge_extra_filters(form_data)
# merge request url params
if request.method == "GET":
utils.merge_request_params(form_data, request.args)
# handle save or overwrite
action = request.args.get("action")
if action == "overwrite" and not slice_overwrite_perm:
return json_error_response(
_("You don't have the rights to alter this chart"),
status=403,
)
if action == "saveas" and not slice_add_perm:
return json_error_response(
_("You don't have the rights to create a chart"),
status=403,
)
if action in ("saveas", "overwrite") and datasource:
return self.save_or_overwrite_slice(
slc,
slice_add_perm,
slice_overwrite_perm,
slice_download_perm,
datasource.id,
datasource.type,
datasource.name,
query_context,
)
standalone_mode = ReservedUrlParameters.is_standalone_mode()
force = request.args.get("force") in {"force", "1", "true"}
dummy_datasource_data: dict[str, Any] = {
"type": datasource_type,
"name": datasource_name,
"columns": [],
"metrics": [],
"database": {"id": 0, "backend": ""},
}
try:
datasource_data = datasource.data if datasource else dummy_datasource_data
except (SupersetException, SQLAlchemyError):
datasource_data = dummy_datasource_data
if datasource:
datasource_data["owners"] = datasource.owners_data
if isinstance(datasource, Query):
datasource_data["columns"] = datasource.columns
bootstrap_data = {
"can_add": slice_add_perm,
"datasource": sanitize_datasource_data(datasource_data),
"form_data": form_data,
"datasource_id": datasource_id,
"datasource_type": datasource_type,
"slice": slc.data if slc else None,
"standalone": standalone_mode,
"force": force,
"user": bootstrap_user_data(g.user, include_perms=True),
"forced_height": request.args.get("height"),
"common": common_bootstrap_payload(),
}
if slc:
title = slc.slice_name
elif datasource:
table_name = (
datasource.table_name
if datasource_type == "table"
else datasource.datasource_name
)
title = _("Explore - %(table)s", table=table_name)
else:
title = _("Explore")
return self.render_template(
"superset/basic.html",
bootstrap_data=json.dumps(
bootstrap_data, default=json.pessimistic_json_iso_dttm_ser
),
entry="explore",
title=title,
standalone_mode=standalone_mode,
)
@staticmethod
def save_or_overwrite_slice( # noqa: C901
# pylint: disable=too-many-arguments,too-many-locals
slc: Slice | None,
slice_add_perm: bool,
slice_overwrite_perm: bool,
slice_download_perm: bool,
datasource_id: int,
datasource_type: str,
datasource_name: str,
query_context: str | None = None,
) -> FlaskResponse:
"""Save or overwrite a slice"""
slice_name = request.args.get("slice_name")
action = request.args.get("action")
form_data = get_form_data()[0]
if action == "saveas":
if "slice_id" in form_data:
form_data.pop("slice_id") # don't save old slice_id
slc = Slice(owners=[g.user] if g.user else [])
utils.remove_extra_adhoc_filters(form_data)
assert slc
slc.params = json.dumps(form_data, indent=2, sort_keys=True)
slc.datasource_name = datasource_name
slc.viz_type = form_data["viz_type"]
slc.datasource_type = datasource_type
slc.datasource_id = datasource_id
slc.last_saved_by = g.user
slc.last_saved_at = datetime.now()
slc.slice_name = slice_name
slc.query_context = query_context
if action == "saveas" and slice_add_perm:
ChartDAO.create(slc)
db.session.commit() # pylint: disable=consider-using-transaction
msg = _("Chart [{}] has been saved").format(slc.slice_name)
flash(msg, "success")
elif action == "overwrite" and slice_overwrite_perm:
ChartDAO.update(slc)
db.session.commit() # pylint: disable=consider-using-transaction
msg = _("Chart [{}] has been overwritten").format(slc.slice_name)
flash(msg, "success")
# Adding slice to a dashboard if requested
dash: Dashboard | None = None
save_to_dashboard_id = request.args.get("save_to_dashboard_id")
new_dashboard_name = request.args.get("new_dashboard_name")
if save_to_dashboard_id:
# Adding the chart to an existing dashboard
dash = cast(
Dashboard,
db.session.query(Dashboard)
.filter_by(id=int(save_to_dashboard_id))
.one(),
)
# check edit dashboard permissions
dash_overwrite_perm = security_manager.is_owner(dash)
if not dash_overwrite_perm:
return json_error_response(
_("You don't have the rights to alter this dashboard"),
status=403,
)
flash(
_("Chart [{}] was added to dashboard [{}]").format(
slc.slice_name, dash.dashboard_title
),
"success",
)
elif new_dashboard_name:
# Creating and adding to a new dashboard
# check create dashboard permissions
dash_add_perm = security_manager.can_access("can_write", "Dashboard")
if not dash_add_perm:
return json_error_response(
_("You don't have the rights to create a dashboard"),
status=403,
)
dash = Dashboard(
dashboard_title=request.args.get("new_dashboard_name"),
owners=[g.user] if g.user else [],
)
flash(
_(
"Dashboard [{}] just got created and chart [{}] was added to it"
).format(dash.dashboard_title, slc.slice_name),
"success",
)
if dash and slc not in dash.slices:
dash.slices.append(slc)
db.session.commit() # pylint: disable=consider-using-transaction
response = {
"can_add": slice_add_perm,
"can_download": slice_download_perm,
"form_data": slc.form_data,
"slice": slc.data,
"dashboard_url": dash.url if dash else None,
"dashboard_id": dash.id if dash else None,
}
if dash and request.args.get("goto_dash") == "true":
response.update({"dashboard": dash.url})
return json_success(json.dumps(response))
@event_logger.log_this
@api
@has_access_api
@expose("/warm_up_cache/", methods=("GET",))
@deprecated(new_target="api/v1/chart/warm_up_cache/")
def warm_up_cache(self) -> FlaskResponse:
"""Warms up the cache for the slice or table.
Note for slices a force refresh occurs.
In terms of the `extra_filters` these can be obtained from records in the JSON
encoded `logs.json` column associated with the `explore_json` action.
"""
slice_id = request.args.get("slice_id")
dashboard_id = request.args.get("dashboard_id")
table_name = request.args.get("table_name")
db_name = request.args.get("db_name")
extra_filters = request.args.get("extra_filters")
slices: list[Slice] = []
if not slice_id and not (table_name and db_name):
return json_error_response(
__(
"Malformed request. slice_id or table_name and db_name "
"arguments are expected"
),
status=400,
)
if slice_id:
slices = db.session.query(Slice).filter_by(id=slice_id).all()
if not slices:
return json_error_response(
__("Chart %(id)s not found", id=slice_id), status=404
)
elif table_name and db_name:
table = (
db.session.query(SqlaTable)
.join(Database)
.filter(
Database.database_name == db_name
or SqlaTable.table_name == table_name
)
).one_or_none()
if not table:
return json_error_response(
__(
"Table %(table)s wasn't found in the database %(db)s",
table=table_name,
db=db_name,
),
status=404,
)
slices = (
db.session.query(Slice)
.filter_by(datasource_id=table.id, datasource_type=table.type)
.all()
)
return json_success(
json.dumps(
[
{
"slice_id" if key == "chart_id" else key: value
for key, value in ChartWarmUpCacheCommand(
slc, dashboard_id, extra_filters
)
.run()
.items()
}
for slc in slices
],
default=json.base_json_conv,
),
)
@has_access
@expose("/dashboard/<dashboard_id_or_slug>/")
@event_logger.log_this_with_extra_payload
def dashboard(
self,
dashboard_id_or_slug: str,
add_extra_log_payload: Callable[..., None] = lambda **kwargs: None,
) -> FlaskResponse:
"""
Server side rendering for a dashboard.
:param dashboard_id_or_slug: identifier for dashboard
:param add_extra_log_payload: added by `log_this_with_manual_updates`, set a
default value to appease pylint
"""
dashboard = Dashboard.get(dashboard_id_or_slug)
if not dashboard:
abort(404)
try:
dashboard.raise_for_access()
except SupersetSecurityException as ex:
# anonymous users should get the login screen, others should go to dashboard list # noqa: E501
if g.user is None or g.user.is_anonymous:
redirect_url = f"{appbuilder.get_url_for_login}?next={request.url}"
warn_msg = "Users must be logged in to view this dashboard."
else:
redirect_url = url_for("DashboardModelView.list")
warn_msg = utils.error_msg_from_exception(ex)
return redirect_with_flash(
url=redirect_url,
message=warn_msg,
category="danger",
)
add_extra_log_payload(
dashboard_id=dashboard.id,
dashboard_version="v2",
dash_edit_perm=(
security_manager.is_owner(dashboard)
and security_manager.can_access("can_write", "Dashboard")
),
edit_mode=(
request.args.get(ReservedUrlParameters.EDIT_MODE.value) == "true"
),
)
return self.render_template(
"superset/spa.html",
entry="spa",
title=dashboard.dashboard_title, # dashboard title is always visible
bootstrap_data=json.dumps(
{
"user": bootstrap_user_data(g.user, include_perms=True),
"common": common_bootstrap_payload(),
},
default=json.pessimistic_json_iso_dttm_ser,
),
standalone_mode=ReservedUrlParameters.is_standalone_mode(),
)
@has_access
@expose("/dashboard/p/<key>/", methods=("GET",))
def dashboard_permalink(
self,
key: str,
) -> FlaskResponse:
try:
value = GetDashboardPermalinkCommand(key).run()
except DashboardPermalinkGetFailedError as ex:
flash(__("Error: %(msg)s", msg=ex.message), "danger")
return redirect(url_for("DashboardModelView.list"))
except DashboardAccessDeniedError as ex:
flash(__("Error: %(msg)s", msg=ex.message), "danger")
return redirect(url_for("DashboardModelView.list"))
if not value:
return json_error_response(_("permalink state not found"), status=404)
dashboard_id, state = value["dashboardId"], value.get("state", {})
url = url_for(
"Superset.dashboard", dashboard_id_or_slug=dashboard_id, permalink_key=key
)
if url_params := state.get("urlParams"):
params = parse.urlencode(url_params)
url = f"{url}&{params}"
if original_params := request.query_string.decode():
url = f"{url}&{original_params}"
if hash_ := state.get("anchor", state.get("hash")):
url = f"{url}#{hash_}"
return redirect(url)
@api
@has_access
@event_logger.log_this
@expose("/log/", methods=("POST",))
def log(self) -> FlaskResponse:
return Response(status=200)
@api
@handle_api_exception
@has_access
@event_logger.log_this
@expose("/fetch_datasource_metadata")
@deprecated(
new_target="api/v1/database/<int:pk>/table/<path:table_name>/<schema_name>/"
)
def fetch_datasource_metadata(self) -> FlaskResponse:
"""
Fetch the datasource metadata.
:returns: The Flask response
:raises SupersetSecurityException: If the user cannot access the resource
"""
datasource_id, datasource_type = request.args["datasourceKey"].split("__")
datasource = DatasourceDAO.get_datasource(
DatasourceType(datasource_type), int(datasource_id)
)
# Check if datasource exists
if not datasource:
return json_error_response(DATASOURCE_MISSING_ERR)
datasource.raise_for_access()
return json_success(json.dumps(sanitize_datasource_data(datasource.data)))
@event_logger.log_this
@expose("/welcome/")
def welcome(self) -> FlaskResponse:
"""Personalized welcome page"""
if not g.user or not get_user_id():
if conf["PUBLIC_ROLE_LIKE"]:
return self.render_template("superset/public_welcome.html")
return redirect(appbuilder.get_url_for_login)
if welcome_dashboard_id := (
db.session.query(UserAttribute.welcome_dashboard_id)
.filter_by(user_id=get_user_id())
.scalar()
):
return self.dashboard(dashboard_id_or_slug=str(welcome_dashboard_id))
payload = {
"user": bootstrap_user_data(g.user, include_perms=True),
"common": common_bootstrap_payload(),
}
return self.render_template(
"superset/spa.html",
entry="spa",
bootstrap_data=json.dumps(
payload, default=json.pessimistic_json_iso_dttm_ser
),
)
@has_access
@event_logger.log_this
@expose("/sqllab/history/", methods=("GET",))
@deprecated(new_target="/sqllab/history")
def sqllab_history(self) -> FlaskResponse:
return redirect(url_for("SqllabView.history"))