Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 17 commits into from
Apr 8, 2022
Merged
8 changes: 4 additions & 4 deletions superset-frontend/src/profile/components/CreatedContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ 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,8 +49,8 @@ class CreatedContent extends React.PureComponent<CreatedContentProps> {
}

renderDashboardTable() {
const mutator = (data: Dashboard[]) =>
data.map(dash => ({
const mutator = (data: DashboardResponse) =>
data.result.map(dash => ({
dashboard: <a href={dash.url}>{dash.title}</a>,
created: moment.utc(dash.dttm).fromNow(),
_created: dash.dttm,
Expand All @@ -59,7 +59,7 @@ class CreatedContent extends React.PureComponent<CreatedContentProps> {
<TableLoader
className="table-condensed"
mutator={mutator}
dataEndpoint={`/superset/created_dashboards/${this.props.user.userId}/`}
dataEndpoint="/api/v1/dashboard/created_by_me/"
noDataText={t('No dashboards')}
columns={['dashboard', 'created']}
sortable
Expand Down
4 changes: 4 additions & 0 deletions superset-frontend/src/types/bootstrapTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export type Dashboard = {
creator_url?: string;
};

export type DashboardResponse = {
result: Dashboard[];
};

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
49 changes: 41 additions & 8 deletions superset/dashboards/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
FilterRelatedRoles,
)
from superset.dashboards.schemas import (
DashboardCreatedByMeResponseSchema,
DashboardDatasetSchema,
DashboardGetResponseSchema,
DashboardPostSchema,
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 @@ -225,6 +227,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
dashboard_dataset_schema = DashboardDatasetSchema()
embedded_response_schema = EmbeddedDashboardResponseSchema()
embedded_config_schema = EmbeddedDashboardConfigSchema()
dashboard_created_by_schema = DashboardCreatedByMeResponseSchema()

base_filters = [["id", DashboardAccessFilter, lambda: []]]

Expand All @@ -244,6 +247,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
""" Override the name set for this collection of endpoints """
openapi_spec_component_schemas = (
ChartEntityResponseSchema,
DashboardCreatedByMeResponseSchema,
DashboardGetResponseSchema,
DashboardDatasetSchema,
GetFavStarIdsSchema,
Expand Down Expand Up @@ -307,8 +311,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 +366,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 +427,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 All @@ -455,6 +453,43 @@ def get_charts(self, id_or_slug: str) -> Response:
except DashboardNotFoundError:
return self.response_404()

@expose("/created_by_me/", methods=["GET"])
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
log_to_statsd=False,
)
def created_by_me(self) -> Response:
"""Gets all dashboards created by the current user
---
get:
description: >-
Gets all dashboards created by the current user
responses:
200:
description: Dashboard
content:
application/json:
schema:
type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/DashboardCreatedByMeResponseSchema'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
404:
$ref: '#/components/responses/404'
"""
dashboards = DashboardDAO.get_dashboards_created_by(g.user.id)
dpgaspar marked this conversation as resolved.
Show resolved Hide resolved
result = self.dashboard_created_by_schema.dump(dashboards, many=True)
return self.response(200, result=result)

@expose("/", methods=["POST"])
@protect()
@safe
Expand Down Expand Up @@ -489,8 +524,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
20 changes: 20 additions & 0 deletions superset/dashboards/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from sqlalchemy import or_
from sqlalchemy.exc import SQLAlchemyError

from superset import security_manager
Expand Down Expand Up @@ -100,6 +101,25 @@ def get_dashboard_and_slices_changed_on( # pylint: disable=invalid-name
# drop microseconds in datetime to match with last_modified header
return max(dashboard_changed_on, slices_changed_on).replace(microsecond=0)

@staticmethod
def get_dashboards_created_by(user_id: int) -> List[Dashboard]:
"""
Gets a list of dashboards that were created or changed by a certain user
:param user_id: The user id
:return: List of dashboards
"""
qry = (
db.session.query(Dashboard)
.filter( # pylint: disable=comparison-with-callable
or_(
Dashboard.created_by_fk == user_id,
Dashboard.changed_by_fk == user_id,
)
)
.order_by(Dashboard.changed_on.desc())
)
return qry.all()
dpgaspar marked this conversation as resolved.
Show resolved Hide resolved

@staticmethod
def get_dashboard_and_datasets_changed_on( # pylint: disable=invalid-name
id_or_slug_or_dashboard: Union[str, Dashboard]
Expand Down
20 changes: 19 additions & 1 deletion superset/dashboards/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
import re
from typing import Any, Dict, Union

from marshmallow import fields, post_load, Schema
from marshmallow import fields, post_dump, post_load, Schema
from marshmallow.validate import Length, ValidationError

from superset.exceptions import SupersetException
from superset.models.dashboard import Dashboard
from superset.utils import core as utils

get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
Expand Down Expand Up @@ -227,6 +228,23 @@ def post_load(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
return data


class DashboardCreatedByMeResponseSchema(Schema):
id = fields.Int()
title = fields.Str()
url = fields.Str()
changed_on = fields.DateTime()
dttm = fields.Int()

# pylint: disable=unused-argument,no-self-use
@post_dump(pass_original=True)
def post_dump(
self, data: Dict[str, Any], obj: Dashboard, many: bool = True
) -> Dict[str, Any]:
data["dttm"] = utils.json_int_dttm_ser(obj.changed_on)
data["title"] = obj.dashboard_title
return data


class DashboardPostSchema(BaseDashboardSchema):
dashboard_title = fields.String(
description=dashboard_title_description,
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.select_star "
"This API endpoint is deprecated and will be removed in version 3.0.0",
self.__class__.__name__,
)
dpgaspar marked this conversation as resolved.
Show resolved Hide resolved

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
51 changes: 49 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,33 @@ 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
"""
uri = f"api/v1/dashboard/created_by_me/"
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()) == [
"changed_on",
"dttm",
"id",
"title",
"url",
]
expected_results = [
{"title": "create_title1", "url": "/superset/dashboard/create_slug1/"},
{"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