feat: deprecate old API and create new API for dashes created by me (#19434)

* feat: deprecate old API and create new API for dashes created by me

* add tests

* fix previous test

* fix test and lint

* fix sqlite test

* fix lint

* fix lint

* lint

* fix tests

* fix tests

* use dashboards get list instead

* clean unnecessary marshmallow schema

* Update superset/views/core.py

Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>

* lint

Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
Daniel Vaz Gaspar 2022-04-08 09:05:22 +01:00 committed by GitHub
parent 8da2c9b96a
commit d6d2777ada
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 143 additions and 24 deletions

View File

@ -16,13 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import rison from 'rison';
import React from 'react';
import moment from 'moment';
import { t } from '@superset-ui/core';
import TableLoader from '../../components/TableLoader';
import { Slice } from '../types';
import { User, Dashboard } from '../../types/bootstrapTypes';
import { User, DashboardResponse } from '../../types/bootstrapTypes';
interface CreatedContentProps {
user: User;
@ -49,17 +50,27 @@ class CreatedContent extends React.PureComponent<CreatedContentProps> {
}
renderDashboardTable() {
const mutator = (data: Dashboard[]) =>
data.map(dash => ({
dashboard: <a href={dash.url}>{dash.title}</a>,
created: moment.utc(dash.dttm).fromNow(),
_created: dash.dttm,
const search = [{ col: 'created_by', opr: 'created_by_me', value: 'me' }];
const query = rison.encode({
keys: ['none'],
columns: ['created_on_delta_humanized', 'dashboard_title', 'url'],
filters: search,
order_column: 'changed_on',
order_direction: 'desc',
page: 0,
page_size: 100,
});
const mutator = (data: DashboardResponse) =>
data.result.map(dash => ({
dashboard: <a href={dash.url}>{dash.dashboard_title}</a>,
created: dash.created_on_delta_humanized,
_created: dash.created_on_delta_humanized,
}));
return (
<TableLoader
className="table-condensed"
mutator={mutator}
dataEndpoint={`/superset/created_dashboards/${this.props.user.userId}/`}
dataEndpoint={`/api/v1/dashboard/?q=${query}`}
noDataText={t('No dashboards')}
columns={['dashboard', 'created']}
sortable

View File

@ -46,6 +46,16 @@ export type Dashboard = {
creator_url?: string;
};
export type DashboardData = {
dashboard_title?: string;
created_on_delta_humanized?: string;
url: string;
};
export type DashboardResponse = {
result: DashboardData[];
};
export interface CommonBootstrapData {
flash_messages: string[][];
conf: JsonObject;

View File

@ -100,6 +100,7 @@ MODEL_VIEW_RW_METHOD_PERMISSION_MAP = {
MODEL_API_RW_METHOD_PERMISSION_MAP = {
"bulk_delete": "write",
"created_by_me": "read",
"delete": "write",
"distinct": "read",
"get": "read",

View File

@ -58,6 +58,7 @@ from superset.dashboards.dao import DashboardDAO
from superset.dashboards.filters import (
DashboardAccessFilter,
DashboardCertifiedFilter,
DashboardCreatedByMeFilter,
DashboardFavoriteFilter,
DashboardTitleOrSlugFilter,
FilterRelatedRoles,
@ -139,6 +140,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"set_embedded",
"delete_embedded",
"thumbnail",
"created_by_me",
}
resource_name = "dashboard"
allow_browser_login = True
@ -166,6 +168,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"changed_by_url",
"changed_on_utc",
"changed_on_delta_humanized",
"created_on_delta_humanized",
"created_by.first_name",
"created_by.id",
"created_by.last_name",
@ -179,13 +182,14 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"roles.name",
"is_managed_externally",
]
list_select_columns = list_columns + ["changed_on", "changed_by_fk"]
list_select_columns = list_columns + ["changed_on", "created_on", "changed_by_fk"]
order_columns = [
"changed_by.first_name",
"changed_on_delta_humanized",
"created_by.first_name",
"dashboard_title",
"published",
"changed_on",
]
add_columns = [
@ -215,6 +219,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
search_filters = {
"dashboard_title": [DashboardTitleOrSlugFilter],
"id": [DashboardFavoriteFilter, DashboardCertifiedFilter],
"created_by": [DashboardCreatedByMeFilter],
}
base_order = ("changed_on", "desc")
@ -226,7 +231,9 @@ class DashboardRestApi(BaseSupersetModelRestApi):
embedded_response_schema = EmbeddedDashboardResponseSchema()
embedded_config_schema = EmbeddedDashboardConfigSchema()
base_filters = [["id", DashboardAccessFilter, lambda: []]]
base_filters = [
["id", DashboardAccessFilter, lambda: []],
]
order_rel_fields = {
"slices": ("slice_name", "asc"),
@ -307,8 +314,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
properties:
result:
$ref: '#/components/schemas/DashboardGetResponseSchema'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
@ -364,8 +369,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
type: array
items:
$ref: '#/components/schemas/DashboardDatasetSchema'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
@ -427,8 +430,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
type: array
items:
$ref: '#/components/schemas/ChartEntityResponseSchema'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
@ -489,8 +490,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
type: number
result:
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:

View File

@ -49,6 +49,21 @@ class DashboardTitleOrSlugFilter(BaseFilter): # pylint: disable=too-few-public-
)
class DashboardCreatedByMeFilter(BaseFilter): # pylint: disable=too-few-public-methods
name = _("Created by me")
arg_name = "created_by_me"
def apply(self, query: Query, value: Any) -> Query:
return query.filter(
or_(
Dashboard.created_by_fk # pylint: disable=comparison-with-callable
== g.user.get_user_id(),
Dashboard.changed_by_fk # pylint: disable=comparison-with-callable
== g.user.get_user_id(),
)
)
class DashboardFavoriteFilter( # pylint: disable=too-few-public-methods
BaseFavoriteFilter
):

View File

@ -420,6 +420,10 @@ class AuditMixinNullable(AuditMixin):
def changed_on_delta_humanized(self) -> str:
return self.changed_on_humanized
@renders("created_on")
def created_on_delta_humanized(self) -> str:
return self.created_on_humanized
@renders("changed_on")
def changed_on_utc(self) -> str:
# Convert naive datetime to UTC
@ -429,6 +433,10 @@ class AuditMixinNullable(AuditMixin):
def changed_on_humanized(self) -> str:
return humanize.naturaltime(datetime.now() - self.changed_on)
@property
def created_on_humanized(self) -> str:
return humanize.naturaltime(datetime.now() - self.created_on)
@renders("changed_on")
def modified(self) -> Markup:
return Markup(f'<span class="no-wrap">{self.changed_on_humanized}</span>')

View File

@ -1587,16 +1587,24 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
@event_logger.log_this
@expose("/created_dashboards/<int:user_id>/", methods=["GET"])
def created_dashboards(self, user_id: int) -> FlaskResponse:
logging.warning(
"%s.created_dashboards "
"This API endpoint is deprecated and will be removed in version 3.0.0",
self.__class__.__name__,
)
error_obj = self.get_user_activity_access_error(user_id)
if error_obj:
return error_obj
Dash = Dashboard
qry = (
db.session.query(Dash)
db.session.query(Dashboard)
.filter( # pylint: disable=comparison-with-callable
or_(Dash.created_by_fk == user_id, Dash.changed_by_fk == user_id)
or_(
Dashboard.created_by_fk == user_id,
Dashboard.changed_by_fk == user_id,
)
)
.order_by(Dash.changed_on.desc())
.order_by(Dashboard.changed_on.desc())
)
payload = [
{

View File

@ -18,6 +18,7 @@
"""Unit tests for Superset"""
from datetime import datetime
import json
import re
import unittest
from random import random
@ -139,9 +140,20 @@ class TestDashboard(SupersetTestCase):
self.login(username="admin")
dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0]
url = "/dashboard/new/"
resp = self.get_resp(url)
response = self.client.get(url, follow_redirects=False)
dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0]
self.assertEqual(dash_count_before + 1, dash_count_after)
group = re.match(
r"http:\/\/localhost\/superset\/dashboard\/([0-9]*)\/\?edit=true",
response.headers["Location"],
)
assert group is not None
# Cleanup
created_dashboard_id = int(group[1])
created_dashboard = db.session.query(Dashboard).get(created_dashboard_id)
db.session.delete(created_dashboard)
db.session.commit()
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_save_dash(self, username="admin"):

View File

@ -18,6 +18,7 @@
"""Unit tests for Superset"""
import json
from io import BytesIO
from time import sleep
from typing import List, Optional
from unittest.mock import patch
from zipfile import is_zipfile, ZipFile
@ -27,7 +28,6 @@ from tests.integration_tests.insert_chart_mixin import InsertChartMixin
import pytest
import prison
import yaml
from sqlalchemy.sql import func
from freezegun import freeze_time
from sqlalchemy import and_
@ -160,6 +160,27 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
db.session.delete(fav_dashboard)
db.session.commit()
@pytest.fixture()
def create_created_by_admin_dashboards(self):
with self.create_app().app_context():
dashboards = []
admin = self.get_user("admin")
for cx in range(2):
dashboard = self.insert_dashboard(
f"create_title{cx}",
f"create_slug{cx}",
[admin.id],
created_by=admin,
)
sleep(1)
dashboards.append(dashboard)
yield dashboards
for dashboard in dashboards:
db.session.delete(dashboard)
db.session.commit()
@pytest.fixture()
def create_dashboard_with_report(self):
with self.create_app().app_context():
@ -674,7 +695,41 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
rv = self.get_assert_metric(uri, "get_list")
self.assertEqual(rv.status_code, 200)
data = json.loads(rv.data.decode("utf-8"))
self.assertEqual(data["count"], 6)
self.assertEqual(data["count"], 5)
@pytest.mark.usefixtures("create_created_by_admin_dashboards")
def test_get_dashboards_created_by_me(self):
"""
Dashboard API: Test get dashboards created by current user
"""
query = {
"columns": ["created_on_delta_humanized", "dashboard_title", "url"],
"filters": [{"col": "created_by", "opr": "created_by_me", "value": "me"}],
"order_column": "changed_on",
"order_direction": "desc",
"page": 0,
"page_size": 100,
}
uri = f"api/v1/dashboard/?q={prison.dumps(query)}"
self.login(username="admin")
rv = self.client.get(uri)
data = json.loads(rv.data.decode("utf-8"))
assert rv.status_code == 200
assert len(data["result"]) == 2
assert list(data["result"][0].keys()) == query["columns"]
expected_results = [
{
"dashboard_title": "create_title1",
"url": "/superset/dashboard/create_slug1/",
},
{
"dashboard_title": "create_title0",
"url": "/superset/dashboard/create_slug0/",
},
]
for idx, response_item in enumerate(data["result"]):
for key, value in expected_results[idx].items():
assert response_item[key] == value
def create_dashboard_import(self):
buf = BytesIO()