diff --git a/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts b/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts index 460b2cc02b468..6664281abe9b9 100644 --- a/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/chart_list/list.test.ts @@ -184,8 +184,12 @@ describe('Charts list', () => { }); it('should allow to favorite/unfavorite', () => { - cy.intercept(`/superset/favstar/slice/*/select/`).as('select'); - cy.intercept(`/superset/favstar/slice/*/unselect/`).as('unselect'); + cy.intercept({ url: `/api/v1/chart/*/favorites/`, method: 'POST' }).as( + 'select', + ); + cy.intercept({ url: `/api/v1/chart/*/favorites/`, method: 'DELETE' }).as( + 'unselect', + ); setGridMode('card'); orderAlphabetical(); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts index 29f1e1c2645b3..84e82b796e0d5 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts @@ -139,11 +139,15 @@ export function interceptLog() { } export function interceptFav() { - cy.intercept(`/superset/favstar/Dashboard/*/select/`).as('select'); + cy.intercept({ url: `/api/v1/dashboard/*/favorites/`, method: 'POST' }).as( + 'select', + ); } export function interceptUnfav() { - cy.intercept(`/superset/favstar/Dashboard/*/unselect/`).as('unselect'); + cy.intercept({ url: `/api/v1/dashboard/*/favorites/`, method: 'POST' }).as( + 'unselect', + ); } export function interceptDataset() { diff --git a/superset-frontend/src/components/FaveStar/FaveStar.test.tsx b/superset-frontend/src/components/FaveStar/FaveStar.test.tsx index ab2fa9fa0ed1e..1ecb372dd06b9 100644 --- a/superset-frontend/src/components/FaveStar/FaveStar.test.tsx +++ b/superset-frontend/src/components/FaveStar/FaveStar.test.tsx @@ -75,7 +75,7 @@ test('render content on tooltip', async () => { expect(screen.getByRole('button')).toBeInTheDocument(); }); -test('Call fetchFaveStar only on the first render', async () => { +test('Call fetchFaveStar on first render and on itemId change', async () => { const props = { itemId: 3, fetchFaveStar: jest.fn(), @@ -92,5 +92,5 @@ test('Call fetchFaveStar only on the first render', async () => { expect(props.fetchFaveStar).toBeCalledWith(props.itemId); rerender(); - expect(props.fetchFaveStar).toBeCalledTimes(1); + expect(props.fetchFaveStar).toBeCalledTimes(2); }); diff --git a/superset-frontend/src/components/FaveStar/index.tsx b/superset-frontend/src/components/FaveStar/index.tsx index c7b605243acd2..8b5d3ee89ae7d 100644 --- a/superset-frontend/src/components/FaveStar/index.tsx +++ b/superset-frontend/src/components/FaveStar/index.tsx @@ -17,8 +17,8 @@ * under the License. */ -import React, { useCallback } from 'react'; -import { css, t, styled, useComponentDidMount } from '@superset-ui/core'; +import React, { useCallback, useEffect } from 'react'; +import { css, t, styled } from '@superset-ui/core'; import { Tooltip } from 'src/components/Tooltip'; import Icons from 'src/components/Icons'; @@ -45,11 +45,9 @@ const FaveStar = ({ saveFaveStar, fetchFaveStar, }: FaveStarProps) => { - useComponentDidMount(() => { - if (fetchFaveStar) { - fetchFaveStar(itemId); - } - }); + useEffect(() => { + fetchFaveStar?.(itemId); + }, [fetchFaveStar, itemId]); const onClick = useCallback( (e: React.MouseEvent) => { diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 058b3700bf44f..a166e5d4db659 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -18,6 +18,7 @@ */ /* eslint camelcase: 0 */ import { ActionCreators as UndoActionCreators } from 'redux-undo'; +import rison from 'rison'; import { ensureIsArray, t, @@ -82,7 +83,6 @@ export function removeSlice(sliceId) { return { type: REMOVE_SLICE, sliceId }; } -const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard'; export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR'; export function toggleFaveStar(isStarred) { return { type: TOGGLE_FAVE_STAR, isStarred }; @@ -92,10 +92,10 @@ export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR'; export function fetchFaveStar(id) { return function fetchFaveStarThunk(dispatch) { return SupersetClient.get({ - endpoint: `${FAVESTAR_BASE_URL}/${id}/count/`, + endpoint: `/api/v1/dashboard/favorite_status/?q=${rison.encode([id])}`, }) .then(({ json }) => { - if (json.count > 0) dispatch(toggleFaveStar(true)); + dispatch(toggleFaveStar(!!json?.result?.[0]?.value)); }) .catch(() => dispatch( @@ -112,10 +112,14 @@ export function fetchFaveStar(id) { export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR'; export function saveFaveStar(id, isStarred) { return function saveFaveStarThunk(dispatch) { - const urlSuffix = isStarred ? 'unselect' : 'select'; - return SupersetClient.get({ - endpoint: `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`, - }) + const endpoint = `/api/v1/dashboard/${id}/favorites/`; + const apiCall = isStarred + ? SupersetClient.delete({ + endpoint, + }) + : SupersetClient.post({ endpoint }); + + return apiCall .then(() => { dispatch(toggleFaveStar(!isStarred)); }) diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index 79b242a179d0b..90790872a2cea 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -279,7 +279,7 @@ export const DashboardPage: FC = ({ idOrSlug }: PageProps) => { }, [addDangerToast, datasets, datasetsApiError, dispatch]); if (error) throw error; // caught in error boundary - if (!readyToRender) return ; + if (!readyToRender || !isDashboardHydrated.current) return ; return ( <> diff --git a/superset-frontend/src/explore/actions/exploreActions.ts b/superset-frontend/src/explore/actions/exploreActions.ts index 0e13499b14c43..36300b4a123a4 100644 --- a/superset-frontend/src/explore/actions/exploreActions.ts +++ b/superset-frontend/src/explore/actions/exploreActions.ts @@ -17,6 +17,7 @@ * under the License. */ /* eslint camelcase: 0 */ +import rison from 'rison'; import { Dataset } from '@superset-ui/chart-controls'; import { t, SupersetClient, QueryFormData } from '@superset-ui/core'; import { Dispatch } from 'redux'; @@ -27,8 +28,6 @@ import { import { Slice } from 'src/types/Chart'; import { SaveActionType } from 'src/explore/types'; -const FAVESTAR_BASE_URL = '/superset/favstar/slice'; - export const UPDATE_FORM_DATA_BY_DATASOURCE = 'UPDATE_FORM_DATA_BY_DATASOURCE'; export function updateFormDataByDatasource( prevDatasource: Dataset, @@ -66,11 +65,9 @@ export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR'; export function fetchFaveStar(sliceId: string) { return function (dispatch: Dispatch) { SupersetClient.get({ - endpoint: `${FAVESTAR_BASE_URL}/${sliceId}/count/`, + endpoint: `/api/v1/chart/favorite_status/?q=${rison.encode([sliceId])}`, }).then(({ json }) => { - if (json.count > 0) { - dispatch(toggleFaveStar(true)); - } + dispatch(toggleFaveStar(!!json?.result?.[0]?.value)); }); }; } @@ -78,10 +75,14 @@ export function fetchFaveStar(sliceId: string) { export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR'; export function saveFaveStar(sliceId: string, isStarred: boolean) { return function (dispatch: Dispatch) { - const urlSuffix = isStarred ? 'unselect' : 'select'; - SupersetClient.get({ - endpoint: `${FAVESTAR_BASE_URL}/${sliceId}/${urlSuffix}/`, - }) + const endpoint = `/api/v1/chart/${sliceId}/favorites/`; + const apiCall = isStarred + ? SupersetClient.delete({ + endpoint, + }) + : SupersetClient.post({ endpoint }); + + apiCall .then(() => dispatch(toggleFaveStar(!isStarred))) .catch(() => { dispatch( diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx index a4ad594387433..4f3b4d8dd05ee 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx @@ -91,7 +91,9 @@ jest.mock('lodash/debounce', () => ({ fetchMock.post('glob:*/api/v1/explore/form_data*', { key: KEY }); fetchMock.put('glob:*/api/v1/explore/form_data*', { key: KEY }); fetchMock.get('glob:*/api/v1/explore/form_data*', {}); -fetchMock.get('glob:*/favstar/slice*', { count: 0 }); +fetchMock.get('glob:*/api/v1/chart/favorite_status*', { + result: [{ value: true }], +}); const defaultPath = '/explore/'; const renderWithRouter = ({ diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts index 6812d1e0c5ca9..dfcf23e1905b2 100644 --- a/superset-frontend/src/views/CRUD/hooks.ts +++ b/superset-frontend/src/views/CRUD/hooks.ts @@ -542,11 +542,6 @@ export function useImportResource( return { state, importResource }; } -enum FavStarClassName { - CHART = 'slice', - DASHBOARD = 'Dashboard', -} - type FavoriteStatusResponse = { result: Array<{ id: string; @@ -599,15 +594,17 @@ export function useFavoriteStatus( const saveFaveStar = useCallback( (id: number, isStarred: boolean) => { - const urlSuffix = isStarred ? 'unselect' : 'select'; - SupersetClient.get({ - endpoint: `/superset/favstar/${ - type === 'chart' ? FavStarClassName.CHART : FavStarClassName.DASHBOARD - }/${id}/${urlSuffix}/`, - }).then( - ({ json }) => { + const endpoint = `/api/v1/${type}/${id}/favorites/`; + const apiCall = isStarred + ? SupersetClient.delete({ + endpoint, + }) + : SupersetClient.post({ endpoint }); + + apiCall.then( + () => { updateFavoriteStatus({ - [id]: (json as { count: number })?.count > 0, + [id]: !isStarred, }); }, createErrorHandler(errMsg => diff --git a/superset/charts/api.py b/superset/charts/api.py index 5b453a2d99f54..f9ccc04e1eb84 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +# pylint: disable=too-many-lines import json import logging from datetime import datetime @@ -111,6 +112,8 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]: "bulk_delete", # not using RouteMethod since locally defined "viz_types", "favorite_status", + "add_favorite", + "remove_favorite", "thumbnail", "screenshot", "cache_screenshot", @@ -848,6 +851,94 @@ def favorite_status(self, **kwargs: Any) -> Response: ] return self.response(200, result=res) + @expose("//favorites/", methods=["POST"]) + @protect() + @safe + @statsd_metrics + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}" + f".add_favorite", + log_to_statsd=False, + ) + def add_favorite(self, pk: int) -> Response: + """Marks the chart as favorite + --- + post: + description: >- + Marks the chart as favorite for the current user + parameters: + - in: path + schema: + type: integer + name: pk + responses: + 200: + description: Chart added to favorites + content: + application/json: + schema: + type: object + properties: + result: + type: object + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 500: + $ref: '#/components/responses/500' + """ + chart = ChartDAO.find_by_id(pk) + if not chart: + return self.response_404() + + ChartDAO.add_favorite(chart) + return self.response(200, result="OK") + + @expose("//favorites/", methods=["DELETE"]) + @protect() + @safe + @statsd_metrics + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}" + f".remove_favorite", + log_to_statsd=False, + ) + def remove_favorite(self, pk: int) -> Response: + """Remove the chart from the user favorite list + --- + delete: + description: >- + Remove the chart from the user favorite list + parameters: + - in: path + schema: + type: integer + name: pk + responses: + 200: + description: Chart removed from favorites + content: + application/json: + schema: + type: object + properties: + result: + type: object + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 500: + $ref: '#/components/responses/500' + """ + chart = ChartDAO.find_by_id(pk) + if not chart: + return self.response_404() + + ChartDAO.remove_favorite(chart) + return self.response(200, result="OK") + @expose("/import/", methods=["POST"]) @protect() @statsd_metrics diff --git a/superset/charts/dao.py b/superset/charts/dao.py index 384bd9a1fe6e2..7102e6ad234bf 100644 --- a/superset/charts/dao.py +++ b/superset/charts/dao.py @@ -16,6 +16,7 @@ # under the License. # pylint: disable=arguments-renamed import logging +from datetime import datetime from typing import List, Optional, TYPE_CHECKING from sqlalchemy.exc import SQLAlchemyError @@ -82,3 +83,32 @@ def favorited_ids(charts: List[Slice]) -> List[FavStar]: ) .all() ] + + @staticmethod + def add_favorite(chart: Slice) -> None: + ids = ChartDAO.favorited_ids([chart]) + if chart.id not in ids: + db.session.add( + FavStar( + class_name=FavStarClassName.CHART, + obj_id=chart.id, + user_id=get_user_id(), + dttm=datetime.now(), + ) + ) + db.session.commit() + + @staticmethod + def remove_favorite(chart: Slice) -> None: + fav = ( + db.session.query(FavStar) + .filter( + FavStar.class_name == FavStarClassName.CHART, + FavStar.obj_id == chart.id, + FavStar.user_id == get_user_id(), + ) + .one_or_none() + ) + if fav: + db.session.delete(fav) + db.session.commit() diff --git a/superset/constants.py b/superset/constants.py index 7007e77e3f1e3..5a1679d6dbead 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -128,6 +128,8 @@ class RouteMethod: # pylint: disable=too-few-public-methods "test_connection": "read", "validate_parameters": "read", "favorite_status": "read", + "add_favorite": "read", + "remove_favorite": "read", "thumbnail": "read", "import_": "write", "refresh": "write", diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 580fc8bc8a809..a252cff000118 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -141,6 +141,8 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]: RouteMethod.RELATED, "bulk_delete", # not using RouteMethod since locally defined "favorite_status", + "add_favorite", + "remove_favorite", "get_charts", "get_datasets", "get_embedded", @@ -1001,6 +1003,94 @@ def favorite_status(self, **kwargs: Any) -> Response: ] return self.response(200, result=res) + @expose("//favorites/", methods=["POST"]) + @protect() + @safe + @statsd_metrics + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}" + f".add_favorite", + log_to_statsd=False, + ) + def add_favorite(self, pk: int) -> Response: + """Marks the dashboard as favorite + --- + post: + description: >- + Marks the dashboard as favorite for the current user + parameters: + - in: path + schema: + type: integer + name: pk + responses: + 200: + description: Dashboard added to favorites + content: + application/json: + schema: + type: object + properties: + result: + type: object + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 500: + $ref: '#/components/responses/500' + """ + dashboard = DashboardDAO.find_by_id(pk) + if not dashboard: + return self.response_404() + + DashboardDAO.add_favorite(dashboard) + return self.response(200, result="OK") + + @expose("//favorites/", methods=["DELETE"]) + @protect() + @safe + @statsd_metrics + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}" + f".remove_favorite", + log_to_statsd=False, + ) + def remove_favorite(self, pk: int) -> Response: + """Remove the dashboard from the user favorite list + --- + delete: + description: >- + Remove the dashboard from the user favorite list + parameters: + - in: path + schema: + type: integer + name: pk + responses: + 200: + description: Dashboard removed from favorites + content: + application/json: + schema: + type: object + properties: + result: + type: object + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + 500: + $ref: '#/components/responses/500' + """ + dashboard = DashboardDAO.find_by_id(pk) + if not dashboard: + return self.response_404() + + DashboardDAO.remove_favorite(dashboard) + return self.response(200, result="OK") + @expose("/import/", methods=["POST"]) @protect() @statsd_metrics diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index c98abee1f048b..a51ddbb92c230 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -307,3 +307,32 @@ def favorited_ids(dashboards: List[Dashboard]) -> List[FavStar]: ) .all() ] + + @staticmethod + def add_favorite(dashboard: Dashboard) -> None: + ids = DashboardDAO.favorited_ids([dashboard]) + if dashboard.id not in ids: + db.session.add( + FavStar( + class_name=FavStarClassName.DASHBOARD, + obj_id=dashboard.id, + user_id=get_user_id(), + dttm=datetime.now(), + ) + ) + db.session.commit() + + @staticmethod + def remove_favorite(dashboard: Dashboard) -> None: + fav = ( + db.session.query(FavStar) + .filter( + FavStar.class_name == FavStarClassName.DASHBOARD, + FavStar.obj_id == dashboard.id, + FavStar.user_id == get_user_id(), + ) + .one_or_none() + ) + if fav: + db.session.delete(fav) + db.session.commit() diff --git a/superset/views/core.py b/superset/views/core.py index 44f1b78af0a01..f2dfa5d1408cf 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1787,6 +1787,7 @@ def warm_up_cache( # pylint: disable=too-many-locals,no-self-use @has_access_api @event_logger.log_this @expose("/favstar////") + @deprecated() def favstar( # pylint: disable=no-self-use self, class_name: str, obj_id: int, action: str ) -> FlaskResponse: diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py index 38fa1b7a6c9d3..e3a58598863a1 100644 --- a/tests/integration_tests/charts/api_tests.py +++ b/tests/integration_tests/charts/api_tests.py @@ -1252,6 +1252,75 @@ def test_get_current_user_favorite_status(self): if res["id"] in users_favorite_ids: assert res["value"] + def test_add_favorite(self): + """ + Dataset API: Test add chart to favorites + """ + chart = Slice( + id=100, + datasource_id=1, + datasource_type="table", + datasource_name="tmp_perm_table", + slice_name="slice_name", + ) + db.session.add(chart) + db.session.commit() + + self.login(username="admin") + uri = f"api/v1/chart/favorite_status/?q={prison.dumps([chart.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is False + + uri = f"api/v1/chart/{chart.id}/favorites/" + self.client.post(uri) + + uri = f"api/v1/chart/favorite_status/?q={prison.dumps([chart.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is True + + db.session.delete(chart) + db.session.commit() + + def test_remove_favorite(self): + """ + Dataset API: Test remove chart from favorites + """ + chart = Slice( + id=100, + datasource_id=1, + datasource_type="table", + datasource_name="tmp_perm_table", + slice_name="slice_name", + ) + db.session.add(chart) + db.session.commit() + + self.login(username="admin") + uri = f"api/v1/chart/{chart.id}/favorites/" + self.client.post(uri) + + uri = f"api/v1/chart/favorite_status/?q={prison.dumps([chart.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is True + + uri = f"api/v1/chart/{chart.id}/favorites/" + self.client.delete(uri) + + uri = f"api/v1/chart/favorite_status/?q={prison.dumps([chart.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is False + + db.session.delete(chart) + db.session.commit() + def test_get_time_range(self): """ Chart API: Test get actually time range from human readable string diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index e5e7b42db775d..6abc649b9a967 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -720,6 +720,75 @@ def test_get_current_user_favorite_status(self): if res["id"] in users_favorite_ids: assert res["value"] + def test_add_favorite(self): + """ + Dataset API: Test add dashboard to favorites + """ + dashboard = Dashboard( + id=100, + dashboard_title="test_dashboard", + slug="test_slug", + slices=[], + published=True, + ) + db.session.add(dashboard) + db.session.commit() + + self.login(username="admin") + uri = f"api/v1/dashboard/favorite_status/?q={prison.dumps([dashboard.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is False + + uri = f"api/v1/dashboard/{dashboard.id}/favorites/" + self.client.post(uri) + + uri = f"api/v1/dashboard/favorite_status/?q={prison.dumps([dashboard.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is True + + db.session.delete(dashboard) + db.session.commit() + + def test_remove_favorite(self): + """ + Dataset API: Test remove dashboard from favorites + """ + dashboard = Dashboard( + id=100, + dashboard_title="test_dashboard", + slug="test_slug", + slices=[], + published=True, + ) + db.session.add(dashboard) + db.session.commit() + + self.login(username="admin") + uri = f"api/v1/dashboard/{dashboard.id}/favorites/" + self.client.post(uri) + + uri = f"api/v1/dashboard/favorite_status/?q={prison.dumps([dashboard.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is True + + uri = f"api/v1/dashboard/{dashboard.id}/favorites/" + self.client.delete(uri) + + uri = f"api/v1/dashboard/favorite_status/?q={prison.dumps([dashboard.id])}" + rv = self.client.get(uri) + data = json.loads(rv.data.decode("utf-8")) + for res in data["result"]: + assert res["value"] is False + + db.session.delete(dashboard) + db.session.commit() + @pytest.mark.usefixtures("create_dashboards") def test_get_dashboards_not_favorite_filter(self): """ diff --git a/tests/unit_tests/charts/dao/dao_tests.py b/tests/unit_tests/charts/dao/dao_tests.py index 15310712a5f8a..faec8694db696 100644 --- a/tests/unit_tests/charts/dao/dao_tests.py +++ b/tests/unit_tests/charts/dao/dao_tests.py @@ -65,3 +65,36 @@ def test_datasource_find_by_id_skip_base_filter_not_found( 125326326, session=session_with_data, skip_base_filter=True ) assert result is None + + +def test_add_favorite(session_with_data: Session) -> None: + from superset.charts.dao import ChartDAO + + chart = ChartDAO.find_by_id(1, session=session_with_data, skip_base_filter=True) + if not chart: + return + assert len(ChartDAO.favorited_ids([chart])) == 0 + + ChartDAO.add_favorite(chart) + assert len(ChartDAO.favorited_ids([chart])) == 1 + + ChartDAO.add_favorite(chart) + assert len(ChartDAO.favorited_ids([chart])) == 1 + + +def test_remove_favorite(session_with_data: Session) -> None: + from superset.charts.dao import ChartDAO + + chart = ChartDAO.find_by_id(1, session=session_with_data, skip_base_filter=True) + if not chart: + return + assert len(ChartDAO.favorited_ids([chart])) == 0 + + ChartDAO.add_favorite(chart) + assert len(ChartDAO.favorited_ids([chart])) == 1 + + ChartDAO.remove_favorite(chart) + assert len(ChartDAO.favorited_ids([chart])) == 0 + + ChartDAO.remove_favorite(chart) + assert len(ChartDAO.favorited_ids([chart])) == 0 diff --git a/tests/unit_tests/dashboards/dao_tests.py b/tests/unit_tests/dashboards/dao_tests.py new file mode 100644 index 0000000000000..a8f93e7513906 --- /dev/null +++ b/tests/unit_tests/dashboards/dao_tests.py @@ -0,0 +1,79 @@ +# 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 typing import Iterator + +import pytest +from sqlalchemy.orm.session import Session + + +@pytest.fixture +def session_with_data(session: Session) -> Iterator[Session]: + from superset.models.dashboard import Dashboard + + engine = session.get_bind() + Dashboard.metadata.create_all(engine) # pylint: disable=no-member + + dashboard_obj = Dashboard( + id=100, + dashboard_title="test_dashboard", + slug="test_slug", + slices=[], + published=True, + ) + + session.add(dashboard_obj) + session.commit() + yield session + session.rollback() + + +def test_add_favorite(session_with_data: Session) -> None: + from superset.dashboards.dao import DashboardDAO + + dashboard = DashboardDAO.find_by_id( + 100, session=session_with_data, skip_base_filter=True + ) + if not dashboard: + return + assert len(DashboardDAO.favorited_ids([dashboard])) == 0 + + DashboardDAO.add_favorite(dashboard) + assert len(DashboardDAO.favorited_ids([dashboard])) == 1 + + DashboardDAO.add_favorite(dashboard) + assert len(DashboardDAO.favorited_ids([dashboard])) == 1 + + +def test_remove_favorite(session_with_data: Session) -> None: + from superset.dashboards.dao import DashboardDAO + + dashboard = DashboardDAO.find_by_id( + 100, session=session_with_data, skip_base_filter=True + ) + if not dashboard: + return + assert len(DashboardDAO.favorited_ids([dashboard])) == 0 + + DashboardDAO.add_favorite(dashboard) + assert len(DashboardDAO.favorited_ids([dashboard])) == 1 + + DashboardDAO.remove_favorite(dashboard) + assert len(DashboardDAO.favorited_ids([dashboard])) == 0 + + DashboardDAO.remove_favorite(dashboard) + assert len(DashboardDAO.favorited_ids([dashboard])) == 0