[dashboard] New, get releated owners and slices (#8872)
This commit is contained in:
parent
b6639504b6
commit
3d9181d270
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue