[dashboard] New, get releated owners and slices (#8872)

This commit is contained in:
Daniel Vaz Gaspar 2019-12-20 10:16:57 +00:00 committed by GitHub
parent b6639504b6
commit 3d9181d270
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 228 additions and 9 deletions

View File

@ -179,6 +179,25 @@ class Dashboard( # pylint: disable=too-many-instance-attributes
title = escape(self.dashboard_title or "<empty>")
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

View File

@ -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

View File

@ -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

View File

@ -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)