feat(api): bump marshmallow and FAB to version 3 (#9964)

* feat(api): bump marshmallow and FAB to version 3

* revert query context tests changes

* obey mypy

* fix tests

* ignore types that collide with marshmallow

* preparing for RC2

* fix tests for marshmallow 3

* typing fixes for marshmallow

* fix tests and black

* fix tests

* bump to RC3 and lint

* Test RC4

* Final 3.0.0

* Address comments, fix tests, better naming, docs

* fix test

* couple of fixes, addressing comments

* bumping marshmallow
This commit is contained in:
Daniel Vaz Gaspar 2020-07-07 13:26:54 +01:00 committed by GitHub
parent bacf567656
commit 878dbcda3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 179 additions and 125 deletions

View File

@ -7,7 +7,7 @@
aiohttp==3.6.2 # via slackclient
alembic==1.4.2 # via flask-migrate
amqp==2.5.2 # via kombu
apispec[yaml]==1.3.3 # via flask-appbuilder
apispec[yaml]==3.3.1 # via flask-appbuilder
async-timeout==3.0.1 # via aiohttp
attrs==19.3.0 # via aiohttp, jsonschema
babel==2.8.0 # via flask-babel
@ -29,7 +29,7 @@ decorator==4.4.2 # via retry
defusedxml==0.6.0 # via python3-openid
dnspython==1.16.0 # via email-validator
email-validator==1.1.0 # via flask-appbuilder
flask-appbuilder==2.3.4 # via apache-superset (setup.py)
flask-appbuilder==3.0.0 # via apache-superset (setup.py)
flask-babel==1.0.0 # via flask-appbuilder
flask-caching==1.8.0 # via apache-superset (setup.py)
flask-compress==1.5.0 # via apache-superset (setup.py)
@ -58,7 +58,7 @@ markdown==3.2.2 # via apache-superset (setup.py)
markupsafe==1.1.1 # via jinja2, mako, wtforms
marshmallow-enum==1.5.1 # via flask-appbuilder
marshmallow-sqlalchemy==0.23.0 # via flask-appbuilder
marshmallow==2.21.0 # via flask-appbuilder, marshmallow-enum, marshmallow-sqlalchemy
marshmallow==3.6.1 # via flask-appbuilder, marshmallow-enum, marshmallow-sqlalchemy
msgpack==1.0.0 # via apache-superset (setup.py)
multidict==4.7.6 # via aiohttp, yarl
numpy==1.18.4 # via pandas, pyarrow
@ -100,4 +100,4 @@ yarl==1.4.2 # via aiohttp
zipp==3.1.0 # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
# setuptools
# setuptools

View File

@ -78,7 +78,7 @@ setup(
"cryptography>=2.4.2",
"dataclasses<0.7",
"flask>=1.1.0, <2.0.0",
"flask-appbuilder>=2.3.4, <2.4.0",
"flask-appbuilder>=3.0.0, <4.0.0",
"flask-caching",
"flask-compress",
"flask-talisman",

View File

@ -23,6 +23,7 @@ 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
from flask_babel import gettext as _, ngettext
from marshmallow import ValidationError
from werkzeug.wrappers import Response as WerkzeugResponse
from werkzeug.wsgi import FileWrapper
@ -99,6 +100,8 @@ class ChartRestApi(BaseSupersetModelRestApi):
"params",
"cache_timeout",
]
show_select_columns = show_columns + ["table.id"]
list_columns = [
"id",
"slice_name",
@ -121,6 +124,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
"params",
"cache_timeout",
]
order_columns = [
"slice_name",
"viz_type",
@ -215,13 +219,14 @@ class ChartRestApi(BaseSupersetModelRestApi):
"""
if not request.is_json:
return self.response_400(message="Request is not JSON")
item = self.add_model_schema.load(request.json)
# This validates custom Schema with custom validations
if item.errors:
return self.response_400(message=item.errors)
try:
new_model = CreateChartCommand(g.user, item.data).run()
return self.response(201, id=new_model.id, result=item.data)
item = self.add_model_schema.load(request.json)
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateChartCommand(g.user, item).run()
return self.response(201, id=new_model.id, result=item)
except ChartInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
except ChartCreateFailedError as ex:
@ -281,13 +286,14 @@ class ChartRestApi(BaseSupersetModelRestApi):
"""
if not request.is_json:
return self.response_400(message="Request is not JSON")
item = self.edit_model_schema.load(request.json)
# This validates custom Schema with custom validations
if item.errors:
return self.response_400(message=item.errors)
try:
changed_model = UpdateChartCommand(g.user, pk, item.data).run()
return self.response(200, id=changed_model.id, result=item.data)
item = self.edit_model_schema.load(request.json)
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
try:
changed_model = UpdateChartCommand(g.user, pk, item).run()
return self.response(200, id=changed_model.id, result=item)
except ChartNotFoundError:
return self.response_404()
except ChartForbiddenError:
@ -442,8 +448,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/400'
500:
$ref: '#/components/responses/500'
"""
"""
if request.is_json:
json_body = request.json
elif request.form.get("form_data"):
@ -452,13 +457,13 @@ class ChartRestApi(BaseSupersetModelRestApi):
else:
return self.response_400(message="Request is not JSON")
try:
query_context, errors = ChartDataQueryContextSchema().load(json_body)
if errors:
return self.response_400(
message=_("Request is incorrect: %(error)s", error=errors)
)
query_context = ChartDataQueryContextSchema().load(json_body)
except KeyError:
return self.response_400(message="Request is incorrect")
except ValidationError as error:
return self.response_400(
_("Request is incorrect: %(error)s", error=error.messages)
)
try:
query_context.raise_for_access()
except SupersetSecurityException:

View File

@ -58,7 +58,7 @@ class UpdateChartCommand(BaseCommand):
return chart
def validate(self) -> None:
exceptions = list()
exceptions: List[ValidationError] = list()
dashboard_ids = self._properties.get("dashboards", [])
owner_ids: Optional[List[int]] = self._properties.get("owners")

View File

@ -246,8 +246,7 @@ class ChartDataAggregateConfigField(fields.Dict):
class ChartDataPostProcessingOperationOptionsSchema(Schema):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
pass
class ChartDataAggregateOptionsSchema(ChartDataPostProcessingOperationOptionsSchema):
@ -369,7 +368,7 @@ class ChartDataSelectOptionsSchema(ChartDataPostProcessingOperationOptionsSchema
"referenced here.",
example=["country", "gender", "age"],
)
exclude = fields.List(
exclude = fields.List( # type: ignore
fields.String(),
description="Columns to exclude from selection.",
example=["my_temp_column"],
@ -676,6 +675,9 @@ class ChartDataQueryObjectSchema(Schema):
timeseries_limit = fields.Integer(
description="Maximum row count for timeseries queries. Default: `0`",
)
timeseries_limit_metric = fields.Integer(
description="Metric used to limit timeseries queries by.", allow_none=True,
)
row_limit = fields.Integer(
description='Maximum row count. Default: `config["ROW_LIMIT"]`',
validate=[
@ -744,13 +746,13 @@ class ChartDataQueryContextSchema(Schema):
validate=validate.OneOf(choices=("json", "csv")),
)
# pylint: disable=no-self-use
# pylint: disable=no-self-use,unused-argument
@post_load
def make_query_context(self, data: Dict[str, Any]) -> QueryContext:
def make_query_context(self, data: Dict[str, Any], **kwargs: Any) -> QueryContext:
query_context = QueryContext(**data)
return query_context
# pylint: enable=no-self-use
# pylint: enable=no-self-use,unused-argument
class ChartDataResponseResult(Schema):

View File

@ -49,7 +49,7 @@ class CommandInvalidError(CommandException):
def normalized_messages(self) -> Dict[Any, Any]:
errors: Dict[Any, Any] = {}
for exception in self._invalid_exceptions:
errors.update(exception.normalized_messages())
errors.update(exception.normalized_messages()) # type: ignore
return errors
@ -77,11 +77,11 @@ class OwnersNotFoundValidationError(ValidationError):
status = 422
def __init__(self) -> None:
super().__init__(_("Owners are invalid"), field_names=["owners"])
super().__init__([_("Owners are invalid")], field_name="owners")
class DatasourceNotFoundValidationError(ValidationError):
status = 404
def __init__(self) -> None:
super().__init__(_("Datasource does not exist"), field_names=["datasource_id"])
super().__init__([_("Datasource does not exist")], field_name="datasource_id")

View File

@ -21,6 +21,7 @@ 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
from flask_babel import ngettext
from marshmallow import ValidationError
from werkzeug.wrappers import Response as WerkzeugResponse
from werkzeug.wsgi import FileWrapper
@ -118,7 +119,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"owners.first_name",
"owners.last_name",
]
edit_columns = [
add_columns = [
"dashboard_title",
"slug",
"owners",
@ -127,9 +128,10 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"json_metadata",
"published",
]
edit_columns = add_columns
search_columns = ("dashboard_title", "slug", "owners", "published")
search_filters = {"dashboard_title": [DashboardTitleOrSlugFilter]}
add_columns = edit_columns
base_order = ("changed_on", "desc")
add_model_schema = DashboardPostSchema()
@ -197,13 +199,14 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"""
if not request.is_json:
return self.response_400(message="Request is not JSON")
item = self.add_model_schema.load(request.json)
# This validates custom Schema with custom validations
if item.errors:
return self.response_400(message=item.errors)
try:
new_model = CreateDashboardCommand(g.user, item.data).run()
return self.response(201, id=new_model.id, result=item.data)
item = self.add_model_schema.load(request.json)
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateDashboardCommand(g.user, item).run()
return self.response(201, id=new_model.id, result=item)
except DashboardInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
except DashboardCreateFailedError as ex:
@ -263,13 +266,14 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"""
if not request.is_json:
return self.response_400(message="Request is not JSON")
item = self.edit_model_schema.load(request.json)
# This validates custom Schema with custom validations
if item.errors:
return self.response_400(message=item.errors)
try:
changed_model = UpdateDashboardCommand(g.user, pk, item.data).run()
return self.response(200, id=changed_model.id, result=item.data)
item = self.edit_model_schema.load(request.json)
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
try:
changed_model = UpdateDashboardCommand(g.user, pk, item).run()
return self.response(200, id=changed_model.id, result=item)
except DashboardNotFoundError:
return self.response_404()
except DashboardForbiddenError:

View File

@ -50,7 +50,7 @@ class CreateDashboardCommand(BaseCommand):
return dashboard
def validate(self) -> None:
exceptions = list()
exceptions: List[ValidationError] = list()
owner_ids: Optional[List[int]] = self._properties.get("owners")
slug: str = self._properties.get("slug", "")

View File

@ -33,7 +33,7 @@ class DashboardSlugExistsValidationError(ValidationError):
"""
def __init__(self) -> None:
super().__init__(_("Must be unique"), field_names=["slug"])
super().__init__([_("Must be unique")], field_name="slug")
class DashboardInvalidError(CommandInvalidError):

View File

@ -18,7 +18,7 @@ import json
import re
from typing import Any, Dict, Union
from marshmallow import fields, pre_load, Schema
from marshmallow import fields, post_load, Schema
from marshmallow.validate import Length, ValidationError
from superset.exceptions import SupersetException
@ -92,7 +92,7 @@ def validate_json_metadata(value: Union[bytes, bytearray, str]) -> None:
value_obj = json.loads(value)
except json.decoder.JSONDecodeError:
raise ValidationError("JSON not valid")
errors = DashboardJSONMetadataSchema(strict=True).validate(value_obj, partial=False)
errors = DashboardJSONMetadataSchema().validate(value_obj, partial=False)
if errors:
raise ValidationError(errors)
@ -110,12 +110,16 @@ class DashboardJSONMetadataSchema(Schema):
class BaseDashboardSchema(Schema):
@pre_load
def pre_load(self, data: Dict[str, Any]) -> None: # pylint: disable=no-self-use
# pylint: disable=no-self-use,unused-argument
@post_load
def post_load(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
if data.get("slug"):
data["slug"] = data["slug"].strip()
data["slug"] = data["slug"].replace(" ", "-")
data["slug"] = re.sub(r"[^\w\-]+", "", data["slug"])
return data
# pylint: disable=no-self-use,unused-argument
class DashboardPostSchema(BaseDashboardSchema):
@ -133,7 +137,7 @@ class DashboardPostSchema(BaseDashboardSchema):
)
css = fields.String()
json_metadata = fields.String(
description=json_metadata_description, validate=validate_json_metadata
description=json_metadata_description, validate=validate_json_metadata,
)
published = fields.Boolean(description=published_description)

View File

@ -21,6 +21,7 @@ import yaml
from flask import g, request, Response
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import ValidationError
from superset.connectors.sqla.models import SqlaTable
from superset.constants import RouteMethod
@ -61,7 +62,6 @@ class DatasetRestApi(BaseSupersetModelRestApi):
resource_name = "dataset"
allow_browser_login = True
class_permission_name = "TableModelView"
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
RouteMethod.EXPORT,
@ -179,13 +179,14 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"""
if not request.is_json:
return self.response_400(message="Request is not JSON")
item = self.add_model_schema.load(request.json)
# This validates custom Schema with custom validations
if item.errors:
return self.response_400(message=item.errors)
try:
new_model = CreateDatasetCommand(g.user, item.data).run()
return self.response(201, id=new_model.id, result=item.data)
item = self.add_model_schema.load(request.json)
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
try:
new_model = CreateDatasetCommand(g.user, item).run()
return self.response(201, id=new_model.id, result=item)
except DatasetInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
except DatasetCreateFailedError as ex:
@ -245,13 +246,14 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"""
if not request.is_json:
return self.response_400(message="Request is not JSON")
item = self.edit_model_schema.load(request.json)
# This validates custom Schema with custom validations
if item.errors:
return self.response_400(message=item.errors)
try:
changed_model = UpdateDatasetCommand(g.user, pk, item.data).run()
return self.response(200, id=changed_model.id, result=item.data)
item = self.edit_model_schema.load(request.json)
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
try:
changed_model = UpdateDatasetCommand(g.user, pk, item).run()
return self.response(200, id=changed_model.id, result=item)
except DatasetNotFoundError:
return self.response_404()
except DatasetForbiddenError:

View File

@ -67,7 +67,7 @@ class CreateDatasetCommand(BaseCommand):
return dataset
def validate(self) -> None:
exceptions = list()
exceptions: List[ValidationError] = list()
database_id = self._properties["database"]
table_name = self._properties["table_name"]
schema = self._properties.get("schema", "")

View File

@ -34,7 +34,7 @@ class DatabaseNotFoundValidationError(ValidationError):
"""
def __init__(self) -> None:
super().__init__(_("Database does not exist"), field_names=["database"])
super().__init__([_("Database does not exist")], field_name="database")
class DatabaseChangeValidationError(ValidationError):
@ -43,7 +43,7 @@ class DatabaseChangeValidationError(ValidationError):
"""
def __init__(self) -> None:
super().__init__(_("Database not allowed to change"), field_names=["database"])
super().__init__([_("Database not allowed to change")], field_name="database")
class DatasetExistsValidationError(ValidationError):
@ -53,7 +53,7 @@ class DatasetExistsValidationError(ValidationError):
def __init__(self, table_name: str) -> None:
super().__init__(
get_datasource_exist_error_msg(table_name), field_names=["table_name"]
get_datasource_exist_error_msg(table_name), field_name="table_name"
)
@ -63,7 +63,7 @@ class DatasetColumnNotFoundValidationError(ValidationError):
"""
def __init__(self) -> None:
super().__init__(_("One or more columns do not exist"), field_names=["columns"])
super().__init__([_("One or more columns do not exist")], field_name="columns")
class DatasetColumnsDuplicateValidationError(ValidationError):
@ -73,7 +73,7 @@ class DatasetColumnsDuplicateValidationError(ValidationError):
def __init__(self) -> None:
super().__init__(
_("One or more columns are duplicated"), field_names=["columns"]
[_("One or more columns are duplicated")], field_name="columns"
)
@ -83,9 +83,7 @@ class DatasetColumnsExistsValidationError(ValidationError):
"""
def __init__(self) -> None:
super().__init__(
_("One or more columns already exist"), field_names=["columns"]
)
super().__init__([_("One or more columns already exist")], field_name="columns")
class DatasetMetricsNotFoundValidationError(ValidationError):
@ -94,7 +92,7 @@ class DatasetMetricsNotFoundValidationError(ValidationError):
"""
def __init__(self) -> None:
super().__init__(_("One or more metrics do not exist"), field_names=["metrics"])
super().__init__([_("One or more metrics do not exist")], field_name="metrics")
class DatasetMetricsDuplicateValidationError(ValidationError):
@ -104,7 +102,7 @@ class DatasetMetricsDuplicateValidationError(ValidationError):
def __init__(self) -> None:
super().__init__(
_("One or more metrics are duplicated"), field_names=["metrics"]
[_("One or more metrics are duplicated")], field_name="metrics"
)
@ -114,9 +112,7 @@ class DatasetMetricsExistsValidationError(ValidationError):
"""
def __init__(self) -> None:
super().__init__(
_("One or more metrics already exist"), field_names=["metrics"]
)
super().__init__([_("One or more metrics already exist")], field_name="metrics")
class TableNotFoundValidationError(ValidationError):
@ -126,20 +122,22 @@ class TableNotFoundValidationError(ValidationError):
def __init__(self, table_name: str) -> None:
super().__init__(
_(
"Table [%(table_name)s] could not be found, "
"please double check your "
"database connection, schema, and "
"table name",
table_name=table_name,
),
field_names=["table_name"],
[
_(
"Table [%(table_name)s] could not be found, "
"please double check your "
"database connection, schema, and "
"table name",
table_name=table_name,
)
],
field_name="table_name",
)
class OwnersNotFoundValidationError(ValidationError):
def __init__(self) -> None:
super().__init__(_("Owners are invalid"), field_names=["owners"])
super().__init__([_("Owners are invalid")], field_name="owners")
class DatasetNotFoundError(CommandException):

View File

@ -66,7 +66,7 @@ class UpdateDatasetCommand(BaseCommand):
raise DatasetUpdateFailedError()
def validate(self) -> None:
exceptions = list()
exceptions: List[ValidationError] = list()
owner_ids: Optional[List[int]] = self._properties.get("owners")
# Validate/populate model exists
self._model = DatasetDAO.find_by_id(self._model_id)

View File

@ -113,7 +113,7 @@ class BaseSupersetModelRestApi(ModelRestApi):
""" # pylint: disable=pointless-string-statement
allowed_rel_fields: Set[str] = set()
openapi_spec_component_schemas: Tuple[Schema, ...] = tuple()
openapi_spec_component_schemas: Tuple[Type[Schema], ...] = tuple()
"""
Add extra schemas to the OpenAPI component schemas section
""" # pylint: disable=pointless-string-statement

View File

@ -52,11 +52,15 @@ class BaseSupersetSchema(Schema):
self,
data: Union[Mapping[str, Any], Iterable[Mapping[str, Any]]],
many: Optional[bool] = None,
partial: Optional[Union[bool, Sequence[str], Set[str]]] = None,
partial: Union[bool, Sequence[str], Set[str], None] = None,
instance: Optional[Model] = None,
**kwargs: Any,
) -> Any:
self.instance = instance
if many is None:
many = False
if partial is None:
partial = False
return super().load(data, many=many, partial=partial, **kwargs)
@post_load

View File

@ -51,7 +51,10 @@ appbuilder.add_api(Model1Api)
class TestBaseModelRestApi(SupersetTestCase):
def test_default_missing_declaration_get(self):
"""
API: Test default missing declaration on get
API: Test default missing declaration on get
We want to make sure that not declared list_columns will
not render all columns by default but just the model's pk
"""
# Check get list response
self.login(username="admin")
@ -73,6 +76,12 @@ class TestBaseModelRestApi(SupersetTestCase):
self.assertEqual(list(response["result"].keys()), ["id"])
def test_default_missing_declaration_put_spec(self):
"""
API: Test default missing declaration on put openapi spec
We want to make sure that not declared edit_columns will
not render all columns by default but just the model's pk
"""
self.login(username="admin")
uri = "api/v1/_openapi"
rv = self.client.get(uri)
@ -91,6 +100,12 @@ class TestBaseModelRestApi(SupersetTestCase):
)
def test_default_missing_declaration_post(self):
"""
API: Test default missing declaration on post
We want to make sure that not declared add_columns will
not accept all columns by default
"""
dashboard_data = {
"dashboard_title": "title1",
"slug": "slug1",
@ -102,30 +117,41 @@ class TestBaseModelRestApi(SupersetTestCase):
self.login(username="admin")
uri = "api/v1/model1api/"
rv = self.client.post(uri, json=dashboard_data)
# dashboard model accepts all fields are null
self.assertEqual(rv.status_code, 201)
response = json.loads(rv.data.decode("utf-8"))
self.assertEqual(list(response["result"].keys()), ["id"])
model = db.session.query(Dashboard).get(response["id"])
self.assertEqual(model.dashboard_title, None)
self.assertEqual(model.slug, None)
self.assertEqual(model.position_json, None)
self.assertEqual(model.json_metadata, None)
db.session.delete(model)
db.session.commit()
self.assertEqual(rv.status_code, 422)
expected_response = {
"message": {
"css": ["Unknown field."],
"dashboard_title": ["Unknown field."],
"json_metadata": ["Unknown field."],
"position_json": ["Unknown field."],
"published": ["Unknown field."],
"slug": ["Unknown field."],
}
}
self.assertEqual(response, expected_response)
def test_default_missing_declaration_put(self):
"""
API: Test default missing declaration on put
We want to make sure that not declared edit_columns will
not accept all columns by default
"""
dashboard = db.session.query(Dashboard).first()
dashboard_data = {"dashboard_title": "CHANGED", "slug": "CHANGED"}
self.login(username="admin")
uri = f"api/v1/model1api/{dashboard.id}"
rv = self.client.put(uri, json=dashboard_data)
# dashboard model accepts all fields are null
self.assertEqual(rv.status_code, 200)
response = json.loads(rv.data.decode("utf-8"))
changed_dashboard = db.session.query(Dashboard).get(dashboard.id)
self.assertNotEqual(changed_dashboard.dashboard_title, "CHANGED")
self.assertNotEqual(changed_dashboard.slug, "CHANGED")
self.assertEqual(rv.status_code, 422)
expected_response = {
"message": {
"dashboard_title": ["Unknown field."],
"slug": ["Unknown field."],
}
}
self.assertEqual(response, expected_response)
class ApiOwnersTestCaseMixin:

View File

@ -338,7 +338,8 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin):
self.assertEqual(rv.status_code, 400)
response = json.loads(rv.data.decode("utf-8"))
self.assertEqual(
response, {"message": {"datasource_type": ["Not a valid choice."]}}
response,
{"message": {"datasource_type": ["Must be one of: druid, table, view."]}},
)
chart_data = {
"slice_name": "title1",
@ -444,7 +445,8 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin):
self.assertEqual(rv.status_code, 400)
response = json.loads(rv.data.decode("utf-8"))
self.assertEqual(
response, {"message": {"datasource_type": ["Not a valid choice."]}}
response,
{"message": {"datasource_type": ["Must be one of: druid, table, view."]}},
)
chart_data = {"datasource_id": 0, "datasource_type": "table"}
uri = f"api/v1/chart/{chart.id}"

View File

@ -18,6 +18,7 @@
"""Unit tests for Superset"""
from typing import Any, Dict, Tuple
from marshmallow import ValidationError
from tests.test_app import app
from superset.charts.schemas import ChartDataQueryContextSchema
from superset.common.query_context import QueryContext
@ -39,8 +40,7 @@ class TestSchema(SupersetTestCase):
# Use defaults
payload["queries"][0].pop("row_limit", None)
payload["queries"][0].pop("row_offset", None)
query_context, errors = load_query_context(payload)
self.assertEqual(errors, {})
query_context = load_query_context(payload)
query_object = query_context.queries[0]
self.assertEqual(query_object.row_limit, app.config["ROW_LIMIT"])
self.assertEqual(query_object.row_offset, 0)
@ -48,8 +48,7 @@ class TestSchema(SupersetTestCase):
# Valid limit and offset
payload["queries"][0]["row_limit"] = 100
payload["queries"][0]["row_offset"] = 200
query_context, errors = ChartDataQueryContextSchema().load(payload)
self.assertEqual(errors, {})
query_context = ChartDataQueryContextSchema().load(payload)
query_object = query_context.queries[0]
self.assertEqual(query_object.row_limit, 100)
self.assertEqual(query_object.row_offset, 200)
@ -57,9 +56,10 @@ class TestSchema(SupersetTestCase):
# too low limit and offset
payload["queries"][0]["row_limit"] = 0
payload["queries"][0]["row_offset"] = -1
query_context, errors = ChartDataQueryContextSchema().load(payload)
self.assertIn("row_limit", errors["queries"][0])
self.assertIn("row_offset", errors["queries"][0])
with self.assertRaises(ValidationError) as context:
_ = ChartDataQueryContextSchema().load(payload)
self.assertIn("row_limit", context.exception.messages["queries"][0])
self.assertIn("row_offset", context.exception.messages["queries"][0])
def test_query_context_null_timegrain(self):
self.login(username="admin")
@ -68,5 +68,4 @@ class TestSchema(SupersetTestCase):
payload = get_query_context(table.name, table.id, table.type)
payload["queries"][0]["extras"]["time_grain_sqla"] = None
_, errors = ChartDataQueryContextSchema().load(payload)
self.assertEqual(errors, {})
_ = ChartDataQueryContextSchema().load(payload)

View File

@ -40,7 +40,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin):
"slug": "slug1_changed",
"position_json": '{"b": "B"}',
"css": "css_changed",
"json_metadata": '{"a": "A"}',
"json_metadata": '{"refresh_frequency": 30}',
"published": False,
}
@ -473,7 +473,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin):
"owners": [admin_id],
"position_json": '{"a": "A"}',
"css": "css",
"json_metadata": '{"b": "B"}',
"json_metadata": '{"refresh_frequency": 30}',
"published": True,
}
self.login(username="admin")

View File

@ -371,6 +371,11 @@ class TestDatasetApi(SupersetTestCase):
self.login(username="admin")
rv = self.get_assert_metric(uri, "get")
data = json.loads(rv.data.decode("utf-8"))
for column in data["result"]["columns"]:
column.pop("changed_on", None)
column.pop("created_on", None)
data["result"]["columns"].append(new_column_data)
rv = self.client.put(uri, json={"columns": data["result"]["columns"]})
@ -404,6 +409,10 @@ class TestDatasetApi(SupersetTestCase):
# Get current cols and alter one
rv = self.get_assert_metric(uri, "get")
resp_columns = json.loads(rv.data.decode("utf-8"))["result"]["columns"]
for column in resp_columns:
column.pop("changed_on", None)
column.pop("created_on", None)
resp_columns[0]["groupby"] = False
resp_columns[0]["filterable"] = False
v = self.client.put(uri, json={"columns": resp_columns})

View File

@ -39,8 +39,7 @@ class TestQueryContext(SupersetTestCase):
payload = get_query_context(
table.name, table.id, table.type, add_postprocessing_operations=True
)
query_context, errors = ChartDataQueryContextSchema().load(payload)
self.assertDictEqual(errors, {})
query_context = ChartDataQueryContextSchema().load(payload)
self.assertEqual(len(query_context.queries), len(payload["queries"]))
for query_idx, query in enumerate(query_context.queries):
payload_query = payload["queries"][query_idx]