You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by hu...@apache.org on 2023/07/27 17:17:34 UTC
[superset] branch master updated: feat(Tags): Allow users to favorite Tags on CRUD Listview page (#24701)
This is an automated email from the ASF dual-hosted git repository.
hugh 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 3b46511439 feat(Tags): Allow users to favorite Tags on CRUD Listview page (#24701)
3b46511439 is described below
commit 3b465114395ff30e2eebe07173236692fb85ab76
Author: Hugh A. Miles II <hu...@gmail.com>
AuthorDate: Thu Jul 27 13:17:26 2023 -0400
feat(Tags): Allow users to favorite Tags on CRUD Listview page (#24701)
---
superset-frontend/src/pages/Tags/index.tsx | 29 +++-
superset-frontend/src/views/CRUD/hooks.ts | 7 +-
superset/daos/tag.py | 108 ++++++++++++++-
superset/exceptions.py | 4 +
...0-34_e0f6f91c2055_create_user_favorite_table.py | 53 ++++++++
superset/tags/api.py | 151 +++++++++++++++++++++
superset/tags/models.py | 14 +-
tests/integration_tests/tags/api_tests.py | 85 ++++++++++++
tests/unit_tests/dao/tag_test.py | 146 ++++++++++++++++++++
9 files changed, 593 insertions(+), 4 deletions(-)
diff --git a/superset-frontend/src/pages/Tags/index.tsx b/superset-frontend/src/pages/Tags/index.tsx
index 41ab3ea5ae..8087b3f4c6 100644
--- a/superset-frontend/src/pages/Tags/index.tsx
+++ b/superset-frontend/src/pages/Tags/index.tsx
@@ -24,7 +24,7 @@ import {
createErrorHandler,
Actions,
} from 'src/views/CRUD/utils';
-import { useListViewResource } from 'src/views/CRUD/hooks';
+import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
import ListView, {
@@ -42,6 +42,7 @@ import { deleteTags } from 'src/features/tags/tags';
import { Tag as AntdTag } from 'antd';
import { Tag } from 'src/views/CRUD/types';
import TagCard from 'src/features/tags/TagCard';
+import FaveStar from 'src/components/FaveStar';
const emptyState = {
title: t('No Tags created'),
@@ -90,6 +91,13 @@ function TagList(props: TagListProps) {
refreshData,
} = useListViewResource<Tag>('tag', t('tag'), addDangerToast);
+ const tagIds = useMemo(() => tags.map(c => c.id), [tags]);
+ const [saveFavoriteStatus, favoriteStatus] = useFavoriteStatus(
+ 'tag',
+ tagIds,
+ addDangerToast,
+ );
+
// TODO: Fix usage of localStorage keying on the user id
const userKey = dangerouslyGetItemDoNotUse(userId?.toString(), null);
@@ -109,6 +117,25 @@ function TagList(props: TagListProps) {
const columns = useMemo(
() => [
+ {
+ Cell: ({
+ row: {
+ original: { id },
+ },
+ }: any) =>
+ userId && (
+ <FaveStar
+ itemId={id}
+ saveFaveStar={saveFavoriteStatus}
+ isStarred={favoriteStatus[id]}
+ />
+ ),
+ Header: '',
+ id: 'id',
+ disableSortBy: true,
+ size: 'xs',
+ hidden: !userId,
+ },
{
Cell: ({
row: {
diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts
index b53731754f..b539ca126f 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -564,10 +564,15 @@ const favoriteApis = {
method: 'GET',
endpoint: '/api/v1/dashboard/favorite_status/',
}),
+ tag: makeApi<Array<string | number>, FavoriteStatusResponse>({
+ requestType: 'rison',
+ method: 'GET',
+ endpoint: '/api/v1/tag/favorite_status/',
+ }),
};
export function useFavoriteStatus(
- type: 'chart' | 'dashboard',
+ type: 'chart' | 'dashboard' | 'tag',
ids: Array<string | number>,
handleErrorMsg: (message: string) => void,
) {
diff --git a/superset/daos/tag.py b/superset/daos/tag.py
index 90b0134ca7..8e4437f49d 100644
--- a/superset/daos/tag.py
+++ b/superset/daos/tag.py
@@ -18,15 +18,26 @@ import logging
from operator import and_
from typing import Any, Optional
+from flask import g
from sqlalchemy.exc import SQLAlchemyError
from superset.daos.base import BaseDAO
from superset.daos.exceptions import DAOCreateFailedError, DAODeleteFailedError
+from superset.exceptions import MissingUserContextException
from superset.extensions import db
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
-from superset.tags.models import get_tag, ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.tags.commands.exceptions import TagNotFoundError
+from superset.tags.models import (
+ get_tag,
+ ObjectTypes,
+ Tag,
+ TaggedObject,
+ TagTypes,
+ user_favorite_tag_table,
+)
+from superset.utils.core import get_user_id
logger = logging.getLogger(__name__)
@@ -257,3 +268,98 @@ class TagDAO(BaseDAO[Tag]):
for obj in saved_queries
)
return results
+
+ @staticmethod
+ def favorite_tag_by_id_for_current_user( # pylint: disable=invalid-name
+ tag_id: int,
+ ) -> None:
+ """
+ Marks a specific tag as a favorite for the current user.
+ This function will find the tag by the provided id,
+ create a new UserFavoriteTag object that represents
+ the user's preference, add that object to the database
+ session, and commit the session. It uses the currently
+ authenticated user from the global 'g' object.
+ Args:
+ tag_id: The id of the tag that is to be marked as
+ favorite.
+ Raises:
+ Any exceptions raised by the find_by_id function,
+ the UserFavoriteTag constructor, or the database session's
+ add and commit methods will propagate up to the caller.
+ Returns:
+ None.
+ """
+ tag = TagDAO.find_by_id(tag_id)
+ user = g.user
+
+ if not user:
+ raise MissingUserContextException(message="User doesn't exist")
+ if not tag:
+ raise TagNotFoundError()
+
+ tag.users_favorited.append(user)
+ db.session.commit()
+
+ @staticmethod
+ def remove_user_favorite_tag(tag_id: int) -> None:
+ """
+ Removes a tag from the current user's favorite tags.
+
+ This function will find the tag by the provided id and remove the tag
+ from the user's list of favorite tags. It uses the currently authenticated
+ user from the global 'g' object.
+
+ Args:
+ tag_id: The id of the tag that is to be removed from the favorite tags.
+
+ Raises:
+ Any exceptions raised by the find_by_id function, the database session's
+ commit method will propagate up to the caller.
+
+ Returns:
+ None.
+ """
+ tag = TagDAO.find_by_id(tag_id)
+ user = g.user
+
+ if not user:
+ raise MissingUserContextException(message="User doesn't exist")
+ if not tag:
+ raise TagNotFoundError()
+
+ tag.users_favorited.remove(user)
+
+ # Commit to save the changes
+ db.session.commit()
+
+ @staticmethod
+ def favorited_ids(tags: list[Tag]) -> list[int]:
+ """
+ Returns the IDs of tags that the current user has favorited.
+
+ This function takes in a list of Tag objects, extracts their IDs, and checks
+ which of these IDs exist in the user_favorite_tag_table for the current user.
+ The function returns a list of these favorited tag IDs.
+
+ Args:
+ tags (list[Tag]): A list of Tag objects.
+
+ Returns:
+ list[Any]: A list of IDs corresponding to the tags that are favorited by
+ the current user.
+
+ Example:
+ favorited_ids([tag1, tag2, tag3])
+ Output: [tag_id1, tag_id3] # if the current user has favorited tag1 and tag3
+ """
+ ids = [tag.id for tag in tags]
+ return [
+ star.tag_id
+ for star in db.session.query(user_favorite_tag_table.c.tag_id)
+ .filter(
+ user_favorite_tag_table.c.tag_id.in_(ids),
+ user_favorite_tag_table.c.user_id == get_user_id(),
+ )
+ .all()
+ ]
diff --git a/superset/exceptions.py b/superset/exceptions.py
index 018d1b6dfb..5f1df73c86 100644
--- a/superset/exceptions.py
+++ b/superset/exceptions.py
@@ -200,6 +200,10 @@ class DatabaseNotFound(SupersetException):
status = 400
+class MissingUserContextException(SupersetException):
+ status = 422
+
+
class QueryObjectValidationError(SupersetException):
status = 400
diff --git a/superset/migrations/versions/2023-07-12_20-34_e0f6f91c2055_create_user_favorite_table.py b/superset/migrations/versions/2023-07-12_20-34_e0f6f91c2055_create_user_favorite_table.py
new file mode 100644
index 0000000000..5a519bae1a
--- /dev/null
+++ b/superset/migrations/versions/2023-07-12_20-34_e0f6f91c2055_create_user_favorite_table.py
@@ -0,0 +1,53 @@
+# 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.
+"""create_user_favorite_table
+
+Revision ID: e0f6f91c2055
+Revises: bf646a0c1501
+Create Date: 2023-07-12 20:34:57.553981
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "e0f6f91c2055"
+down_revision = "bf646a0c1501"
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "user_favorite_tag",
+ sa.Column("user_id", sa.Integer(), nullable=False),
+ sa.Column("tag_id", sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["tag_id"],
+ ["tag.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["user_id"],
+ ["ab_user.id"],
+ ),
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ op.drop_table("user_favorite_tag")
diff --git a/superset/tags/api.py b/superset/tags/api.py
index f9aa7f7be9..21791a6ef0 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -23,6 +23,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.daos.tag import TagDAO
+from superset.exceptions import MissingUserContextException
from superset.extensions import event_logger
from superset.tags.commands.create import CreateCustomTagCommand
from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
@@ -61,6 +62,9 @@ class TagRestApi(BaseSupersetModelRestApi):
"get_all_objects",
"add_objects",
"delete_object",
+ "add_favorite",
+ "remove_favorite",
+ "favorite_status",
}
resource_name = "tag"
@@ -384,3 +388,150 @@ class TagRestApi(BaseSupersetModelRestApi):
exc_info=True,
)
return self.response_422(message=str(ex))
+
+ @expose("/favorite_status/", methods=("GET",))
+ @protect()
+ @safe
+ @statsd_metrics
+ @rison({"type": "array", "items": {"type": "integer"}})
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
+ f".favorite_status",
+ log_to_statsd=False,
+ )
+ def favorite_status(self, **kwargs: Any) -> Response:
+ """Favorite Stars for Dashboards
+ ---
+ get:
+ description: >-
+ Check favorited dashboards for current user
+ parameters:
+ - in: query
+ name: q
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/get_fav_star_ids_schema'
+ responses:
+ 200:
+ description:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GetFavStarIdsSchema"
+ 400:
+ $ref: '#/components/responses/400'
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ try:
+ requested_ids = kwargs["rison"]
+ tags = TagDAO.find_by_ids(requested_ids)
+ users_favorited_tags = TagDAO.favorited_ids(tags)
+ res = [
+ {"id": request_id, "value": request_id in users_favorited_tags}
+ for request_id in requested_ids
+ ]
+ return self.response(200, result=res)
+ except TagNotFoundError:
+ return self.response_404()
+ except MissingUserContextException as ex:
+ return self.response_422(message=str(ex))
+
+ @expose("/<pk>/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 tag as favorite
+ ---
+ post:
+ description: >-
+ Marks the tag as favorite for the current user
+ parameters:
+ - in: path
+ schema:
+ type: integer
+ name: pk
+ responses:
+ 200:
+ description: Tag added to favorites
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ result:
+ type: object
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ 422:
+ $ref: '#/components/responses/422'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ try:
+ TagDAO.favorite_tag_by_id_for_current_user(pk)
+ return self.response(200, result="OK")
+ except TagNotFoundError:
+ return self.response_404()
+ except MissingUserContextException as ex:
+ return self.response_422(message=str(ex))
+
+ @expose("/<pk>/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 tag from the user favorite list
+ ---
+ delete:
+ description: >-
+ Remove the tag from the user favorite list
+ parameters:
+ - in: path
+ schema:
+ type: integer
+ name: pk
+ responses:
+ 200:
+ description: Tag removed from favorites
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ result:
+ type: object
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ 422:
+ $ref: '#/components/responses/422'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ try:
+ TagDAO.remove_user_favorite_tag(pk)
+ return self.response(200, result="OK")
+ except TagNotFoundError:
+ return self.response_404()
+ except MissingUserContextException as ex:
+ return self.response_422(message=str(ex))
diff --git a/superset/tags/models.py b/superset/tags/models.py
index 79022b7e6d..7e350061fa 100644
--- a/superset/tags/models.py
+++ b/superset/tags/models.py
@@ -20,11 +20,12 @@ import enum
from typing import TYPE_CHECKING
from flask_appbuilder import Model
-from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text
+from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Table, Text
from sqlalchemy.engine.base import Connection
from sqlalchemy.orm import relationship, Session, sessionmaker
from sqlalchemy.orm.mapper import Mapper
+from superset import security_manager
from superset.models.helpers import AuditMixinNullable
if TYPE_CHECKING:
@@ -36,6 +37,13 @@ if TYPE_CHECKING:
Session = sessionmaker(autoflush=False)
+user_favorite_tag_table = Table(
+ "user_favorite_tag",
+ Model.metadata, # pylint: disable=no-member
+ Column("user_id", Integer, ForeignKey("ab_user.id")),
+ Column("tag_id", Integer, ForeignKey("tag.id")),
+)
+
class TagTypes(enum.Enum):
@@ -82,6 +90,10 @@ class Tag(Model, AuditMixinNullable):
"TaggedObject", back_populates="tag", overlaps="objects,tags"
)
+ users_favorited = relationship(
+ security_manager.user_model, secondary=user_favorite_tag_table
+ )
+
class TaggedObject(Model, AuditMixinNullable):
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index b047388a68..d1231db97e 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -18,12 +18,17 @@
"""Unit tests for Superset"""
import json
+from flask import g
import pytest
import prison
from sqlalchemy.sql import func
+from sqlalchemy import and_
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
+from superset.tags.models import user_favorite_tag_table
+from unittest.mock import patch
+
import tests.integration_tests.test_app
from superset import db, security_manager
@@ -372,3 +377,83 @@ class TestTagApi(SupersetTestCase):
# check that tags are all gone
tags = db.session.query(Tag).filter(Tag.name.in_(example_tag_names))
self.assertEqual(tags.count(), 0)
+
+ @pytest.mark.usefixtures("create_tags")
+ def test_delete_favorite_tag(self):
+ self.login(username="admin")
+ user_id = self.get_user(username="admin").get_id()
+ tag = db.session.query(Tag).first()
+ uri = f"api/v1/tag/{tag.id}/favorites/"
+ tag = db.session.query(Tag).first()
+ rv = self.client.post(uri, follow_redirects=True)
+
+ self.assertEqual(rv.status_code, 200)
+ from sqlalchemy import and_
+ from superset.tags.models import user_favorite_tag_table
+ from flask import g
+
+ association_row = (
+ db.session.query(user_favorite_tag_table)
+ .filter(
+ and_(
+ user_favorite_tag_table.c.tag_id == tag.id,
+ user_favorite_tag_table.c.user_id == user_id,
+ )
+ )
+ .one_or_none()
+ )
+
+ assert association_row is not None
+
+ uri = f"api/v1/tag/{tag.id}/favorites/"
+ rv = self.client.delete(uri, follow_redirects=True)
+
+ self.assertEqual(rv.status_code, 200)
+ association_row = (
+ db.session.query(user_favorite_tag_table)
+ .filter(
+ and_(
+ user_favorite_tag_table.c.tag_id == tag.id,
+ user_favorite_tag_table.c.user_id == user_id,
+ )
+ )
+ .one_or_none()
+ )
+
+ assert association_row is None
+
+ @pytest.mark.usefixtures("create_tags")
+ def test_add_tag_not_found(self):
+ self.login(username="admin")
+ uri = f"api/v1/tag/123/favorites/"
+ rv = self.client.post(uri, follow_redirects=True)
+
+ self.assertEqual(rv.status_code, 404)
+
+ @pytest.mark.usefixtures("create_tags")
+ def test_delete_favorite_tag_not_found(self):
+ self.login(username="admin")
+ uri = f"api/v1/tag/123/favorites/"
+ rv = self.client.delete(uri, follow_redirects=True)
+
+ self.assertEqual(rv.status_code, 404)
+
+ @pytest.mark.usefixtures("create_tags")
+ @patch("superset.daos.tag.g")
+ def test_add_tag_user_not_found(self, flask_g):
+ self.login(username="admin")
+ flask_g.user = None
+ uri = f"api/v1/tag/123/favorites/"
+ rv = self.client.post(uri, follow_redirects=True)
+
+ self.assertEqual(rv.status_code, 422)
+
+ @pytest.mark.usefixtures("create_tags")
+ @patch("superset.daos.tag.g")
+ def test_delete_favorite_tag_user_not_found(self, flask_g):
+ self.login(username="admin")
+ flask_g.user = None
+ uri = f"api/v1/tag/123/favorites/"
+ rv = self.client.delete(uri, follow_redirects=True)
+
+ self.assertEqual(rv.status_code, 422)
diff --git a/tests/unit_tests/dao/tag_test.py b/tests/unit_tests/dao/tag_test.py
new file mode 100644
index 0000000000..3f5666d2b5
--- /dev/null
+++ b/tests/unit_tests/dao/tag_test.py
@@ -0,0 +1,146 @@
+# 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 collections.abc import Iterator
+
+import pytest
+from sqlalchemy.orm.session import Session
+
+
+def test_user_favorite_tag(mocker):
+ from superset.daos.tag import TagDAO
+
+ # Mock the behavior of TagDAO and g
+ mock_session = mocker.patch("superset.daos.tag.db.session")
+ mock_TagDAO = mocker.patch(
+ "superset.daos.tag.TagDAO"
+ ) # Replace with the actual path to TagDAO
+ mock_TagDAO.find_by_id.return_value = mocker.MagicMock(users_favorited=[])
+
+ mock_g = mocker.patch("superset.daos.tag.g") # Replace with the actual path to g
+ mock_g.user = mocker.MagicMock()
+
+ # Call the function with a test tag_id
+ TagDAO.favorite_tag_by_id_for_current_user(123)
+
+ # Check that find_by_id was called with the right argument
+ mock_TagDAO.find_by_id.assert_called_once_with(123)
+
+ # Check that users_favorited was updated correctly
+ assert mock_TagDAO.find_by_id().users_favorited == [mock_g.user]
+
+ mock_session.commit.assert_called_once()
+
+
+def test_remove_user_favorite_tag(mocker):
+ from superset.daos.tag import TagDAO
+
+ # Mock the behavior of TagDAO and g
+ mock_session = mocker.patch("superset.daos.tag.db.session")
+ mock_TagDAO = mocker.patch("superset.daos.tag.TagDAO")
+ mock_tag = mocker.MagicMock(users_favorited=[])
+ mock_TagDAO.find_by_id.return_value = mock_tag
+
+ mock_g = mocker.patch("superset.daos.tag.g") # Replace with the actual path to g
+ mock_user = mocker.MagicMock()
+ mock_g.user = mock_user
+
+ # Append the mock user to the tag's list of favorited users
+ mock_tag.users_favorited.append(mock_user)
+
+ # Call the function with a test tag_id
+ TagDAO.remove_user_favorite_tag(123)
+
+ # Check that find_by_id was called with the right argument
+ mock_TagDAO.find_by_id.assert_called_once_with(123)
+
+ # Check that users_favorited no longer contains the user
+ assert mock_user not in mock_tag.users_favorited
+
+ # Check that the session was committed
+ mock_session.commit.assert_called_once()
+
+
+def test_remove_user_favorite_tag_no_user(mocker):
+ from superset.daos.tag import TagDAO
+ from superset.exceptions import MissingUserContextException
+
+ # Mock the behavior of TagDAO and g
+ mock_session = mocker.patch("superset.daos.tag.db.session")
+ mock_TagDAO = mocker.patch("superset.daos.tag.TagDAO")
+ mock_tag = mocker.MagicMock(users_favorited=[])
+ mock_TagDAO.find_by_id.return_value = mock_tag
+
+ mock_g = mocker.patch("superset.daos.tag.g") # Replace with the actual path to g
+
+ # Test with no user
+ mock_g.user = None
+ with pytest.raises(MissingUserContextException):
+ TagDAO.remove_user_favorite_tag(1)
+
+
+def test_remove_user_favorite_tag_exc_raise(mocker):
+ from superset.daos.tag import TagDAO
+ from superset.exceptions import MissingUserContextException
+
+ # Mock the behavior of TagDAO and g
+ mock_session = mocker.patch("superset.daos.tag.db.session")
+ mock_TagDAO = mocker.patch("superset.daos.tag.TagDAO")
+ mock_tag = mocker.MagicMock(users_favorited=[])
+ mock_TagDAO.find_by_id.return_value = mock_tag
+
+ mock_g = mocker.patch("superset.daos.tag.g") # Replace with the actual path to g
+
+ # Test that exception is raised when commit fails
+ mock_session.commit.side_effect = Exception("DB Error")
+ with pytest.raises(Exception):
+ TagDAO.remove_user_favorite_tag(1)
+
+
+def test_user_favorite_tag_no_user(mocker):
+ from superset.daos.tag import TagDAO
+ from superset.exceptions import MissingUserContextException
+
+ # Mock the behavior of TagDAO and g
+ mock_session = mocker.patch("superset.daos.tag.db.session")
+ mock_TagDAO = mocker.patch("superset.daos.tag.TagDAO")
+ mock_tag = mocker.MagicMock(users_favorited=[])
+ mock_TagDAO.find_by_id.return_value = mock_tag
+
+ mock_g = mocker.patch("superset.daos.tag.g") # Replace with the actual path to g
+
+ # Test with no user
+ mock_g.user = None
+ with pytest.raises(MissingUserContextException):
+ TagDAO.favorite_tag_by_id_for_current_user(1)
+
+
+def test_user_favorite_tag_exc_raise(mocker):
+ from superset.daos.tag import TagDAO
+ from superset.exceptions import MissingUserContextException
+
+ # Mock the behavior of TagDAO and g
+ mock_session = mocker.patch("superset.daos.tag.db.session")
+ mock_TagDAO = mocker.patch("superset.daos.tag.TagDAO")
+ mock_tag = mocker.MagicMock(users_favorited=[])
+ mock_TagDAO.find_by_id.return_value = mock_tag
+
+ mock_g = mocker.patch("superset.daos.tag.g") # Replace with the actual path to g
+
+ # Test that exception is raised when commit fails
+ mock_session.commit.side_effect = Exception("DB Error")
+ with pytest.raises(Exception):
+ TagDAO.remove_user_favorite_tag(1)