diff --git a/superset/views/base.py b/superset/views/base.py index 25aa6896db..f2a9a8a88a 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -93,7 +93,7 @@ def data_payload_response(payload_json, has_error=False): def generate_download_headers(extension, filename=None): filename = filename if filename else datetime.now().strftime("%Y%m%d_%H%M%S") - content_disp = "attachment; filename={}.{}".format(filename, extension) + content_disp = f"attachment; filename={filename}.{extension}" headers = {"Content-Disposition": content_disp} return headers diff --git a/superset/views/dashboard/api.py b/superset/views/dashboard/api.py index 134506b2ac..92f74224bc 100644 --- a/superset/views/dashboard/api.py +++ b/superset/views/dashboard/api.py @@ -17,8 +17,8 @@ import json import re -from flask import current_app, g, request -from flask_appbuilder.api import expose, protect, safe +from flask import current_app, g, make_response, request +from flask_appbuilder.api import expose, protect, rison, safe from flask_appbuilder.models.sqla.interface import SQLAInterface from marshmallow import fields, post_load, pre_load, Schema, ValidationError from marshmallow.validate import Length @@ -31,6 +31,7 @@ from superset.views.base import ( BaseSupersetModelRestApi, BaseSupersetSchema, check_ownership_and_item_exists, + generate_download_headers, ) from .mixin import DashboardMixin @@ -159,6 +160,9 @@ class DashboardPutSchema(BaseDashboardSchema): return self.instance +get_export_ids_schema = {"type": "array", "items": {"type": "integer"}} + + class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi): datamodel = SQLAInterface(Dashboard) @@ -169,6 +173,7 @@ class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi): method_permission_name = { "get_list": "list", "get": "show", + "export": "mulexport", "post": "add", "put": "edit", "delete": "delete", @@ -356,3 +361,52 @@ class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi): return self.response(200, message="OK") except SQLAlchemyError as e: return self.response_422(message=str(e)) + + @expose("/export/", methods=["GET"]) + @protect() + @safe + @rison(get_export_ids_schema) + def export(self, **kwargs): + """Export dashboards + --- + get: + parameters: + - in: query + name: q + content: + application/json: + schema: + type: array + items: + type: integer + responses: + 200: + description: Dashboard export + content: + text/plain: + schema: + type: string + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 422: + $ref: '#/components/responses/422' + 500: + $ref: '#/components/responses/500' + """ + query = self.datamodel.session.query(Dashboard).filter( + Dashboard.id.in_(kwargs["rison"]) + ) + query = self._base_filters.apply_all(query) + ids = [item.id for item in query.all()] + if not ids: + return self.response_404() + export = Dashboard.export_dashboards(ids) + resp = make_response(export, 200) + resp.headers["Content-Disposition"] = generate_download_headers("json")[ + "Content-Disposition" + ] + return resp diff --git a/tests/dashboard_api_tests.py b/tests/dashboard_api_tests.py index 31303aab46..62e70ab252 100644 --- a/tests/dashboard_api_tests.py +++ b/tests/dashboard_api_tests.py @@ -24,6 +24,7 @@ from flask_appbuilder.security.sqla import models as ab_models from superset import db, security_manager from superset.models import core as models from superset.models.slice import Slice +from superset.views.base import generate_download_headers from .base_tests import SupersetTestCase @@ -423,3 +424,43 @@ class DashboardApiTests(SupersetTestCase): rv = self.client.get(uri) self.assertEqual(rv.status_code, 404) + + def test_export(self): + """ + Dashboard API: Test dashboard export + """ + self.login(username="admin") + argument = [1, 2] + uri = f"api/v1/dashboard/export/?q={prison.dumps(argument)}" + + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 200) + self.assertEqual( + rv.headers["Content-Disposition"], + generate_download_headers("json")["Content-Disposition"], + ) + + def test_export_not_found(self): + """ + Dashboard API: Test dashboard export not found + """ + self.login(username="admin") + argument = [1000] + uri = f"api/v1/dashboard/export/?q={prison.dumps(argument)}" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 404) + + def test_export_not_allowed(self): + """ + Dashboard API: Test dashboard export not not allowed + """ + admin_id = self.get_user("admin").id + dashboard = self.insert_dashboard("title", "slug1", [admin_id], published=False) + + self.login(username="gamma") + argument = [dashboard.id] + uri = f"api/v1/dashboard/export/?q={prison.dumps(argument)}" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 404) + db.session.delete(dashboard) + db.session.commit()