Skip to content

Commit

Permalink
feat: deprecate old API and create new API for dashes created by me (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
dpgaspar and villebro committed Apr 8, 2022
1 parent 8da2c9b commit d6d2777
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 24 deletions.
25 changes: 18 additions & 7 deletions superset-frontend/src/profile/components/CreatedContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions superset-frontend/src/types/bootstrapTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions superset/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class RouteMethod: # pylint: disable=too-few-public-methods

MODEL_API_RW_METHOD_PERMISSION_MAP = {
"bulk_delete": "write",
"created_by_me": "read",
"delete": "write",
"distinct": "read",
"get": "read",
Expand Down
19 changes: 9 additions & 10 deletions superset/dashboards/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from superset.dashboards.filters import (
DashboardAccessFilter,
DashboardCertifiedFilter,
DashboardCreatedByMeFilter,
DashboardFavoriteFilter,
DashboardTitleOrSlugFilter,
FilterRelatedRoles,
Expand Down Expand Up @@ -139,6 +140,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"set_embedded",
"delete_embedded",
"thumbnail",
"created_by_me",
}
resource_name = "dashboard"
allow_browser_login = True
Expand Down Expand Up @@ -166,6 +168,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"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",
Expand All @@ -179,13 +182,14 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"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 = [
Expand Down Expand Up @@ -215,6 +219,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
search_filters = {
"dashboard_title": [DashboardTitleOrSlugFilter],
"id": [DashboardFavoriteFilter, DashboardCertifiedFilter],
"created_by": [DashboardCreatedByMeFilter],
}
base_order = ("changed_on", "desc")

Expand All @@ -226,7 +231,9 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
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"),
Expand Down Expand Up @@ -307,8 +314,6 @@ def get(self, dash: Dashboard) -> Response:
properties:
result:
$ref: '#/components/schemas/DashboardGetResponseSchema'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
Expand Down Expand Up @@ -364,8 +369,6 @@ def get_datasets(self, id_or_slug: str) -> Response:
type: array
items:
$ref: '#/components/schemas/DashboardDatasetSchema'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
Expand Down Expand Up @@ -427,8 +430,6 @@ def get_charts(self, id_or_slug: str) -> Response:
type: array
items:
$ref: '#/components/schemas/ChartEntityResponseSchema'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
Expand Down Expand Up @@ -489,8 +490,6 @@ def post(self) -> Response:
type: number
result:
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
Expand Down
15 changes: 15 additions & 0 deletions superset/dashboards/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ def apply(self, query: Query, value: Any) -> Query:
)


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
):
Expand Down
8 changes: 8 additions & 0 deletions superset/models/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,10 @@ def changed_on_(self) -> Markup:
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
Expand All @@ -429,6 +433,10 @@ def changed_on_utc(self) -> str:
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>')
Expand Down
16 changes: 12 additions & 4 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1587,16 +1587,24 @@ def fave_dashboards(self, user_id: int) -> FlaskResponse:
@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 = [
{
Expand Down
14 changes: 13 additions & 1 deletion tests/integration_tests/dashboard_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""Unit tests for Superset"""
from datetime import datetime
import json
import re
import unittest
from random import random

Expand Down Expand Up @@ -139,9 +140,20 @@ def test_new_dashboard(self):
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"):
Expand Down
59 changes: 57 additions & 2 deletions tests/integration_tests/dashboards/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +28,6 @@
import pytest
import prison
import yaml
from sqlalchemy.sql import func

from freezegun import freeze_time
from sqlalchemy import and_
Expand Down Expand Up @@ -160,6 +160,27 @@ def create_dashboards(self):
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():
Expand Down Expand Up @@ -674,7 +695,41 @@ def test_gets_not_certified_dashboards_filter(self):
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()
Expand Down

0 comments on commit d6d2777

Please sign in to comment.