You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by dp...@apache.org on 2022/04/08 08:05:32 UTC

[superset] branch master updated: feat: deprecate old API and create new API for dashes created by me (#19434)

This is an automated email from the ASF dual-hosted git repository.

dpgaspar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new d6d2777ada feat: deprecate old API and create new API for dashes created by me (#19434)
d6d2777ada is described below

commit d6d2777ada0768682fde7f32cd7e49ec6b0203f2
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Fri Apr 8 09:05:22 2022 +0100

    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 <33...@users.noreply.github.com>
    
    * lint
    
    Co-authored-by: Ville Brofeldt <33...@users.noreply.github.com>
---
 .../src/profile/components/CreatedContent.tsx      | 25 ++++++---
 superset-frontend/src/types/bootstrapTypes.ts      | 10 ++++
 superset/constants.py                              |  1 +
 superset/dashboards/api.py                         | 19 ++++---
 superset/dashboards/filters.py                     | 15 ++++++
 superset/models/helpers.py                         |  8 +++
 superset/views/core.py                             | 16 ++++--
 tests/integration_tests/dashboard_tests.py         | 14 ++++-
 tests/integration_tests/dashboards/api_tests.py    | 59 +++++++++++++++++++++-
 9 files changed, 143 insertions(+), 24 deletions(-)

diff --git a/superset-frontend/src/profile/components/CreatedContent.tsx b/superset-frontend/src/profile/components/CreatedContent.tsx
index b097dee5e1..61dfb418f0 100644
--- a/superset-frontend/src/profile/components/CreatedContent.tsx
+++ b/superset-frontend/src/profile/components/CreatedContent.tsx
@@ -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
diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts
index dc41eb5878..33314e7e46 100644
--- a/superset-frontend/src/types/bootstrapTypes.ts
+++ b/superset-frontend/src/types/bootstrapTypes.ts
@@ -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;
diff --git a/superset/constants.py b/superset/constants.py
index 8399aa457a..2269bdc7b1 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -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",
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index d97b5f78e3..fb9c36ca03 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -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:
diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py
index 52a945ca41..3bbef14f4c 100644
--- a/superset/dashboards/filters.py
+++ b/superset/dashboards/filters.py
@@ -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
 ):
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index 86ac2c1a98..baa0566c01 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -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>')
diff --git a/superset/views/core.py b/superset/views/core.py
index f806ea7b0f..0f5b88887c 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -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 = [
             {
diff --git a/tests/integration_tests/dashboard_tests.py b/tests/integration_tests/dashboard_tests.py
index 63453d85ee..3ad9b07e29 100644
--- a/tests/integration_tests/dashboard_tests.py
+++ b/tests/integration_tests/dashboard_tests.py
@@ -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"):
diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py
index a179fa7c7e..66b498eb35 100644
--- a/tests/integration_tests/dashboards/api_tests.py
+++ b/tests/integration_tests/dashboards/api_tests.py
@@ -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()