You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by dp...@apache.org on 2019/12/20 10:17:13 UTC
[incubator-superset] branch master updated: [dashboard] New,
get releated owners and slices (#8872)
This is an automated email from the ASF dual-hosted git repository.
dpgaspar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 3d9181d [dashboard] New, get releated owners and slices (#8872)
3d9181d is described below
commit 3d9181d27047f919109701ef5007654c494614c4
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Fri Dec 20 10:16:57 2019 +0000
[dashboard] New, get releated owners and slices (#8872)
---
superset/models/dashboard.py | 19 ++++++
superset/views/base.py | 130 +++++++++++++++++++++++++++++++++++++++-
superset/views/dashboard/api.py | 24 +++++++-
tests/dashboard_api_tests.py | 64 ++++++++++++++++++--
4 files changed, 228 insertions(+), 9 deletions(-)
diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py
index 89d0ef6..74094d6 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -180,6 +180,25 @@ class Dashboard( # pylint: disable=too-many-instance-attributes
return Markup(f'<a href="{self.url}">{title}</a>')
@property
+ def changed_by_name(self):
+ if not self.changed_by:
+ return ""
+ return str(self.changed_by)
+
+ @property
+ def changed_by_url(self):
+ if not self.changed_by:
+ return ""
+ return f"/superset/profile/{self.changed_by.username}"
+
+ @property
+ def owners_json(self) -> List[Dict[str, Any]]:
+ owners = []
+ for owner in self.owners:
+ owners.append({"name": owner.name})
+ return owners
+
+ @property
def data(self) -> Dict[str, Any]:
positions = self.position_json
if positions:
diff --git a/superset/views/base.py b/superset/views/base.py
index 54ed081..15f711e 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -18,14 +18,16 @@ import functools
import logging
import traceback
from datetime import datetime
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Optional, Tuple
import simplejson as json
import yaml
from flask import abort, flash, g, get_flashed_messages, redirect, Response, session
-from flask_appbuilder import BaseView, Model, ModelView
+from flask_appbuilder import BaseView, Model, ModelRestApi, ModelView
from flask_appbuilder.actions import action
+from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.forms import DynamicForm
+from flask_appbuilder.models.filters import Filters
from flask_appbuilder.models.sqla.filters import BaseFilter
from flask_appbuilder.widgets import ListWidget
from flask_babel import get_locale, gettext as __, lazy_gettext as _
@@ -373,6 +375,130 @@ class BaseSupersetSchema(Schema):
return super().load(data, many=many, partial=partial, **kwargs)
+get_related_schema = {
+ "type": "object",
+ "properties": {
+ "page_size": {"type": "integer"},
+ "page": {"type": "integer"},
+ "filter": {"type": "string"},
+ },
+}
+
+
+class BaseSupersetModelRestApi(ModelRestApi):
+ """
+ Extends FAB's ModelResApi to implement specific superset generic functionality
+ """
+
+ order_rel_fields: Dict[str, Tuple[str, str]] = {}
+ """
+ Impose ordering on related fields query::
+
+ order_rel_fields = {
+ "<RELATED_FIELD>": ("<RELATED_FIELD_FIELD>", "<asc|desc>"),
+ ...
+ }
+ """ # pylint: disable=pointless-string-statement
+ filter_rel_fields_field: Dict[str, str] = {}
+ """
+ Declare the related field field for filtering::
+
+ filter_rel_fields_field = {
+ "<RELATED_FIELD>": "<RELATED_FIELD_FIELD>", "<asc|desc>")
+ }
+ """ # pylint: disable=pointless-string-statement
+
+ def _get_related_filter(self, datamodel, column_name: str, value: str) -> Filters:
+ filter_field = self.filter_rel_fields_field.get(column_name)
+ filters = datamodel.get_filters([filter_field])
+ if value:
+ filters.rest_add_filters(
+ [{"opr": "sw", "col": filter_field, "value": value}]
+ )
+ return filters
+
+ @expose("/related/<column_name>", methods=["GET"])
+ @protect()
+ @safe
+ @rison(get_related_schema)
+ def related(self, column_name: str, **kwargs):
+ """Get related fields data
+ ---
+ get:
+ parameters:
+ - in: path
+ schema:
+ type: string
+ name: column_name
+ - in: query
+ name: q
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ page_size:
+ type: integer
+ page:
+ type: integer
+ filter:
+ type: string
+ responses:
+ 200:
+ description: Related column data
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ count:
+ type: integer
+ result:
+ type: object
+ properties:
+ value:
+ type: integer
+ text:
+ 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'
+ """
+ args = kwargs.get("rison", {})
+ # handle pagination
+ page, page_size = self._handle_page_args(args)
+ try:
+ datamodel = self.datamodel.get_related_interface(column_name)
+ except KeyError:
+ return self.response_404()
+ page, page_size = self._sanitize_page_args(page, page_size)
+ # handle ordering
+ order_field = self.order_rel_fields.get(column_name)
+ if order_field:
+ order_column, order_direction = order_field
+ else:
+ order_column, order_direction = "", ""
+ # handle filters
+ filters = self._get_related_filter(datamodel, column_name, args.get("filter"))
+ # Make the query
+ count, values = datamodel.query(
+ filters, order_column, order_direction, page=page, page_size=page_size
+ )
+ # produce response
+ result = [
+ {"value": datamodel.get_pk_value(value), "text": str(value)}
+ for value in values
+ ]
+ return self.response(200, count=count, result=result)
+
+
class CsvResponse(Response): # pylint: disable=too-many-ancestors
"""
Override Response to take into account csv encoding from config.py
diff --git a/superset/views/dashboard/api.py b/superset/views/dashboard/api.py
index 284fe4b..5068478 100644
--- a/superset/views/dashboard/api.py
+++ b/superset/views/dashboard/api.py
@@ -18,7 +18,6 @@ import json
import re
from flask import current_app, g, request
-from flask_appbuilder import ModelRestApi
from flask_appbuilder.api import expose, protect, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import fields, post_load, pre_load, Schema, ValidationError
@@ -29,7 +28,7 @@ import superset.models.core as models
from superset import appbuilder
from superset.exceptions import SupersetException
from superset.utils import core as utils
-from superset.views.base import BaseSupersetSchema
+from superset.views.base import BaseSupersetModelRestApi, BaseSupersetSchema
from .mixin import DashboardMixin
@@ -157,7 +156,7 @@ class DashboardPutSchema(BaseDashboardSchema):
return self.instance
-class DashboardRestApi(DashboardMixin, ModelRestApi):
+class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi):
datamodel = SQLAInterface(models.Dashboard)
resource_name = "dashboard"
@@ -171,6 +170,7 @@ class DashboardRestApi(DashboardMixin, ModelRestApi):
"put": "edit",
"delete": "delete",
"info": "list",
+ "related": "list",
}
exclude_route_methods = ("info",)
show_columns = [
@@ -185,10 +185,28 @@ class DashboardRestApi(DashboardMixin, ModelRestApi):
"table_names",
"charts",
]
+ order_columns = ["dashboard_title", "changed_on", "published", "changed_by_fk"]
+ list_columns = [
+ "id",
+ "dashboard_title",
+ "url",
+ "published",
+ "owners_json",
+ "changed_by.username",
+ "changed_by_name",
+ "changed_by_url",
+ "changed_on",
+ ]
add_model_schema = DashboardPostSchema()
edit_model_schema = DashboardPutSchema()
+ order_rel_fields = {
+ "slices": ("slice_name", "asc"),
+ "owners": ("first_name", "asc"),
+ }
+ filter_rel_fields_field = {"owners": "first_name", "slices": "slice_name"}
+
@expose("/", methods=["POST"])
@protect()
@safe
diff --git a/tests/dashboard_api_tests.py b/tests/dashboard_api_tests.py
index 1e1a3ee..9a60d0a 100644
--- a/tests/dashboard_api_tests.py
+++ b/tests/dashboard_api_tests.py
@@ -18,6 +18,7 @@
import json
from typing import List
+import prison
from flask_appbuilder.security.sqla import models as ab_models
from superset import db, security_manager
@@ -332,10 +333,7 @@ class DashboardApiTests(SupersetTestCase):
def test_update_dashboard_not_owned(self):
"""
- Dashboard API: Test update slug formatting
- """
- """
- Dashboard API: Test delete try not owned
+ Dashboard API: Test update dashboard not owner
"""
user_alpha1 = self.create_user(
"alpha1", "password", "Alpha", email="alpha1@superset.org"
@@ -353,3 +351,61 @@ class DashboardApiTests(SupersetTestCase):
db.session.delete(user_alpha1)
db.session.delete(user_alpha2)
db.session.commit()
+
+ def test_get_related_owners(self):
+ """
+ Dashboard API: Test dashboard get related owners
+ """
+ self.login(username="admin")
+ uri = f"api/v1/dashboard/related/owners"
+ rv = self.client.get(uri)
+ self.assertEqual(rv.status_code, 200)
+ response = json.loads(rv.data.decode("utf-8"))
+ expected_response = {
+ "count": 6,
+ "result": [
+ {"text": "admin user", "value": 1},
+ {"text": "alpha user", "value": 5},
+ {"text": "explore_beta user", "value": 6},
+ {"text": "gamma user", "value": 2},
+ {"text": "gamma2 user", "value": 3},
+ {"text": "gamma_sqllab user", "value": 4},
+ ],
+ }
+ self.assertEqual(response["count"], expected_response["count"])
+ # This is needed to be implemented like this because ordering varies between
+ # postgres and mysql
+ for result in expected_response["result"]:
+ self.assertIn(result, response["result"])
+
+ def test_get_filter_related_owners(self):
+ """
+ Dashboard API: Test dashboard get filter related owners
+ """
+ self.login(username="admin")
+ argument = {"filter": "a"}
+ uri = "api/v1/dashboard/related/owners?{}={}".format(
+ "q", prison.dumps(argument)
+ )
+
+ rv = self.client.get(uri)
+ self.assertEqual(rv.status_code, 200)
+ response = json.loads(rv.data.decode("utf-8"))
+ expected_response = {
+ "count": 2,
+ "result": [
+ {"text": "admin user", "value": 1},
+ {"text": "alpha user", "value": 5},
+ ],
+ }
+ self.assertEqual(response, expected_response)
+
+ def test_get_related_fail(self):
+ """
+ Dashboard API: Test dashboard get related fail
+ """
+ self.login(username="admin")
+ uri = "api/v1/dashboard/related/owner"
+
+ rv = self.client.get(uri)
+ self.assertEqual(rv.status_code, 404)