# 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. import json import re from typing import Callable, List, Union from flask import g, redirect, request, Response from flask_appbuilder import expose from flask_appbuilder.actions import action from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_appbuilder.security.decorators import has_access from flask_babel import gettext as __, lazy_gettext as _ from flask_login import AnonymousUserMixin, LoginManager from superset import db, event_logger, is_feature_enabled, security_manager from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.dashboard import Dashboard as DashboardModel from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import ( BaseSupersetView, check_ownership, common_bootstrap_payload, DeleteMixin, generate_download_headers, SupersetModelView, ) from superset.views.dashboard.mixin import DashboardMixin class DashboardModelView( DashboardMixin, SupersetModelView, DeleteMixin ): # pylint: disable=too-many-ancestors route_base = "/dashboard" datamodel = SQLAInterface(DashboardModel) # TODO disable api_read and api_delete (used by cypress) # once we move to ChartRestModelApi class_permission_name = "Dashboard" method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP include_route_methods = RouteMethod.CRUD_SET | { RouteMethod.API_READ, RouteMethod.API_DELETE, "download_dashboards", } @has_access @expose("/list/") def list(self) -> FlaskResponse: if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): return super().list() return super().render_app_template() @action("mulexport", __("Export"), __("Export dashboards?"), "fa-database") def mulexport( # pylint: disable=no-self-use self, items: Union["DashboardModelView", List["DashboardModelView"]] ) -> FlaskResponse: if not isinstance(items, list): items = [items] ids = "".join("&id={}".format(d.id) for d in items) return redirect("/dashboard/export_dashboards_form?{}".format(ids[1:])) @event_logger.log_this @has_access @expose("/export_dashboards_form") def download_dashboards(self) -> FlaskResponse: if request.args.get("action") == "go": ids = request.args.getlist("id") return Response( DashboardModel.export_dashboards(ids), headers=generate_download_headers("json"), mimetype="application/text", ) return self.render_template( "superset/export_dashboards.html", dashboards_url="/dashboard/list" ) def pre_add(self, item: "DashboardModelView") -> None: item.slug = item.slug or None if item.slug: item.slug = item.slug.strip() item.slug = item.slug.replace(" ", "-") item.slug = re.sub(r"[^\w\-]+", "", item.slug) if g.user not in item.owners: item.owners.append(g.user) utils.validate_json(item.json_metadata) utils.validate_json(item.position_json) owners = list(item.owners) for slc in item.slices: slc.owners = list(set(owners) | set(slc.owners)) def pre_update(self, item: "DashboardModelView") -> None: check_ownership(item) self.pre_add(item) class Dashboard(BaseSupersetView): """The base views for Superset!""" class_permission_name = "Dashboard" method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP @has_access @expose("/new/") def new(self) -> FlaskResponse: # pylint: disable=no-self-use """Creates a new, blank dashboard and redirects to it in edit mode""" metadata = {} if is_feature_enabled("ENABLE_FILTER_BOX_MIGRATION"): metadata = { "native_filter_configuration": [], "show_native_filters": True, } new_dashboard = DashboardModel( dashboard_title="[ untitled dashboard ]", owners=[g.user], json_metadata=json.dumps(metadata, sort_keys=True), ) db.session.add(new_dashboard) db.session.commit() return redirect(f"/superset/dashboard/{new_dashboard.id}/?edit=true") @expose("//embedded") @event_logger.log_this_with_extra_payload def embedded( 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. used in the decorators :param add_extra_log_payload: added by `log_this_with_manual_updates`, set a default value to appease pylint """ if not is_feature_enabled("EMBEDDED_SUPERSET"): return Response(status=404) # Log in as an anonymous user, just for this view. # This view needs to be visible to all users, # and building the page fails if g.user and/or ctx.user aren't present. login_manager: LoginManager = security_manager.lm login_manager.reload_user(AnonymousUserMixin()) add_extra_log_payload( dashboard_id=dashboard_id_or_slug, dashboard_version="v2", ) bootstrap_data = { "common": common_bootstrap_payload(), "embedded": {"dashboard_id": dashboard_id_or_slug}, } return self.render_template( "superset/spa.html", entry="embedded", bootstrap_data=json.dumps( bootstrap_data, default=utils.pessimistic_json_iso_dttm_ser ), ) class DashboardModelViewAsync(DashboardModelView): # pylint: disable=too-many-ancestors route_base = "/dashboardasync" class_permission_name = "Dashboard" method_permission_name = MODEL_VIEW_RW_METHOD_PERMISSION_MAP include_route_methods = {RouteMethod.API_READ} list_columns = [ "id", "dashboard_link", "creator", "modified", "dashboard_title", "changed_on", "url", "changed_by_name", ] label_columns = { "dashboard_link": _("Dashboard"), "dashboard_title": _("Title"), "creator": _("Creator"), "modified": _("Modified"), }