mirror of https://github.com/apache/superset.git
[dashboard] New, add statsd metrics to the API (#9519)
This commit is contained in:
parent
d9ebd32485
commit
7b11b44abe
|
@ -51,7 +51,11 @@ from superset.models.dashboard import Dashboard
|
||||||
from superset.tasks.thumbnails import cache_dashboard_thumbnail
|
from superset.tasks.thumbnails import cache_dashboard_thumbnail
|
||||||
from superset.utils.screenshots import DashboardScreenshot
|
from superset.utils.screenshots import DashboardScreenshot
|
||||||
from superset.views.base import generate_download_headers
|
from superset.views.base import generate_download_headers
|
||||||
from superset.views.base_api import BaseSupersetModelRestApi, RelatedFieldFilter
|
from superset.views.base_api import (
|
||||||
|
BaseSupersetModelRestApi,
|
||||||
|
RelatedFieldFilter,
|
||||||
|
statsd_metrics,
|
||||||
|
)
|
||||||
from superset.views.filters import FilterRelatedOwners
|
from superset.views.filters import FilterRelatedOwners
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -139,6 +143,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
@expose("/", methods=["POST"])
|
@expose("/", methods=["POST"])
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
def post(self) -> Response:
|
def post(self) -> Response:
|
||||||
"""Creates a new Dashboard
|
"""Creates a new Dashboard
|
||||||
---
|
---
|
||||||
|
@ -193,6 +198,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
@expose("/<pk>", methods=["PUT"])
|
@expose("/<pk>", methods=["PUT"])
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
def put( # pylint: disable=too-many-return-statements, arguments-differ
|
def put( # pylint: disable=too-many-return-statements, arguments-differ
|
||||||
self, pk: int
|
self, pk: int
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
@ -260,6 +266,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
@expose("/<pk>", methods=["DELETE"])
|
@expose("/<pk>", methods=["DELETE"])
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ
|
def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ
|
||||||
"""Deletes a Dashboard
|
"""Deletes a Dashboard
|
||||||
---
|
---
|
||||||
|
@ -306,6 +313,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
@expose("/", methods=["DELETE"])
|
@expose("/", methods=["DELETE"])
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
@rison(get_delete_ids_schema)
|
@rison(get_delete_ids_schema)
|
||||||
def bulk_delete(
|
def bulk_delete(
|
||||||
self, **kwargs: Any
|
self, **kwargs: Any
|
||||||
|
@ -366,6 +374,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||||
@expose("/export/", methods=["GET"])
|
@expose("/export/", methods=["GET"])
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
@rison(get_export_ids_schema)
|
@rison(get_export_ids_schema)
|
||||||
def export(self, **kwargs: Any) -> Response:
|
def export(self, **kwargs: Any) -> Response:
|
||||||
"""Export dashboards
|
"""Export dashboards
|
||||||
|
|
|
@ -38,8 +38,10 @@ from email.mime.text import MIMEText
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from time import struct_time
|
from time import struct_time
|
||||||
|
from timeit import default_timer
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
Callable,
|
||||||
Dict,
|
Dict,
|
||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
|
@ -1223,6 +1225,21 @@ def create_ssl_cert_file(certificate: str) -> str:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def time_function(func: Callable, *args, **kwargs) -> Tuple[float, Any]:
|
||||||
|
"""
|
||||||
|
Measures the amount of time a function takes to execute in ms
|
||||||
|
|
||||||
|
:param func: The function execution time to measure
|
||||||
|
:param args: args to be passed to the function
|
||||||
|
:param kwargs: kwargs to be passed to the function
|
||||||
|
:return: A tuple with the duration and response from the function
|
||||||
|
"""
|
||||||
|
start = default_timer()
|
||||||
|
response = func(*args, **kwargs)
|
||||||
|
stop = default_timer()
|
||||||
|
return stop - start, response
|
||||||
|
|
||||||
|
|
||||||
def MediumText() -> Variant:
|
def MediumText() -> Variant:
|
||||||
return Text().with_variant(MEDIUMTEXT(), "mysql")
|
return Text().with_variant(MEDIUMTEXT(), "mysql")
|
||||||
|
|
||||||
|
|
|
@ -14,15 +14,18 @@
|
||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
from typing import cast, Dict, Set, Tuple, Type, Union
|
from typing import Any, cast, Dict, Optional, Set, Tuple, Type, Union
|
||||||
|
|
||||||
|
from flask import Response
|
||||||
from flask_appbuilder import ModelRestApi
|
from flask_appbuilder import ModelRestApi
|
||||||
from flask_appbuilder.api import expose, protect, rison, safe
|
from flask_appbuilder.api import expose, protect, rison, safe
|
||||||
from flask_appbuilder.models.filters import BaseFilter, Filters
|
from flask_appbuilder.models.filters import BaseFilter, Filters
|
||||||
from flask_appbuilder.models.sqla.filters import FilterStartsWith
|
from flask_appbuilder.models.sqla.filters import FilterStartsWith
|
||||||
|
|
||||||
from superset.stats_logger import BaseStatsLogger
|
from superset.stats_logger import BaseStatsLogger
|
||||||
|
from superset.utils.core import time_function
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
get_related_schema = {
|
get_related_schema = {
|
||||||
|
@ -35,6 +38,19 @@ get_related_schema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def statsd_metrics(f):
|
||||||
|
"""
|
||||||
|
Handle sending all statsd metrics from the REST API
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wraps(self, *args: Any, **kwargs: Any) -> Response:
|
||||||
|
duration, response = time_function(f, self, *args, **kwargs)
|
||||||
|
self.send_stats_metrics(response, f.__name__, duration)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return functools.update_wrapper(wraps, f)
|
||||||
|
|
||||||
|
|
||||||
class RelatedFieldFilter:
|
class RelatedFieldFilter:
|
||||||
# data class to specify what filter to use on a /related endpoint
|
# data class to specify what filter to use on a /related endpoint
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
|
@ -128,11 +144,71 @@ class BaseSupersetModelRestApi(ModelRestApi):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def incr_stats(self, action: str, func_name: str) -> None:
|
def incr_stats(self, action: str, func_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Proxy function for statsd.incr to impose a key structure for REST API's
|
||||||
|
|
||||||
|
:param action: String with an action name eg: error, success
|
||||||
|
:param func_name: The function name
|
||||||
|
"""
|
||||||
self.stats_logger.incr(f"{self.__class__.__name__}.{func_name}.{action}")
|
self.stats_logger.incr(f"{self.__class__.__name__}.{func_name}.{action}")
|
||||||
|
|
||||||
|
def timing_stats(self, action: str, func_name: str, value: float) -> None:
|
||||||
|
"""
|
||||||
|
Proxy function for statsd.incr to impose a key structure for REST API's
|
||||||
|
|
||||||
|
:param action: String with an action name eg: error, success
|
||||||
|
:param func_name: The function name
|
||||||
|
:param value: A float with the time it took for the endpoint to execute
|
||||||
|
"""
|
||||||
|
self.stats_logger.timing(
|
||||||
|
f"{self.__class__.__name__}.{func_name}.{action}", value
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_stats_metrics(
|
||||||
|
self, response: Response, key: str, time_delta: Optional[float] = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Helper function to handle sending statsd metrics
|
||||||
|
|
||||||
|
:param response: flask response object, will evaluate if it was an error
|
||||||
|
:param key: The function name
|
||||||
|
:param time_delta: Optional time it took for the endpoint to execute
|
||||||
|
"""
|
||||||
|
if 200 <= response.status_code < 400:
|
||||||
|
self.incr_stats("success", key)
|
||||||
|
else:
|
||||||
|
self.incr_stats("error", key)
|
||||||
|
if time_delta:
|
||||||
|
self.timing_stats("time", key, time_delta)
|
||||||
|
|
||||||
|
def info_headless(self, **kwargs) -> Response:
|
||||||
|
"""
|
||||||
|
Add statsd metrics to builtin FAB _info endpoint
|
||||||
|
"""
|
||||||
|
duration, response = time_function(super().info_headless, **kwargs)
|
||||||
|
self.send_stats_metrics(response, self.info.__name__, duration)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_headless(self, pk, **kwargs) -> Response:
|
||||||
|
"""
|
||||||
|
Add statsd metrics to builtin FAB GET endpoint
|
||||||
|
"""
|
||||||
|
duration, response = time_function(super().get_headless, pk, **kwargs)
|
||||||
|
self.send_stats_metrics(response, self.get.__name__, duration)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_list_headless(self, **kwargs) -> Response:
|
||||||
|
"""
|
||||||
|
Add statsd metrics to builtin FAB GET list endpoint
|
||||||
|
"""
|
||||||
|
duration, response = time_function(super().get_list_headless, **kwargs)
|
||||||
|
self.send_stats_metrics(response, self.get_list.__name__, duration)
|
||||||
|
return response
|
||||||
|
|
||||||
@expose("/related/<column_name>", methods=["GET"])
|
@expose("/related/<column_name>", methods=["GET"])
|
||||||
@protect()
|
@protect()
|
||||||
@safe
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
@rison(get_related_schema)
|
@rison(get_related_schema)
|
||||||
def related(self, column_name: str, **kwargs):
|
def related(self, column_name: str, **kwargs):
|
||||||
"""Get related fields data
|
"""Get related fields data
|
||||||
|
@ -185,6 +261,7 @@ class BaseSupersetModelRestApi(ModelRestApi):
|
||||||
$ref: '#/components/responses/500'
|
$ref: '#/components/responses/500'
|
||||||
"""
|
"""
|
||||||
if column_name not in self.allowed_rel_fields:
|
if column_name not in self.allowed_rel_fields:
|
||||||
|
self.incr_stats("error", self.related.__name__)
|
||||||
return self.response_404()
|
return self.response_404()
|
||||||
args = kwargs.get("rison", {})
|
args = kwargs.get("rison", {})
|
||||||
# handle pagination
|
# handle pagination
|
||||||
|
|
|
@ -18,10 +18,11 @@
|
||||||
"""Unit tests for Superset"""
|
"""Unit tests for Superset"""
|
||||||
import imp
|
import imp
|
||||||
import json
|
import json
|
||||||
from typing import Union
|
from typing import Union, Dict
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from flask import Response
|
||||||
from flask_appbuilder.security.sqla import models as ab_models
|
from flask_appbuilder.security.sqla import models as ab_models
|
||||||
from flask_testing import TestCase
|
from flask_testing import TestCase
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ from superset.models.core import Database
|
||||||
from superset.models.dashboard import Dashboard
|
from superset.models.dashboard import Dashboard
|
||||||
from superset.models.datasource_access_request import DatasourceAccessRequest
|
from superset.models.datasource_access_request import DatasourceAccessRequest
|
||||||
from superset.utils.core import get_example_database
|
from superset.utils.core import get_example_database
|
||||||
|
from superset.views.base_api import BaseSupersetModelRestApi
|
||||||
|
|
||||||
FAKE_DB_NAME = "fake_db_100"
|
FAKE_DB_NAME = "fake_db_100"
|
||||||
|
|
||||||
|
@ -328,3 +330,81 @@ class SupersetTestCase(TestCase):
|
||||||
def get_dash_by_slug(self, dash_slug):
|
def get_dash_by_slug(self, dash_slug):
|
||||||
sesh = db.session()
|
sesh = db.session()
|
||||||
return sesh.query(Dashboard).filter_by(slug=dash_slug).first()
|
return sesh.query(Dashboard).filter_by(slug=dash_slug).first()
|
||||||
|
|
||||||
|
def get_assert_metric(self, uri: str, func_name: str) -> Response:
|
||||||
|
"""
|
||||||
|
Simple client get with an extra assertion for statsd metrics
|
||||||
|
|
||||||
|
:param uri: The URI to use for the HTTP GET
|
||||||
|
:param func_name: The function name that the HTTP GET triggers
|
||||||
|
for the statsd metric assertion
|
||||||
|
:return: HTTP Response
|
||||||
|
"""
|
||||||
|
with patch.object(
|
||||||
|
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||||
|
) as mock_method:
|
||||||
|
rv = self.client.get(uri)
|
||||||
|
if 200 <= rv.status_code < 400:
|
||||||
|
mock_method.assert_called_once_with("success", func_name)
|
||||||
|
else:
|
||||||
|
mock_method.assert_called_once_with("error", func_name)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def delete_assert_metric(self, uri: str, func_name: str) -> Response:
|
||||||
|
"""
|
||||||
|
Simple client delete with an extra assertion for statsd metrics
|
||||||
|
|
||||||
|
:param uri: The URI to use for the HTTP DELETE
|
||||||
|
:param func_name: The function name that the HTTP DELETE triggers
|
||||||
|
for the statsd metric assertion
|
||||||
|
:return: HTTP Response
|
||||||
|
"""
|
||||||
|
with patch.object(
|
||||||
|
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||||
|
) as mock_method:
|
||||||
|
rv = self.client.delete(uri)
|
||||||
|
if 200 <= rv.status_code < 400:
|
||||||
|
mock_method.assert_called_once_with("success", func_name)
|
||||||
|
else:
|
||||||
|
mock_method.assert_called_once_with("error", func_name)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def post_assert_metric(self, uri: str, data: Dict, func_name: str) -> Response:
|
||||||
|
"""
|
||||||
|
Simple client post with an extra assertion for statsd metrics
|
||||||
|
|
||||||
|
:param uri: The URI to use for the HTTP POST
|
||||||
|
:param data: The JSON data payload to be posted
|
||||||
|
:param func_name: The function name that the HTTP POST triggers
|
||||||
|
for the statsd metric assertion
|
||||||
|
:return: HTTP Response
|
||||||
|
"""
|
||||||
|
with patch.object(
|
||||||
|
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||||
|
) as mock_method:
|
||||||
|
rv = self.client.post(uri, json=data)
|
||||||
|
if 200 <= rv.status_code < 400:
|
||||||
|
mock_method.assert_called_once_with("success", func_name)
|
||||||
|
else:
|
||||||
|
mock_method.assert_called_once_with("error", func_name)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def put_assert_metric(self, uri: str, data: Dict, func_name: str) -> Response:
|
||||||
|
"""
|
||||||
|
Simple client put with an extra assertion for statsd metrics
|
||||||
|
|
||||||
|
:param uri: The URI to use for the HTTP PUT
|
||||||
|
:param data: The JSON data payload to be posted
|
||||||
|
:param func_name: The function name that the HTTP PUT triggers
|
||||||
|
for the statsd metric assertion
|
||||||
|
:return: HTTP Response
|
||||||
|
"""
|
||||||
|
with patch.object(
|
||||||
|
BaseSupersetModelRestApi, "incr_stats", return_value=None
|
||||||
|
) as mock_method:
|
||||||
|
rv = self.client.put(uri, json=data)
|
||||||
|
if 200 <= rv.status_code < 400:
|
||||||
|
mock_method.assert_called_once_with("success", func_name)
|
||||||
|
else:
|
||||||
|
mock_method.assert_called_once_with("error", func_name)
|
||||||
|
return rv
|
||||||
|
|
|
@ -85,7 +85,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
dashboard = self.insert_dashboard("title", "slug1", [admin.id])
|
dashboard = self.insert_dashboard("title", "slug1", [admin.id])
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
uri = f"api/v1/dashboard/{dashboard.id}"
|
uri = f"api/v1/dashboard/{dashboard.id}"
|
||||||
rv = self.client.get(uri)
|
rv = self.get_assert_metric(uri, "get")
|
||||||
self.assertEqual(rv.status_code, 200)
|
self.assertEqual(rv.status_code, 200)
|
||||||
expected_result = {
|
expected_result = {
|
||||||
"changed_by": None,
|
"changed_by": None,
|
||||||
|
@ -121,6 +121,15 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
db.session.delete(dashboard)
|
db.session.delete(dashboard)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
def test_info_dashboard(self):
|
||||||
|
"""
|
||||||
|
Dashboard API: Test info
|
||||||
|
"""
|
||||||
|
self.login(username="admin")
|
||||||
|
uri = f"api/v1/dashboard/_info"
|
||||||
|
rv = self.get_assert_metric(uri, "info")
|
||||||
|
self.assertEqual(rv.status_code, 200)
|
||||||
|
|
||||||
def test_get_dashboard_not_found(self):
|
def test_get_dashboard_not_found(self):
|
||||||
"""
|
"""
|
||||||
Dashboard API: Test get dashboard not found
|
Dashboard API: Test get dashboard not found
|
||||||
|
@ -128,7 +137,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
max_id = db.session.query(func.max(Dashboard.id)).scalar()
|
max_id = db.session.query(func.max(Dashboard.id)).scalar()
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
uri = f"api/v1/dashboard/{max_id + 1}"
|
uri = f"api/v1/dashboard/{max_id + 1}"
|
||||||
rv = self.client.get(uri)
|
rv = self.get_assert_metric(uri, "get")
|
||||||
self.assertEqual(rv.status_code, 404)
|
self.assertEqual(rv.status_code, 404)
|
||||||
|
|
||||||
def test_get_dashboard_no_data_access(self):
|
def test_get_dashboard_no_data_access(self):
|
||||||
|
@ -160,7 +169,8 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
"filters": [{"col": "dashboard_title", "opr": "sw", "value": "ti"}]
|
"filters": [{"col": "dashboard_title", "opr": "sw", "value": "ti"}]
|
||||||
}
|
}
|
||||||
uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}"
|
uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}"
|
||||||
rv = self.client.get(uri)
|
|
||||||
|
rv = self.get_assert_metric(uri, "get_list")
|
||||||
self.assertEqual(rv.status_code, 200)
|
self.assertEqual(rv.status_code, 200)
|
||||||
data = json.loads(rv.data.decode("utf-8"))
|
data = json.loads(rv.data.decode("utf-8"))
|
||||||
self.assertEqual(data["count"], 1)
|
self.assertEqual(data["count"], 1)
|
||||||
|
@ -259,7 +269,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
dashboard_id = self.insert_dashboard("title", "slug1", [admin_id]).id
|
dashboard_id = self.insert_dashboard("title", "slug1", [admin_id]).id
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||||
rv = self.client.delete(uri)
|
rv = self.delete_assert_metric(uri, "delete")
|
||||||
self.assertEqual(rv.status_code, 200)
|
self.assertEqual(rv.status_code, 200)
|
||||||
model = db.session.query(Dashboard).get(dashboard_id)
|
model = db.session.query(Dashboard).get(dashboard_id)
|
||||||
self.assertEqual(model, None)
|
self.assertEqual(model, None)
|
||||||
|
@ -282,7 +292,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
argument = dashboard_ids
|
argument = dashboard_ids
|
||||||
uri = f"api/v1/dashboard/?q={prison.dumps(argument)}"
|
uri = f"api/v1/dashboard/?q={prison.dumps(argument)}"
|
||||||
rv = self.client.delete(uri)
|
rv = self.delete_assert_metric(uri, "bulk_delete")
|
||||||
self.assertEqual(rv.status_code, 200)
|
self.assertEqual(rv.status_code, 200)
|
||||||
response = json.loads(rv.data.decode("utf-8"))
|
response = json.loads(rv.data.decode("utf-8"))
|
||||||
expected_response = {"message": f"Deleted {dashboard_count} dashboards"}
|
expected_response = {"message": f"Deleted {dashboard_count} dashboards"}
|
||||||
|
@ -469,7 +479,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
}
|
}
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
uri = "api/v1/dashboard/"
|
uri = "api/v1/dashboard/"
|
||||||
rv = self.client.post(uri, json=dashboard_data)
|
rv = self.post_assert_metric(uri, dashboard_data, "post")
|
||||||
self.assertEqual(rv.status_code, 201)
|
self.assertEqual(rv.status_code, 201)
|
||||||
data = json.loads(rv.data.decode("utf-8"))
|
data = json.loads(rv.data.decode("utf-8"))
|
||||||
model = db.session.query(Dashboard).get(data.get("id"))
|
model = db.session.query(Dashboard).get(data.get("id"))
|
||||||
|
@ -521,7 +531,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
dashboard_data = {"dashboard_title": "a" * 600}
|
dashboard_data = {"dashboard_title": "a" * 600}
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
uri = "api/v1/dashboard/"
|
uri = "api/v1/dashboard/"
|
||||||
rv = self.client.post(uri, json=dashboard_data)
|
rv = self.post_assert_metric(uri, dashboard_data, "post")
|
||||||
self.assertEqual(rv.status_code, 400)
|
self.assertEqual(rv.status_code, 400)
|
||||||
response = json.loads(rv.data.decode("utf-8"))
|
response = json.loads(rv.data.decode("utf-8"))
|
||||||
expected_response = {
|
expected_response = {
|
||||||
|
@ -604,7 +614,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
dashboard_id = self.insert_dashboard("title1", "slug1", [admin.id]).id
|
dashboard_id = self.insert_dashboard("title1", "slug1", [admin.id]).id
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
uri = f"api/v1/dashboard/{dashboard_id}"
|
uri = f"api/v1/dashboard/{dashboard_id}"
|
||||||
rv = self.client.put(uri, json=self.dashboard_data)
|
rv = self.put_assert_metric(uri, self.dashboard_data, "put")
|
||||||
self.assertEqual(rv.status_code, 200)
|
self.assertEqual(rv.status_code, 200)
|
||||||
model = db.session.query(Dashboard).get(dashboard_id)
|
model = db.session.query(Dashboard).get(dashboard_id)
|
||||||
self.assertEqual(model.dashboard_title, self.dashboard_data["dashboard_title"])
|
self.assertEqual(model.dashboard_title, self.dashboard_data["dashboard_title"])
|
||||||
|
@ -802,7 +812,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
self.login(username="alpha2", password="password")
|
self.login(username="alpha2", password="password")
|
||||||
dashboard_data = {"dashboard_title": "title1_changed", "slug": "slug1 changed"}
|
dashboard_data = {"dashboard_title": "title1_changed", "slug": "slug1 changed"}
|
||||||
uri = f"api/v1/dashboard/{dashboard.id}"
|
uri = f"api/v1/dashboard/{dashboard.id}"
|
||||||
rv = self.client.put(uri, json=dashboard_data)
|
rv = self.put_assert_metric(uri, dashboard_data, "put")
|
||||||
self.assertEqual(rv.status_code, 403)
|
self.assertEqual(rv.status_code, 403)
|
||||||
db.session.delete(dashboard)
|
db.session.delete(dashboard)
|
||||||
db.session.delete(user_alpha1)
|
db.session.delete(user_alpha1)
|
||||||
|
@ -816,8 +826,7 @@ class DashboardApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
argument = [1, 2]
|
argument = [1, 2]
|
||||||
uri = f"api/v1/dashboard/export/?q={prison.dumps(argument)}"
|
uri = f"api/v1/dashboard/export/?q={prison.dumps(argument)}"
|
||||||
|
rv = self.get_assert_metric(uri, "export")
|
||||||
rv = self.client.get(uri)
|
|
||||||
self.assertEqual(rv.status_code, 200)
|
self.assertEqual(rv.status_code, 200)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
rv.headers["Content-Disposition"],
|
rv.headers["Content-Disposition"],
|
||||||
|
|
Loading…
Reference in New Issue