mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
docs(api): improve openapi documentation for dash, charts and queries (#9724)
This commit is contained in:
committed by
GitHub
parent
911f117673
commit
0d85d25314
@@ -18,7 +18,6 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import simplejson
|
||||
from apispec import APISpec
|
||||
from flask import g, make_response, redirect, request, Response, url_for
|
||||
from flask_appbuilder.api import expose, protect, rison, safe
|
||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
@@ -48,6 +47,7 @@ from superset.charts.schemas import (
|
||||
ChartPostSchema,
|
||||
ChartPutSchema,
|
||||
get_delete_ids_schema,
|
||||
openapi_spec_methods_override,
|
||||
thumbnail_query_schema,
|
||||
)
|
||||
from superset.constants import RouteMethod
|
||||
@@ -145,14 +145,21 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
edit_model_schema = ChartPutSchema()
|
||||
|
||||
openapi_spec_tag = "Charts"
|
||||
""" Override the name set for this collection of endpoints """
|
||||
openapi_spec_component_schemas = CHART_DATA_SCHEMAS
|
||||
""" Add extra schemas to the OpenAPI components schema section """
|
||||
openapi_spec_methods = openapi_spec_methods_override
|
||||
""" Overrides GET methods OpenApi descriptions """
|
||||
|
||||
order_rel_fields = {
|
||||
"slices": ("slice_name", "asc"),
|
||||
"owners": ("first_name", "asc"),
|
||||
}
|
||||
|
||||
related_field_filters = {
|
||||
"owners": RelatedFieldFilter("first_name", FilterRelatedOwners)
|
||||
}
|
||||
|
||||
allowed_rel_fields = {"owners"}
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -169,7 +176,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
post:
|
||||
description: >-
|
||||
Create a new Chart
|
||||
Create a new Chart.
|
||||
requestBody:
|
||||
description: Chart schema
|
||||
required: true
|
||||
@@ -224,7 +231,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
put:
|
||||
description: >-
|
||||
Changes a Chart
|
||||
Changes a Chart.
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
@@ -290,7 +297,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
delete:
|
||||
description: >-
|
||||
Deletes a Chart
|
||||
Deletes a Chart.
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
@@ -340,7 +347,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
delete:
|
||||
description: >-
|
||||
Deletes multiple Charts in a bulk operation
|
||||
Deletes multiple Charts in a bulk operation.
|
||||
parameters:
|
||||
- in: query
|
||||
name: q
|
||||
@@ -457,7 +464,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
"""Get Chart thumbnail
|
||||
---
|
||||
get:
|
||||
description: Compute or get already computed chart thumbnail from cache
|
||||
description: Compute or get already computed chart thumbnail from cache.
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
@@ -509,13 +516,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
|
||||
)
|
||||
|
||||
def add_apispec_components(self, api_spec: APISpec) -> None:
|
||||
for chart_type in CHART_DATA_SCHEMAS:
|
||||
api_spec.components.schema(
|
||||
chart_type.__name__, schema=chart_type,
|
||||
)
|
||||
super().add_apispec_components(api_spec)
|
||||
|
||||
@expose("/datasources", methods=["GET"])
|
||||
@protect()
|
||||
@safe
|
||||
@@ -523,6 +523,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
"""Get available datasources
|
||||
---
|
||||
get:
|
||||
description: Get available datasources.
|
||||
responses:
|
||||
200:
|
||||
description: charts unique datasource data
|
||||
|
||||
@@ -23,12 +23,69 @@ from superset.common.query_context import QueryContext
|
||||
from superset.exceptions import SupersetException
|
||||
from superset.utils import core as utils
|
||||
|
||||
#
|
||||
# RISON/JSON schemas for query parameters
|
||||
#
|
||||
get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
|
||||
thumbnail_query_schema = {
|
||||
"type": "object",
|
||||
"properties": {"force": {"type": "boolean"}},
|
||||
}
|
||||
|
||||
#
|
||||
# Column schema descriptions
|
||||
#
|
||||
slice_name_description = "The name of the chart."
|
||||
description_description = "A description of the chart propose."
|
||||
viz_type_description = "The type of chart visualization used."
|
||||
owners_description = (
|
||||
"Owner are users ids allowed to delete or change this chart. "
|
||||
"If left empty you will be one of the owners of the chart."
|
||||
)
|
||||
params_description = (
|
||||
"Parameters are generated dynamically when clicking the save "
|
||||
"or overwrite button in the explore view. "
|
||||
"This JSON object for power users who may want to alter specific parameters."
|
||||
)
|
||||
cache_timeout_description = (
|
||||
"Duration (in seconds) of the caching timeout "
|
||||
"for this chart. Note this defaults to the datasource/table"
|
||||
" timeout if undefined."
|
||||
)
|
||||
datasource_id_description = (
|
||||
"The id of the dataset/datasource this new chart will use. "
|
||||
"A complete datasource identification needs `datasouce_id` "
|
||||
"and `datasource_type`."
|
||||
)
|
||||
datasource_type_description = (
|
||||
"The type of dataset/datasource identified on `datasource_id`."
|
||||
)
|
||||
datasource_name_description = "The datasource name."
|
||||
dashboards_description = "A list of dashboards to include this new chart to."
|
||||
|
||||
#
|
||||
# OpenAPI method specification overrides
|
||||
#
|
||||
openapi_spec_methods_override = {
|
||||
"get": {"get": {"description": "Get a chart detail information."}},
|
||||
"get_list": {
|
||||
"get": {
|
||||
"description": "Get a list of charts, use Rison or JSON query "
|
||||
"parameters for filtering, sorting, pagination and "
|
||||
" for selecting specific columns and metadata.",
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"get": {
|
||||
"description": "Several metadata information about chart API endpoints.",
|
||||
}
|
||||
},
|
||||
"related": {
|
||||
"get": {"description": "Get a list of all possible owners for a chart."}
|
||||
},
|
||||
}
|
||||
""" Overrides GET methods OpenApi descriptions """
|
||||
|
||||
|
||||
def validate_json(value: Union[bytes, bytearray, str]) -> None:
|
||||
try:
|
||||
@@ -38,35 +95,74 @@ def validate_json(value: Union[bytes, bytearray, str]) -> None:
|
||||
|
||||
|
||||
class ChartPostSchema(Schema):
|
||||
slice_name = fields.String(required=True, validate=Length(1, 250))
|
||||
description = fields.String(allow_none=True)
|
||||
viz_type = fields.String(allow_none=True, validate=Length(0, 250))
|
||||
owners = fields.List(fields.Integer())
|
||||
params = fields.String(allow_none=True, validate=validate_json)
|
||||
cache_timeout = fields.Integer(allow_none=True)
|
||||
datasource_id = fields.Integer(required=True)
|
||||
datasource_type = fields.String(required=True)
|
||||
datasource_name = fields.String(allow_none=True)
|
||||
dashboards = fields.List(fields.Integer())
|
||||
"""
|
||||
Schema to add a new chart.
|
||||
"""
|
||||
|
||||
slice_name = fields.String(
|
||||
description=slice_name_description, required=True, validate=Length(1, 250)
|
||||
)
|
||||
description = fields.String(description=description_description, allow_none=True)
|
||||
viz_type = fields.String(
|
||||
description=viz_type_description,
|
||||
validate=Length(0, 250),
|
||||
example=["bar", "line_multi", "area", "table"],
|
||||
)
|
||||
owners = fields.List(fields.Integer(description=owners_description))
|
||||
params = fields.String(
|
||||
description=params_description, allow_none=True, validate=validate_json
|
||||
)
|
||||
cache_timeout = fields.Integer(
|
||||
description=cache_timeout_description, allow_none=True
|
||||
)
|
||||
datasource_id = fields.Integer(description=datasource_id_description, required=True)
|
||||
datasource_type = fields.String(
|
||||
description=datasource_type_description,
|
||||
validate=validate.OneOf(choices=("druid", "table", "view")),
|
||||
required=True,
|
||||
)
|
||||
datasource_name = fields.String(
|
||||
description=datasource_name_description, allow_none=True
|
||||
)
|
||||
dashboards = fields.List(fields.Integer(description=dashboards_description))
|
||||
|
||||
|
||||
class ChartPutSchema(Schema):
|
||||
slice_name = fields.String(allow_none=True, validate=Length(0, 250))
|
||||
description = fields.String(allow_none=True)
|
||||
viz_type = fields.String(allow_none=True, validate=Length(0, 250))
|
||||
owners = fields.List(fields.Integer())
|
||||
params = fields.String(allow_none=True)
|
||||
cache_timeout = fields.Integer(allow_none=True)
|
||||
datasource_id = fields.Integer(allow_none=True)
|
||||
datasource_type = fields.String(allow_none=True)
|
||||
dashboards = fields.List(fields.Integer())
|
||||
"""
|
||||
Schema to update or patch a chart
|
||||
"""
|
||||
|
||||
slice_name = fields.String(
|
||||
description=slice_name_description, allow_none=True, validate=Length(0, 250)
|
||||
)
|
||||
description = fields.String(description=description_description, allow_none=True)
|
||||
viz_type = fields.String(
|
||||
description=viz_type_description,
|
||||
allow_none=True,
|
||||
validate=Length(0, 250),
|
||||
example=["bar", "line_multi", "area", "table"],
|
||||
)
|
||||
owners = fields.List(fields.Integer(description=owners_description))
|
||||
params = fields.String(description=params_description, allow_none=True)
|
||||
cache_timeout = fields.Integer(
|
||||
description=cache_timeout_description, allow_none=True
|
||||
)
|
||||
datasource_id = fields.Integer(
|
||||
description=datasource_id_description, allow_none=True
|
||||
)
|
||||
datasource_type = fields.String(
|
||||
description=datasource_type_description,
|
||||
validate=validate.OneOf(choices=("druid", "table", "view")),
|
||||
allow_none=True,
|
||||
)
|
||||
dashboards = fields.List(fields.Integer(description=dashboards_description))
|
||||
|
||||
|
||||
class ChartDataColumnSchema(Schema):
|
||||
column_name = fields.String(
|
||||
description="The name of the target column", example="mycol",
|
||||
)
|
||||
type = fields.String(description="Type of target column", example="BIGINT",)
|
||||
type = fields.String(description="Type of target column", example="BIGINT")
|
||||
|
||||
|
||||
class ChartDataAdhocMetricSchema(Schema):
|
||||
|
||||
@@ -45,6 +45,7 @@ from superset.dashboards.schemas import (
|
||||
DashboardPutSchema,
|
||||
get_delete_ids_schema,
|
||||
get_export_ids_schema,
|
||||
openapi_spec_methods_override,
|
||||
thumbnail_query_schema,
|
||||
)
|
||||
from superset.models.dashboard import Dashboard
|
||||
@@ -145,6 +146,9 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
}
|
||||
allowed_rel_fields = {"owners"}
|
||||
|
||||
openapi_spec_methods = openapi_spec_methods_override
|
||||
""" Overrides GET methods OpenApi descriptions """
|
||||
|
||||
def __init__(self) -> None:
|
||||
if is_feature_enabled("THUMBNAILS"):
|
||||
self.include_route_methods = self.include_route_methods | {"thumbnail"}
|
||||
@@ -159,7 +163,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
post:
|
||||
description: >-
|
||||
Create a new Dashboard
|
||||
Create a new Dashboard.
|
||||
requestBody:
|
||||
description: Dashboard schema
|
||||
required: true
|
||||
@@ -216,7 +220,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
put:
|
||||
description: >-
|
||||
Changes a Dashboard
|
||||
Changes a Dashboard.
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
@@ -282,7 +286,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
delete:
|
||||
description: >-
|
||||
Deletes a Dashboard
|
||||
Deletes a Dashboard.
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
@@ -332,7 +336,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
delete:
|
||||
description: >-
|
||||
Deletes multiple Dashboards in a bulk operation
|
||||
Deletes multiple Dashboards in a bulk operation.
|
||||
parameters:
|
||||
- in: query
|
||||
name: q
|
||||
@@ -391,7 +395,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
get:
|
||||
description: >-
|
||||
Exports multiple Dashboards and downloads them as YAML files
|
||||
Exports multiple Dashboards and downloads them as YAML files.
|
||||
parameters:
|
||||
- in: query
|
||||
name: q
|
||||
@@ -444,7 +448,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
---
|
||||
get:
|
||||
description: >-
|
||||
Compute async or get already computed dashboard thumbnail from cache
|
||||
Compute async or get already computed dashboard thumbnail from cache.
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
|
||||
@@ -31,6 +31,52 @@ thumbnail_query_schema = {
|
||||
"properties": {"force": {"type": "boolean"}},
|
||||
}
|
||||
|
||||
dashboard_title_description = "A title for the dashboard."
|
||||
slug_description = "Unique identifying part for the web address of the dashboard."
|
||||
owners_description = (
|
||||
"Owner are users ids allowed to delete or change this dashboard. "
|
||||
"If left empty you will be one of the owners of the dashboard."
|
||||
)
|
||||
position_json_description = (
|
||||
"This json object describes the positioning of the widgets "
|
||||
"in the dashboard. It is dynamically generated when "
|
||||
"adjusting the widgets size and positions by using "
|
||||
"drag & drop in the dashboard view"
|
||||
)
|
||||
css_description = "Override CSS for the dashboard."
|
||||
json_metadata_description = (
|
||||
"This JSON object is generated dynamically when clicking "
|
||||
"the save or overwrite button in the dashboard view. "
|
||||
"It is exposed here for reference and for power users who may want to alter "
|
||||
" specific parameters."
|
||||
)
|
||||
published_description = (
|
||||
"Determines whether or not this dashboard is visible in "
|
||||
"the list of all dashboards."
|
||||
)
|
||||
|
||||
|
||||
openapi_spec_methods_override = {
|
||||
"get": {"get": {"description": "Get a dashboard detail information."}},
|
||||
"get_list": {
|
||||
"get": {
|
||||
"description": "Get a list of dashboards, use Rison or JSON query "
|
||||
"parameters for filtering, sorting, pagination and "
|
||||
" for selecting specific columns and metadata.",
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"get": {
|
||||
"description": "Several metadata information about dashboard API "
|
||||
"endpoints.",
|
||||
}
|
||||
},
|
||||
"related": {
|
||||
"get": {"description": "Get a list of all possible owners for a dashboard."}
|
||||
},
|
||||
}
|
||||
""" Overrides GET methods OpenApi descriptions """
|
||||
|
||||
|
||||
def validate_json(value: Union[bytes, bytearray, str]) -> None:
|
||||
try:
|
||||
@@ -73,20 +119,44 @@ class BaseDashboardSchema(Schema):
|
||||
|
||||
|
||||
class DashboardPostSchema(BaseDashboardSchema):
|
||||
dashboard_title = fields.String(allow_none=True, validate=Length(0, 500))
|
||||
slug = fields.String(allow_none=True, validate=[Length(1, 255)])
|
||||
owners = fields.List(fields.Integer())
|
||||
position_json = fields.String(validate=validate_json)
|
||||
dashboard_title = fields.String(
|
||||
description=dashboard_title_description,
|
||||
allow_none=True,
|
||||
validate=Length(0, 500),
|
||||
)
|
||||
slug = fields.String(
|
||||
description=slug_description, allow_none=True, validate=[Length(1, 255)]
|
||||
)
|
||||
owners = fields.List(fields.Integer(description=owners_description))
|
||||
position_json = fields.String(
|
||||
description=position_json_description, validate=validate_json
|
||||
)
|
||||
css = fields.String()
|
||||
json_metadata = fields.String(validate=validate_json_metadata)
|
||||
published = fields.Boolean()
|
||||
json_metadata = fields.String(
|
||||
description=json_metadata_description, validate=validate_json_metadata
|
||||
)
|
||||
published = fields.Boolean(description=published_description)
|
||||
|
||||
|
||||
class DashboardPutSchema(BaseDashboardSchema):
|
||||
dashboard_title = fields.String(allow_none=True, validate=Length(0, 500))
|
||||
slug = fields.String(allow_none=True, validate=Length(0, 255))
|
||||
owners = fields.List(fields.Integer(allow_none=True))
|
||||
position_json = fields.String(allow_none=True, validate=validate_json)
|
||||
css = fields.String(allow_none=True)
|
||||
json_metadata = fields.String(allow_none=True, validate=validate_json_metadata)
|
||||
published = fields.Boolean(allow_none=True)
|
||||
dashboard_title = fields.String(
|
||||
description=dashboard_title_description,
|
||||
allow_none=True,
|
||||
validate=Length(0, 500),
|
||||
)
|
||||
slug = fields.String(
|
||||
description=slug_description, allow_none=True, validate=Length(0, 255)
|
||||
)
|
||||
owners = fields.List(
|
||||
fields.Integer(description=owners_description, allow_none=True)
|
||||
)
|
||||
position_json = fields.String(
|
||||
description=position_json_description, allow_none=True, validate=validate_json
|
||||
)
|
||||
css = fields.String(description=css_description, allow_none=True)
|
||||
json_metadata = fields.String(
|
||||
description=json_metadata_description,
|
||||
allow_none=True,
|
||||
validate=validate_json_metadata,
|
||||
)
|
||||
published = fields.Boolean(description=published_description, allow_none=True)
|
||||
|
||||
@@ -21,6 +21,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from superset.constants import RouteMethod
|
||||
from superset.models.sql_lab import Query
|
||||
from superset.queries.filters import QueryFilter
|
||||
from superset.queries.schemas import openapi_spec_methods_override
|
||||
from superset.views.base_api import BaseSupersetModelRestApi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -70,3 +71,4 @@ class QueryRestApi(BaseSupersetModelRestApi):
|
||||
base_order = ("changed_on", "desc")
|
||||
|
||||
openapi_spec_tag = "Queries"
|
||||
openapi_spec_methods = openapi_spec_methods_override
|
||||
|
||||
28
superset/queries/schemas.py
Normal file
28
superset/queries/schemas.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
|
||||
openapi_spec_methods_override = {
|
||||
"get": {"get": {"description": "Get query detail information."}},
|
||||
"get_list": {
|
||||
"get": {
|
||||
"description": "Get a list of queries, use Rison or JSON query "
|
||||
"parameters for filtering, sorting, pagination and "
|
||||
" for selecting specific columns and metadata.",
|
||||
}
|
||||
},
|
||||
}
|
||||
""" Overrides GET methods OpenApi descriptions """
|
||||
@@ -18,11 +18,13 @@ import functools
|
||||
import logging
|
||||
from typing import Any, cast, Dict, Optional, Set, Tuple, Type, Union
|
||||
|
||||
from apispec import APISpec
|
||||
from flask import Response
|
||||
from flask_appbuilder import ModelRestApi
|
||||
from flask_appbuilder.api import expose, protect, rison, safe
|
||||
from flask_appbuilder.models.filters import BaseFilter, Filters
|
||||
from flask_appbuilder.models.sqla.filters import FilterStartsWith
|
||||
from marshmallow import Schema
|
||||
|
||||
from superset.stats_logger import BaseStatsLogger
|
||||
from superset.utils.core import time_function
|
||||
@@ -109,10 +111,23 @@ class BaseSupersetModelRestApi(ModelRestApi):
|
||||
""" # pylint: disable=pointless-string-statement
|
||||
allowed_rel_fields: Set[str] = set()
|
||||
|
||||
openapi_spec_component_schemas: Tuple[Schema, ...] = tuple()
|
||||
"""
|
||||
Add extra schemas to the OpenAPI component schemas section
|
||||
""" # pylint: disable=pointless-string-statement
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.stats_logger = BaseStatsLogger()
|
||||
|
||||
def add_apispec_components(self, api_spec: APISpec) -> None:
|
||||
|
||||
for schema in self.openapi_spec_component_schemas:
|
||||
api_spec.components.schema(
|
||||
schema.__name__, schema=schema,
|
||||
)
|
||||
super().add_apispec_components(api_spec)
|
||||
|
||||
def create_blueprint(self, appbuilder, *args, **kwargs):
|
||||
self.stats_logger = self.appbuilder.get_app.config["STATS_LOGGER"]
|
||||
return super().create_blueprint(appbuilder, *args, **kwargs)
|
||||
|
||||
@@ -333,10 +333,10 @@ class ChartApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
}
|
||||
uri = f"api/v1/chart/"
|
||||
rv = self.post_assert_metric(uri, chart_data, "post")
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
self.assertEqual(rv.status_code, 400)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(
|
||||
response, {"message": {"datasource_id": ["Datasource does not exist"]}}
|
||||
response, {"message": {"datasource_type": ["Not a valid choice."]}}
|
||||
)
|
||||
chart_data = {
|
||||
"slice_name": "title1",
|
||||
@@ -439,10 +439,10 @@ class ChartApiTests(SupersetTestCase, ApiOwnersTestCaseMixin):
|
||||
chart_data = {"datasource_id": 1, "datasource_type": "unknown"}
|
||||
uri = f"api/v1/chart/{chart.id}"
|
||||
rv = self.put_assert_metric(uri, chart_data, "put")
|
||||
self.assertEqual(rv.status_code, 422)
|
||||
self.assertEqual(rv.status_code, 400)
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(
|
||||
response, {"message": {"datasource_id": ["Datasource does not exist"]}}
|
||||
response, {"message": {"datasource_type": ["Not a valid choice."]}}
|
||||
)
|
||||
chart_data = {"datasource_id": 0, "datasource_type": "table"}
|
||||
uri = f"api/v1/chart/{chart.id}"
|
||||
|
||||
Reference in New Issue
Block a user