From 4bc87915bd94a75c16de8b60a47cc15505aefcdc Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" Date: Wed, 16 Aug 2023 09:32:29 -0300 Subject: [PATCH 1/2] feat: Moves Profile to Single Page App (SPA) --- .../src/features/home/RightMenu.tsx | 2 +- .../profile}/CreatedContent.test.tsx | 2 +- .../profile}/CreatedContent.tsx | 0 .../profile}/Favorites.test.tsx | 2 +- .../profile}/Favorites.tsx | 2 +- .../profile}/RecentActivity.test.tsx | 2 +- .../profile}/RecentActivity.tsx | 7 +-- .../profile}/Security.test.tsx | 2 +- .../profile}/Security.tsx | 0 .../profile}/UserInfo.test.tsx | 2 +- .../profile}/UserInfo.tsx | 0 .../profile}/fixtures.tsx | 0 .../src/{ => features}/profile/types.ts | 0 .../Profile/Profile.test.tsx} | 13 ++--- .../App.tsx => pages/Profile/index.tsx} | 10 ++-- superset-frontend/src/profile/App.tsx | 58 ------------------- superset-frontend/src/profile/index.tsx | 23 -------- superset-frontend/src/views/routes.tsx | 8 +++ superset-frontend/webpack.config.js | 1 - superset/initialization/__init__.py | 2 + superset/views/base.py | 2 +- superset/views/core.py | 20 +------ superset/views/profile.py | 40 +++++++++++++ 23 files changed, 74 insertions(+), 124 deletions(-) rename superset-frontend/src/{profile/components => features/profile}/CreatedContent.test.tsx (95%) rename superset-frontend/src/{profile/components => features/profile}/CreatedContent.tsx (100%) rename superset-frontend/src/{profile/components => features/profile}/Favorites.test.tsx (96%) rename superset-frontend/src/{profile/components => features/profile}/Favorites.tsx (98%) rename superset-frontend/src/{profile/components => features/profile}/RecentActivity.test.tsx (95%) rename superset-frontend/src/{profile/components => features/profile}/RecentActivity.tsx (91%) rename superset-frontend/src/{profile/components => features/profile}/Security.test.tsx (97%) rename superset-frontend/src/{profile/components => features/profile}/Security.tsx (100%) rename superset-frontend/src/{profile/components => features/profile}/UserInfo.test.tsx (97%) rename superset-frontend/src/{profile/components => features/profile}/UserInfo.tsx (100%) rename superset-frontend/src/{profile/components => features/profile}/fixtures.tsx (100%) rename superset-frontend/src/{ => features}/profile/types.ts (100%) rename superset-frontend/src/{profile/components/App.test.tsx => pages/Profile/Profile.test.tsx} (79%) rename superset-frontend/src/{profile/components/App.tsx => pages/Profile/index.tsx} (90%) delete mode 100644 superset-frontend/src/profile/App.tsx delete mode 100644 superset-frontend/src/profile/index.tsx create mode 100644 superset/views/profile.py diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index d0b8e44ab756b..831ae85ba39f3 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -474,7 +474,7 @@ const RightMenu = ({ {navbarRight.user_profile_url && ( - {t('Profile')} + {t('Profile')} )} {navbarRight.user_info_url && ( diff --git a/superset-frontend/src/profile/components/CreatedContent.test.tsx b/superset-frontend/src/features/profile/CreatedContent.test.tsx similarity index 95% rename from superset-frontend/src/profile/components/CreatedContent.test.tsx rename to superset-frontend/src/features/profile/CreatedContent.test.tsx index 817448cf35ea7..df49e98aff90d 100644 --- a/superset-frontend/src/profile/components/CreatedContent.test.tsx +++ b/superset-frontend/src/features/profile/CreatedContent.test.tsx @@ -20,8 +20,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; -import CreatedContent from 'src/profile/components/CreatedContent'; import TableLoader from 'src/components/TableLoader'; +import CreatedContent from './CreatedContent'; import { user } from './fixtures'; diff --git a/superset-frontend/src/profile/components/CreatedContent.tsx b/superset-frontend/src/features/profile/CreatedContent.tsx similarity index 100% rename from superset-frontend/src/profile/components/CreatedContent.tsx rename to superset-frontend/src/features/profile/CreatedContent.tsx diff --git a/superset-frontend/src/profile/components/Favorites.test.tsx b/superset-frontend/src/features/profile/Favorites.test.tsx similarity index 96% rename from superset-frontend/src/profile/components/Favorites.test.tsx rename to superset-frontend/src/features/profile/Favorites.test.tsx index 8b5eaf4e86c45..e21967f3bf170 100644 --- a/superset-frontend/src/profile/components/Favorites.test.tsx +++ b/superset-frontend/src/features/profile/Favorites.test.tsx @@ -20,8 +20,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; -import Favorites from 'src/profile/components/Favorites'; import TableLoader from 'src/components/TableLoader'; +import Favorites from './Favorites'; import { user } from './fixtures'; diff --git a/superset-frontend/src/profile/components/Favorites.tsx b/superset-frontend/src/features/profile/Favorites.tsx similarity index 98% rename from superset-frontend/src/profile/components/Favorites.tsx rename to superset-frontend/src/features/profile/Favorites.tsx index 834f933071676..f38f174779689 100644 --- a/superset-frontend/src/profile/components/Favorites.tsx +++ b/superset-frontend/src/features/profile/Favorites.tsx @@ -22,7 +22,7 @@ import moment from 'moment'; import { t } from '@superset-ui/core'; import { DashboardResponse, BootstrapUser } from 'src/types/bootstrapTypes'; import TableLoader from '../../components/TableLoader'; -import { Chart } from '../types'; +import { Chart } from './types'; interface FavoritesProps { user: BootstrapUser; diff --git a/superset-frontend/src/profile/components/RecentActivity.test.tsx b/superset-frontend/src/features/profile/RecentActivity.test.tsx similarity index 95% rename from superset-frontend/src/profile/components/RecentActivity.test.tsx rename to superset-frontend/src/features/profile/RecentActivity.test.tsx index 73fdeb6e841c8..a64c209296743 100644 --- a/superset-frontend/src/profile/components/RecentActivity.test.tsx +++ b/superset-frontend/src/features/profile/RecentActivity.test.tsx @@ -18,8 +18,8 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -import RecentActivity from 'src/profile/components/RecentActivity'; import TableLoader from 'src/components/TableLoader'; +import RecentActivity from './RecentActivity'; import { user } from './fixtures'; diff --git a/superset-frontend/src/profile/components/RecentActivity.tsx b/superset-frontend/src/features/profile/RecentActivity.tsx similarity index 91% rename from superset-frontend/src/profile/components/RecentActivity.tsx rename to superset-frontend/src/features/profile/RecentActivity.tsx index 2810fb3544963..d550d2a9531c4 100644 --- a/superset-frontend/src/profile/components/RecentActivity.tsx +++ b/superset-frontend/src/features/profile/RecentActivity.tsx @@ -20,10 +20,9 @@ import React from 'react'; import moment from 'moment'; import { t } from '@superset-ui/core'; import rison from 'rison'; - -import TableLoader from '../../components/TableLoader'; -import { ActivityResult } from '../types'; -import { BootstrapUser } from '../../types/bootstrapTypes'; +import TableLoader from 'src/components/TableLoader'; +import { BootstrapUser } from 'src/types/bootstrapTypes'; +import { ActivityResult } from './types'; interface RecentActivityProps { user: BootstrapUser; diff --git a/superset-frontend/src/profile/components/Security.test.tsx b/superset-frontend/src/features/profile/Security.test.tsx similarity index 97% rename from superset-frontend/src/profile/components/Security.test.tsx rename to superset-frontend/src/features/profile/Security.test.tsx index 31eda0aae7652..af4706375c848 100644 --- a/superset-frontend/src/profile/components/Security.test.tsx +++ b/superset-frontend/src/features/profile/Security.test.tsx @@ -18,9 +18,9 @@ */ import React from 'react'; import { styledMount as mount } from 'spec/helpers/theming'; -import Security from 'src/profile/components/Security'; import Label from 'src/components/Label'; import { user, userNoPerms } from './fixtures'; +import Security from './Security'; describe('Security', () => { const mockedProps = { diff --git a/superset-frontend/src/profile/components/Security.tsx b/superset-frontend/src/features/profile/Security.tsx similarity index 100% rename from superset-frontend/src/profile/components/Security.tsx rename to superset-frontend/src/features/profile/Security.tsx diff --git a/superset-frontend/src/profile/components/UserInfo.test.tsx b/superset-frontend/src/features/profile/UserInfo.test.tsx similarity index 97% rename from superset-frontend/src/profile/components/UserInfo.test.tsx rename to superset-frontend/src/features/profile/UserInfo.test.tsx index 9d8a904cb568b..6bbc2b52d1e8e 100644 --- a/superset-frontend/src/profile/components/UserInfo.test.tsx +++ b/superset-frontend/src/features/profile/UserInfo.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import Gravatar from 'react-gravatar'; import { mount } from 'enzyme'; -import UserInfo from 'src/profile/components/UserInfo'; +import UserInfo from './UserInfo'; import { user } from './fixtures'; diff --git a/superset-frontend/src/profile/components/UserInfo.tsx b/superset-frontend/src/features/profile/UserInfo.tsx similarity index 100% rename from superset-frontend/src/profile/components/UserInfo.tsx rename to superset-frontend/src/features/profile/UserInfo.tsx diff --git a/superset-frontend/src/profile/components/fixtures.tsx b/superset-frontend/src/features/profile/fixtures.tsx similarity index 100% rename from superset-frontend/src/profile/components/fixtures.tsx rename to superset-frontend/src/features/profile/fixtures.tsx diff --git a/superset-frontend/src/profile/types.ts b/superset-frontend/src/features/profile/types.ts similarity index 100% rename from superset-frontend/src/profile/types.ts rename to superset-frontend/src/features/profile/types.ts diff --git a/superset-frontend/src/profile/components/App.test.tsx b/superset-frontend/src/pages/Profile/Profile.test.tsx similarity index 79% rename from superset-frontend/src/profile/components/App.test.tsx rename to superset-frontend/src/pages/Profile/Profile.test.tsx index 4f7a4caa1e1f8..cf1446c29ac62 100644 --- a/superset-frontend/src/profile/components/App.test.tsx +++ b/superset-frontend/src/pages/Profile/Profile.test.tsx @@ -19,26 +19,25 @@ import React from 'react'; import { Row, Col } from 'src/components'; import { shallow } from 'enzyme'; -import App from 'src/profile/components/App'; +import Profile from 'src/pages/Profile'; +import { user } from 'src/features/profile/fixtures'; -import { user } from './fixtures'; - -describe('App', () => { +describe('Profile', () => { const mockedProps = { user, }; it('is valid', () => { - expect(React.isValidElement()).toBe(true); + expect(React.isValidElement()).toBe(true); }); it('renders 2 Col', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(Row)).toExist(); expect(wrapper.find(Col)).toHaveLength(2); }); it('renders 4 Tabs', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('[tab]')).toHaveLength(4); }); }); diff --git a/superset-frontend/src/profile/components/App.tsx b/superset-frontend/src/pages/Profile/index.tsx similarity index 90% rename from superset-frontend/src/profile/components/App.tsx rename to superset-frontend/src/pages/Profile/index.tsx index 08130176fe61a..511017941a85b 100644 --- a/superset-frontend/src/profile/components/App.tsx +++ b/superset-frontend/src/pages/Profile/index.tsx @@ -21,11 +21,11 @@ import { t, styled } from '@superset-ui/core'; import { Row, Col } from 'src/components'; import Tabs from 'src/components/Tabs'; import { BootstrapUser } from 'src/types/bootstrapTypes'; -import Favorites from './Favorites'; -import UserInfo from './UserInfo'; -import Security from './Security'; -import RecentActivity from './RecentActivity'; -import CreatedContent from './CreatedContent'; +import Favorites from 'src/features/profile/Favorites'; +import UserInfo from 'src/features/profile/UserInfo'; +import Security from 'src/features/profile/Security'; +import RecentActivity from 'src/features/profile/RecentActivity'; +import CreatedContent from 'src/features/profile/CreatedContent'; interface AppProps { user: BootstrapUser; diff --git a/superset-frontend/src/profile/App.tsx b/superset-frontend/src/profile/App.tsx deleted file mode 100644 index ed331ce73725c..0000000000000 --- a/superset-frontend/src/profile/App.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React from 'react'; -import { hot } from 'react-hot-loader/root'; -import thunk from 'redux-thunk'; -import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; -import { Provider } from 'react-redux'; -import { ThemeProvider } from '@superset-ui/core'; -import { GlobalStyles } from 'src/GlobalStyles'; -import App from 'src/profile/components/App'; -import messageToastReducer from 'src/components/MessageToasts/reducers'; -import { initEnhancer } from 'src/reduxUtils'; -import setupApp from 'src/setup/setupApp'; -import setupExtensions from 'src/setup/setupExtensions'; -import { theme } from 'src/preamble'; -import ToastContainer from 'src/components/MessageToasts/ToastContainer'; -import getBootstrapData from 'src/utils/getBootstrapData'; - -setupApp(); -setupExtensions(); - -const bootstrapData = getBootstrapData(); - -const store = createStore( - combineReducers({ - messageToasts: messageToastReducer, - }), - {}, - compose(applyMiddleware(thunk), initEnhancer(false)), -); - -const Application = () => ( - - - - - - - -); - -export default hot(Application); diff --git a/superset-frontend/src/profile/index.tsx b/superset-frontend/src/profile/index.tsx deleted file mode 100644 index c257009e64fd5..0000000000000 --- a/superset-frontend/src/profile/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -ReactDOM.render(, document.getElementById('app')); diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx index 23a25a5d855b8..197284d3acb6a 100644 --- a/superset-frontend/src/views/routes.tsx +++ b/superset-frontend/src/views/routes.tsx @@ -119,6 +119,10 @@ const RowLevelSecurityList = lazy( ), ); +const Profile = lazy( + () => import(/* webpackChunkName: "Profile" */ 'src/pages/Profile'), +); + type Routes = { path: string; Component: React.ComponentType; @@ -217,6 +221,10 @@ export const routes: Routes = [ path: '/rowlevelsecurity/list', Component: RowLevelSecurityList, }, + { + path: '/profile', + Component: Profile, + }, ]; if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) { diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index fb137c06cbe60..a47670d9d4023 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -212,7 +212,6 @@ const config = { spa: addPreamble('/src/views/index.tsx'), embedded: addPreamble('/src/embedded/index.tsx'), sqllab: addPreamble('/src/SqlLab/index.tsx'), - profile: addPreamble('/src/profile/index.tsx'), showSavedQuery: [path.join(APP_DIR, '/src/showSavedQuery/index.jsx')], }, output, diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 828be7840162b..c390a6f779ec4 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -182,6 +182,7 @@ def init_views(self) -> None: from superset.views.key_value import KV from superset.views.log.api import LogRestApi from superset.views.log.views import LogModelView + from superset.views.profile import ProfileView from superset.views.redirects import R from superset.views.sql_lab.views import ( SavedQueryView, @@ -309,6 +310,7 @@ def init_views(self) -> None: appbuilder.add_view_no_menu(ExplorePermalinkView) appbuilder.add_view_no_menu(KV) appbuilder.add_view_no_menu(R) + appbuilder.add_view_no_menu(ProfileView) appbuilder.add_view_no_menu(SavedQueryView) appbuilder.add_view_no_menu(SavedQueryViewApi) appbuilder.add_view_no_menu(SliceAsync) diff --git a/superset/views/base.py b/superset/views/base.py index a2c62df41bf7e..d1c865374a424 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -406,7 +406,7 @@ def menu_data(user: User) -> dict[str, Any]: "user_login_url": appbuilder.get_url_for_login, "user_profile_url": None if user.is_anonymous or is_feature_enabled("MENU_HIDE_USER_INFO") - else "/superset/profile/", + else "/profile/", "locale": session.get("locale", "en"), }, } diff --git a/superset/views/core.py b/superset/views/core.py index d377f94d653c3..366ec6d664e38 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -80,7 +80,6 @@ base_json_conv, DatasourceType, get_user_id, - get_username, ReservedUrlParameters, ) from superset.views.base import ( @@ -985,24 +984,9 @@ def welcome(self) -> FlaskResponse: @has_access @event_logger.log_this @expose("/profile/") + @deprecated(new_target="/profile") def profile(self) -> FlaskResponse: - """User profile page""" - user = g.user if hasattr(g, "user") and g.user else None - if not user or security_manager.is_guest_user(user) or user.is_anonymous: - abort(404) - payload = { - "user": bootstrap_user_data(user, include_perms=True), - "common": common_bootstrap_payload(user), - } - - return self.render_template( - "superset/basic.html", - title=_("%(user)s's profile", user=get_username()), - entry="profile", - bootstrap_data=json.dumps( - payload, default=utils.pessimistic_json_iso_dttm_ser - ), - ) + return redirect("/profile/") @has_access @event_logger.log_this diff --git a/superset/views/profile.py b/superset/views/profile.py new file mode 100644 index 0000000000000..3308a0f645edc --- /dev/null +++ b/superset/views/profile.py @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from flask import abort, g +from flask_appbuilder import permission_name +from flask_appbuilder.api import expose +from flask_appbuilder.security.decorators import has_access + +from superset import event_logger, security_manager +from superset.superset_typing import FlaskResponse + +from .base import BaseSupersetView + + +class ProfileView(BaseSupersetView): + route_base = "/profile" + class_permission_name = "Profile" + + @expose("/") + @has_access + @permission_name("read") + @event_logger.log_this + def root(self) -> FlaskResponse: + user = g.user if hasattr(g, "user") and g.user else None + if not user or security_manager.is_guest_user(user) or user.is_anonymous: + abort(404) + return super().render_app_template() From 7eee58640a839a333415d0cbffac8c7f6a0d5a0b Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" Date: Wed, 16 Aug 2023 10:49:28 -0300 Subject: [PATCH 2/2] Moves tests --- tests/integration_tests/core_tests.py | 138 ++----------------- tests/integration_tests/profile_tests.py | 164 +++++++++++++++++++++++ 2 files changed, 172 insertions(+), 130 deletions(-) create mode 100644 tests/integration_tests/profile_tests.py diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index ddcfa189b2b28..f1b1025b0ecd2 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -26,12 +26,10 @@ from urllib.parse import quote import pandas as pd -import prison import pytest import pytz import sqlalchemy as sqla from flask_babel import lazy_gettext as _ -from sqlalchemy import Table from sqlalchemy.exc import SQLAlchemyError import superset.utils.database @@ -43,10 +41,9 @@ from superset.connectors.sqla.models import SqlaTable from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.mssql import MssqlEngineSpec -from superset.exceptions import QueryObjectValidationError, SupersetException +from superset.exceptions import SupersetException from superset.extensions import async_query_manager, cache_manager from superset.models import core as models -from superset.models.annotations import Annotation, AnnotationLayer from superset.models.cache import CacheKey from superset.models.dashboard import Dashboard from superset.models.slice import Slice @@ -56,9 +53,8 @@ from superset.utils import core as utils from superset.utils.core import backend from superset.utils.database import get_example_database -from superset.views import core as views from superset.views.database.views import DatabaseView -from tests.integration_tests.conftest import CTAS_SCHEMA_NAME, with_feature_flags +from tests.integration_tests.conftest import with_feature_flags from tests.integration_tests.fixtures.birth_names_dashboard import ( load_birth_names_dashboard_with_slices, load_birth_names_data, @@ -67,12 +63,10 @@ load_energy_table_data, load_energy_table_with_slice, ) -from tests.integration_tests.fixtures.public_role import public_role_like_gamma from tests.integration_tests.fixtures.world_bank_dashboard import ( load_world_bank_dashboard_with_slices, load_world_bank_data, ) -from tests.integration_tests.insert_chart_mixin import InsertChartMixin from tests.integration_tests.test_app import app from .base_tests import SupersetTestCase @@ -88,7 +82,7 @@ def cleanup(): yield -class TestCore(SupersetTestCase, InsertChartMixin): +class TestCore(SupersetTestCase): def setUp(self): self.table_ids = { tbl.table_name: tbl.id for tbl in (db.session.query(SqlaTable).all()) @@ -109,25 +103,6 @@ def insert_dashboard_created_by(self, username: str) -> Dashboard: ) return dashboard - def insert_chart_created_by(self, username: str) -> Slice: - user = self.get_user(username) - dataset = db.session.query(SqlaTable).first() - chart = self.insert_chart( - f"create_title_test", - [user.id], - dataset.id, - created_by=user, - ) - return chart - - @pytest.fixture() - def insert_dashboard_created_by_admin(self): - with self.create_app().app_context(): - dashboard = self.insert_dashboard_created_by("admin") - yield dashboard - db.session.delete(dashboard) - db.session.commit() - @pytest.fixture() def insert_dashboard_created_by_gamma(self): dashboard = self.insert_dashboard_created_by("gamma") @@ -135,14 +110,6 @@ def insert_dashboard_created_by_gamma(self): db.session.delete(dashboard) db.session.commit() - @pytest.fixture() - def insert_chart_created_by_admin(self): - with self.create_app().app_context(): - chart = self.insert_chart_created_by("admin") - yield chart - db.session.delete(chart) - db.session.commit() - def test_login(self): resp = self.get_resp("/login/", data=dict(username="admin", password="general")) self.assertNotIn("User confirmation needed", resp) @@ -515,100 +482,6 @@ def test_fetch_datasource_metadata(self): for k in keys: self.assertIn(k, resp.keys()) - @pytest.mark.usefixtures("insert_dashboard_created_by_admin") - @pytest.mark.usefixtures("insert_chart_created_by_admin") - @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") - def test_user_profile(self, username="admin"): - self.login(username=username) - slc = self.get_slice("Girls", db.session) - dashboard = db.session.query(Dashboard).filter_by(slug="births").first() - # Set a favorite dashboard - self.client.post(f"/api/v1/dashboard/{dashboard.id}/favorites/", json={}) - # Set a favorite chart - self.client.post(f"/api/v1/chart/{slc.id}/favorites/", json={}) - - # Get favorite dashboards: - request_query = { - "columns": ["created_on_delta_humanized", "dashboard_title", "url"], - "filters": [{"col": "id", "opr": "dashboard_is_favorite", "value": True}], - "keys": ["none"], - "order_column": "changed_on", - "order_direction": "desc", - "page": 0, - "page_size": 100, - } - url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}" - resp = self.client.get(url) - assert resp.json["count"] == 1 - assert resp.json["result"][0]["dashboard_title"] == "USA Births Names" - - # Get Favorite Charts - request_query = { - "filters": [{"col": "id", "opr": "chart_is_favorite", "value": True}], - "order_column": "slice_name", - "order_direction": "asc", - "page": 0, - "page_size": 25, - } - url = f"api/v1/chart/?q={prison.dumps(request_query)}" - resp = self.client.get(url) - assert resp.json["count"] == 1 - assert resp.json["result"][0]["id"] == slc.id - - # Get recent activity - url = "/api/v1/log/recent_activity/?q=(page_size:50)" - resp = self.client.get(url) - # TODO data for recent activity varies for sqlite, we should be able to assert - # the returned data - assert resp.status_code == 200 - - # Get dashboards created by the user - request_query = { - "columns": ["created_on_delta_humanized", "dashboard_title", "url"], - "filters": [ - {"col": "created_by", "opr": "dashboard_created_by_me", "value": "me"} - ], - "keys": ["none"], - "order_column": "changed_on", - "order_direction": "desc", - "page": 0, - "page_size": 100, - } - url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}" - resp = self.client.get(url) - assert resp.json["result"][0]["dashboard_title"] == "create_title_test" - - # Get charts created by the user - request_query = { - "columns": ["created_on_delta_humanized", "slice_name", "url"], - "filters": [ - {"col": "created_by", "opr": "chart_created_by_me", "value": "me"} - ], - "keys": ["none"], - "order_column": "changed_on_delta_humanized", - "order_direction": "desc", - "page": 0, - "page_size": 100, - } - url = f"/api/v1/chart/?q={prison.dumps(request_query)}" - resp = self.client.get(url) - assert resp.json["count"] == 1 - assert resp.json["result"][0]["slice_name"] == "create_title_test" - - resp = self.get_resp(f"/superset/profile/") - self.assertIn('"app"', resp) - - def test_user_profile_gamma(self): - self.login(username="gamma") - resp = self.get_resp(f"/superset/profile/") - self.assertIn('"app"', resp) - - @pytest.mark.usefixtures("public_role_like_gamma") - def test_user_profile_anonymous(self): - self.logout() - resp = self.client.get("/superset/profile/") - assert resp.status_code == 404 - @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_slice_id_is_always_logged_correctly_on_web_request(self): # explore case @@ -1330,6 +1203,11 @@ def test_has_table_by_name(self): is True ) + def test_redirect_new_profile(self): + self.login(username="admin") + resp = self.client.get("/superset/profile/") + assert resp.status_code == 302 + if __name__ == "__main__": unittest.main() diff --git a/tests/integration_tests/profile_tests.py b/tests/integration_tests/profile_tests.py new file mode 100644 index 0000000000000..aa5448139e707 --- /dev/null +++ b/tests/integration_tests/profile_tests.py @@ -0,0 +1,164 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import prison +import pytest + +from superset import db +from superset.connectors.sqla.models import SqlaTable +from superset.models.dashboard import Dashboard +from superset.models.slice import Slice +from tests.integration_tests.fixtures.birth_names_dashboard import ( + load_birth_names_dashboard_with_slices, + load_birth_names_data, +) +from tests.integration_tests.fixtures.public_role import public_role_like_gamma +from tests.integration_tests.insert_chart_mixin import InsertChartMixin + +from .base_tests import SupersetTestCase + + +class TestProfile(SupersetTestCase, InsertChartMixin): + def insert_dashboard_created_by(self, username: str) -> Dashboard: + user = self.get_user(username) + dashboard = self.insert_dashboard( + f"create_title_test", + f"create_slug_test", + [user.id], + created_by=user, + ) + return dashboard + + @pytest.fixture() + def insert_dashboard_created_by_admin(self): + with self.create_app().app_context(): + dashboard = self.insert_dashboard_created_by("admin") + yield dashboard + db.session.delete(dashboard) + db.session.commit() + + def insert_chart_created_by(self, username: str) -> Slice: + user = self.get_user(username) + dataset = db.session.query(SqlaTable).first() + chart = self.insert_chart( + f"create_title_test", + [user.id], + dataset.id, + created_by=user, + ) + return chart + + @pytest.fixture() + def insert_chart_created_by_admin(self): + with self.create_app().app_context(): + chart = self.insert_chart_created_by("admin") + yield chart + db.session.delete(chart) + db.session.commit() + + @pytest.mark.usefixtures("insert_dashboard_created_by_admin") + @pytest.mark.usefixtures("insert_chart_created_by_admin") + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") + def test_user_profile(self, username="admin"): + self.login(username=username) + slc = self.get_slice("Girls", db.session) + dashboard = db.session.query(Dashboard).filter_by(slug="births").first() + # Set a favorite dashboard + self.client.post(f"/api/v1/dashboard/{dashboard.id}/favorites/", json={}) + # Set a favorite chart + self.client.post(f"/api/v1/chart/{slc.id}/favorites/", json={}) + + # Get favorite dashboards: + request_query = { + "columns": ["created_on_delta_humanized", "dashboard_title", "url"], + "filters": [{"col": "id", "opr": "dashboard_is_favorite", "value": True}], + "keys": ["none"], + "order_column": "changed_on", + "order_direction": "desc", + "page": 0, + "page_size": 100, + } + url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}" + resp = self.client.get(url) + assert resp.json["count"] == 1 + assert resp.json["result"][0]["dashboard_title"] == "USA Births Names" + + # Get Favorite Charts + request_query = { + "filters": [{"col": "id", "opr": "chart_is_favorite", "value": True}], + "order_column": "slice_name", + "order_direction": "asc", + "page": 0, + "page_size": 25, + } + url = f"api/v1/chart/?q={prison.dumps(request_query)}" + resp = self.client.get(url) + assert resp.json["count"] == 1 + assert resp.json["result"][0]["id"] == slc.id + + # Get recent activity + url = "/api/v1/log/recent_activity/?q=(page_size:50)" + resp = self.client.get(url) + # TODO data for recent activity varies for sqlite, we should be able to assert + # the returned data + assert resp.status_code == 200 + + # Get dashboards created by the user + request_query = { + "columns": ["created_on_delta_humanized", "dashboard_title", "url"], + "filters": [ + {"col": "created_by", "opr": "dashboard_created_by_me", "value": "me"} + ], + "keys": ["none"], + "order_column": "changed_on", + "order_direction": "desc", + "page": 0, + "page_size": 100, + } + url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}" + resp = self.client.get(url) + assert resp.json["result"][0]["dashboard_title"] == "create_title_test" + + # Get charts created by the user + request_query = { + "columns": ["created_on_delta_humanized", "slice_name", "url"], + "filters": [ + {"col": "created_by", "opr": "chart_created_by_me", "value": "me"} + ], + "keys": ["none"], + "order_column": "changed_on_delta_humanized", + "order_direction": "desc", + "page": 0, + "page_size": 100, + } + url = f"/api/v1/chart/?q={prison.dumps(request_query)}" + resp = self.client.get(url) + assert resp.json["count"] == 1 + assert resp.json["result"][0]["slice_name"] == "create_title_test" + + resp = self.get_resp(f"/profile/") + self.assertIn('"app"', resp) + + def test_user_profile_gamma(self): + self.login(username="gamma") + resp = self.get_resp(f"/profile/") + self.assertIn('"app"', resp) + + @pytest.mark.usefixtures("public_role_like_gamma") + def test_user_profile_anonymous(self): + self.logout() + resp = self.client.get("/profile/") + assert resp.status_code == 404