Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[query] Migrate api v1 query to new location #9479

Merged
merged 10 commits into from
Apr 9, 2020
117 changes: 116 additions & 1 deletion superset/charts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import logging
from typing import Any

from flask import g, request, Response
import simplejson
from flask import g, make_response, request, Response
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
Expand All @@ -41,8 +42,12 @@
ChartPutSchema,
get_delete_ids_schema,
)
from superset.common.query_context import QueryContext
from superset.constants import RouteMethod
from superset.exceptions import SupersetSecurityException
from superset.extensions import event_logger, security_manager
from superset.models.slice import Slice
from superset.utils.core import json_int_dttm_ser
from superset.views.base_api import BaseSupersetModelRestApi, RelatedFieldFilter
from superset.views.filters import FilterRelatedOwners

Expand All @@ -59,6 +64,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
RouteMethod.EXPORT,
RouteMethod.RELATED,
"bulk_delete", # not using RouteMethod since locally defined
"data",
}
class_permission_name = "SliceModelView"
show_columns = [
Expand Down Expand Up @@ -348,3 +354,112 @@ def bulk_delete(
return self.response_403()
except ChartBulkDeleteFailedError as e:
return self.response_422(message=str(e))

@expose("/data", methods=["POST"])
@event_logger.log_this
@protect()
@safe
def data(self) -> Response:
"""
Takes a query context constructed in the client and returns payload
data response for the given query.
---
post:
description: >-
Takes a query context constructed in the client and returns payload data
response for the given query.
requestBody:
description: Query context schema
required: true
content:
application/json:
schema:
type: object
properties:
datasource:
type: object
description: The datasource where the query will run
properties:
id:
type: integer
type:
type: string
queries:
type: array
items:
type: object
properties:
granularity:
type: string
groupby:
type: array
items:
type: string
metrics:
type: array
items:
type: object
filters:
type: array
items:
type: string
row_limit:
type: integer
responses:
200:
description: Query result
content:
application/json:
schema:
type: array
items:
type: object
properties:
cache_key:
type: string
cached_dttm:
type: string
cache_timeout:
type: integer
error:
type: string
is_cached:
type: boolean
query:
type: string
status:
type: string
stacktrace:
type: string
rowcount:
type: integer
data:
type: array
items:
type: object
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
404:
$ref: '#/components/responses/404'
500:
$ref: '#/components/responses/500'
"""
if not request.is_json:
return self.response_400(message="Request is not JSON")
try:
query_context = QueryContext(**request.json)
except KeyError:
return self.response_400(message="Request is incorrect")
try:
security_manager.assert_query_context_permission(query_context)
except SupersetSecurityException:
return self.response_401()
payload_json = query_context.get_payload()
response_data = simplejson.dumps(
payload_json, default=json_int_dttm_ser, ignore_nan=True
)
resp = make_response(response_data, 200)
resp.headers["Content-Type"] = "application/json; charset=utf-8"
return resp
1 change: 1 addition & 0 deletions superset/views/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class BaseSupersetModelRestApi(ModelRestApi):
"info": "list",
"related": "list",
"refresh": "edit",
"data": "list",
}

order_rel_fields: Dict[str, Tuple[str, str]] = {}
Expand Down
40 changes: 39 additions & 1 deletion tests/charts/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# under the License.
"""Unit tests for Superset"""
import json
from typing import List, Optional
from typing import Any, Dict, List, Optional

import prison
from sqlalchemy.sql import func
Expand Down Expand Up @@ -69,6 +69,22 @@ def insert_chart(
db.session.commit()
return slice

def _get_query_context(self) -> Dict[str, Any]:
self.login(username="admin")
slc = self.get_slice("Girl Name Cloud", db.session)
return {
"datasource": {"id": slc.datasource_id, "type": slc.datasource_type},
"queries": [
{
"granularity": "ds",
"groupby": ["name"],
"metrics": [{"label": "sum__num"}],
"filters": [],
"row_limit": 100,
}
],
}

def test_delete_chart(self):
"""
Chart API: Test delete
Expand Down Expand Up @@ -580,3 +596,25 @@ def test_get_charts_no_data_access(self):
self.assertEqual(rv.status_code, 200)
data = json.loads(rv.data.decode("utf-8"))
self.assertEqual(data["count"], 0)

def test_chart_data(self):
"""
Query API: Test chart data query
"""
self.login(username="admin")
query_context = self._get_query_context()
uri = "api/v1/chart/data"
rv = self.client.post(uri, json=query_context)
self.assertEqual(rv.status_code, 200)
data = json.loads(rv.data.decode("utf-8"))
self.assertEqual(data[0]["rowcount"], 100)

def test_query_exec_not_allowed(self):
"""
Query API: Test chart data query not allowed
"""
self.login(username="gamma")
query_context = self._get_query_context()
uri = "api/v1/chart/data"
rv = self.client.post(uri, json=query_context)
self.assertEqual(rv.status_code, 401)
2 changes: 1 addition & 1 deletion tests/queries/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
# isort:skip_file
"""Unit tests for Superset"""
import json
import uuid
import random
import string
from typing import Dict, Any

import prison
from sqlalchemy.sql import func
Expand Down