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)