mirror of https://github.com/apache/superset.git
[dashboards] New, export api (#8941)
* [dashboards] Multiple exports * [dashboards] Fix, mulexport permission missing * [dashboards] Test for security filtered export * [dashboards] Address PR comments
This commit is contained in:
parent
65c5922a3e
commit
123246fca6
|
@ -93,7 +93,7 @@ def data_payload_response(payload_json, has_error=False):
|
||||||
|
|
||||||
def generate_download_headers(extension, filename=None):
|
def generate_download_headers(extension, filename=None):
|
||||||
filename = filename if filename else datetime.now().strftime("%Y%m%d_%H%M%S")
|
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}
|
headers = {"Content-Disposition": content_disp}
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from flask import current_app, g, request
|
from flask import current_app, g, make_response, request
|
||||||
from flask_appbuilder.api import expose, protect, safe
|
from flask_appbuilder.api import expose, protect, rison, safe
|
||||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
from marshmallow import fields, post_load, pre_load, Schema, ValidationError
|
from marshmallow import fields, post_load, pre_load, Schema, ValidationError
|
||||||
from marshmallow.validate import Length
|
from marshmallow.validate import Length
|
||||||
|
@ -31,6 +31,7 @@ from superset.views.base import (
|
||||||
BaseSupersetModelRestApi,
|
BaseSupersetModelRestApi,
|
||||||
BaseSupersetSchema,
|
BaseSupersetSchema,
|
||||||
check_ownership_and_item_exists,
|
check_ownership_and_item_exists,
|
||||||
|
generate_download_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .mixin import DashboardMixin
|
from .mixin import DashboardMixin
|
||||||
|
@ -159,6 +160,9 @@ class DashboardPutSchema(BaseDashboardSchema):
|
||||||
return self.instance
|
return self.instance
|
||||||
|
|
||||||
|
|
||||||
|
get_export_ids_schema = {"type": "array", "items": {"type": "integer"}}
|
||||||
|
|
||||||
|
|
||||||
class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi):
|
class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi):
|
||||||
datamodel = SQLAInterface(Dashboard)
|
datamodel = SQLAInterface(Dashboard)
|
||||||
|
|
||||||
|
@ -169,6 +173,7 @@ class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi):
|
||||||
method_permission_name = {
|
method_permission_name = {
|
||||||
"get_list": "list",
|
"get_list": "list",
|
||||||
"get": "show",
|
"get": "show",
|
||||||
|
"export": "mulexport",
|
||||||
"post": "add",
|
"post": "add",
|
||||||
"put": "edit",
|
"put": "edit",
|
||||||
"delete": "delete",
|
"delete": "delete",
|
||||||
|
@ -356,3 +361,52 @@ class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi):
|
||||||
return self.response(200, message="OK")
|
return self.response(200, message="OK")
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
return self.response_422(message=str(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
|
||||||
|
|
|
@ -24,6 +24,7 @@ from flask_appbuilder.security.sqla import models as ab_models
|
||||||
from superset import db, security_manager
|
from superset import db, security_manager
|
||||||
from superset.models import core as models
|
from superset.models import core as models
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
|
from superset.views.base import generate_download_headers
|
||||||
|
|
||||||
from .base_tests import SupersetTestCase
|
from .base_tests import SupersetTestCase
|
||||||
|
|
||||||
|
@ -423,3 +424,43 @@ class DashboardApiTests(SupersetTestCase):
|
||||||
|
|
||||||
rv = self.client.get(uri)
|
rv = self.client.get(uri)
|
||||||
self.assertEqual(rv.status_code, 404)
|
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()
|
||||||
|
|
Loading…
Reference in New Issue