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/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 136f90f  feat: SavedQuery REST API for bulk delete and new API fields 
(#10793)
136f90f is described below

commit 136f90f87c1d2e8f681090b044fe11f11e5652c7
Author: Daniel Vaz Gaspar <[email protected]>
AuthorDate: Fri Sep 11 09:36:47 2020 +0100

    feat: SavedQuery REST API for bulk delete and new API fields (#10793)
    
    * feat: SavedQuery REST API for bulk delete
    
    * fix, singular msg and test
    
    * remove 403 from OpenAPI spec
    
    * filter by current user using created_by add sql_tables field
    
    * fixes for new filter, add user field on pre_update, pre_add
    
    * add lru cache to property
    
    * Revert "add lru cache to property"
    
    This reverts commit ad0d9428
---
 superset/app.py                                    |   2 +-
 superset/models/sql_lab.py                         |   8 +-
 .../{savedqueries => saved_queries}/__init__.py    |   0
 superset/queries/saved_queries/api.py              | 168 ++++++++++++++++++++
 .../commands}/__init__.py                          |   0
 .../queries/saved_queries/commands/bulk_delete.py  |  53 +++++++
 .../commands/exceptions.py}                        |  11 ++
 superset/queries/saved_queries/dao.py              |  47 ++++++
 .../__init__.py => saved_queries/filters.py}       |  19 +++
 .../__init__.py => saved_queries/schemas.py}       |  17 ++
 superset/queries/savedqueries/api.py               | 103 -------------
 tests/queries/saved_queries/api_tests.py           | 171 +++++++++++++++++----
 12 files changed, 463 insertions(+), 136 deletions(-)

diff --git a/superset/app.py b/superset/app.py
index e073f45..2f41d37 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -143,7 +143,7 @@ class SupersetAppInitializer:
         from superset.databases.api import DatabaseRestApi
         from superset.datasets.api import DatasetRestApi
         from superset.queries.api import QueryRestApi
-        from superset.queries.savedqueries.api import SavedQueryRestApi
+        from superset.queries.saved_queries.api import SavedQueryRestApi
         from superset.views.access_requests import AccessRequestsModelView
         from superset.views.alerts import (
             AlertLogModelView,
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index 414dec4..c101b77 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -17,7 +17,7 @@
 """A collection of ORM sqlalchemy models for SQL Lab"""
 import re
 from datetime import datetime
-from typing import Any, Dict
+from typing import Any, Dict, List
 
 import simplejson as json
 import sqlalchemy as sqla
@@ -39,7 +39,7 @@ from sqlalchemy.orm import backref, relationship
 from superset import security_manager
 from superset.models.helpers import AuditMixinNullable, ExtraJSONMixin
 from superset.models.tags import QueryUpdater
-from superset.sql_parse import CtasMethod
+from superset.sql_parse import CtasMethod, ParsedQuery, Table
 from superset.utils.core import QueryStatus, user_label
 
 
@@ -203,6 +203,10 @@ class SavedQuery(Model, AuditMixinNullable, 
ExtraJSONMixin):
     def url(self) -> str:
         return "/superset/sqllab?savedQueryId={0}".format(self.id)
 
+    @property
+    def sql_tables(self) -> List[Table]:
+        return list(ParsedQuery(self.sql).tables)
+
 
 class TabState(Model, AuditMixinNullable, ExtraJSONMixin):
 
diff --git a/superset/queries/savedqueries/__init__.py 
b/superset/queries/saved_queries/__init__.py
similarity index 100%
copy from superset/queries/savedqueries/__init__.py
copy to superset/queries/saved_queries/__init__.py
diff --git a/superset/queries/saved_queries/api.py 
b/superset/queries/saved_queries/api.py
new file mode 100644
index 0000000..81204a8
--- /dev/null
+++ b/superset/queries/saved_queries/api.py
@@ -0,0 +1,168 @@
+# 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 logging
+from typing import Any
+
+from flask import g, Response
+from flask_appbuilder.api import expose, protect, rison, safe
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from flask_babel import ngettext
+
+from superset.constants import RouteMethod
+from superset.databases.filters import DatabaseFilter
+from superset.models.sql_lab import SavedQuery
+from superset.queries.saved_queries.commands.bulk_delete import (
+    BulkDeleteSavedQueryCommand,
+)
+from superset.queries.saved_queries.commands.exceptions import (
+    SavedQueryBulkDeleteFailedError,
+    SavedQueryNotFoundError,
+)
+from superset.queries.saved_queries.filters import SavedQueryFilter
+from superset.queries.saved_queries.schemas import (
+    get_delete_ids_schema,
+    openapi_spec_methods_override,
+)
+from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class SavedQueryRestApi(BaseSupersetModelRestApi):
+    datamodel = SQLAInterface(SavedQuery)
+
+    include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
+        RouteMethod.RELATED,
+        RouteMethod.DISTINCT,
+        "bulk_delete",  # not using RouteMethod since locally defined
+    }
+    class_permission_name = "SavedQueryView"
+    resource_name = "saved_query"
+    allow_browser_login = True
+
+    base_filters = [["id", SavedQueryFilter, lambda: []]]
+
+    show_columns = [
+        "created_by.first_name",
+        "created_by.id",
+        "created_by.last_name",
+        "database.database_name",
+        "database.id",
+        "description",
+        "id",
+        "label",
+        "schema",
+        "sql",
+        "sql_tables",
+    ]
+    list_columns = [
+        "created_by.first_name",
+        "created_by.id",
+        "created_by.last_name",
+        "database.database_name",
+        "database.id",
+        "db_id",
+        "description",
+        "label",
+        "schema",
+        "sql",
+        "sql_tables",
+    ]
+    add_columns = ["db_id", "description", "label", "schema", "sql"]
+    edit_columns = add_columns
+    order_columns = [
+        "schema",
+        "label",
+        "description",
+        "sql",
+        "created_by.first_name",
+        "database.database_name",
+    ]
+
+    apispec_parameter_schemas = {
+        "get_delete_ids_schema": get_delete_ids_schema,
+    }
+    openapi_spec_tag = "Queries"
+    openapi_spec_methods = openapi_spec_methods_override
+
+    related_field_filters = {
+        "database": "database_name",
+    }
+    filter_rel_fields = {"database": [["id", DatabaseFilter, lambda: []]]}
+    allowed_rel_fields = {"database"}
+    allowed_distinct_fields = {"schema"}
+
+    def pre_add(self, item: SavedQuery) -> None:
+        item.user = g.user
+
+    def pre_update(self, item: SavedQuery) -> None:
+        self.pre_add(item)
+
+    @expose("/", methods=["DELETE"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @rison(get_delete_ids_schema)
+    def bulk_delete(
+        self, **kwargs: Any
+    ) -> Response:  # pylint: disable=arguments-differ
+        """Delete bulk Saved Queries
+        ---
+        delete:
+          description: >-
+            Deletes multiple saved queries in a bulk operation.
+          parameters:
+          - in: query
+            name: q
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/get_delete_ids_schema'
+          responses:
+            200:
+              description: Saved queries bulk delete
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            422:
+              $ref: '#/components/responses/422'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        item_ids = kwargs["rison"]
+        try:
+            BulkDeleteSavedQueryCommand(g.user, item_ids).run()
+            return self.response(
+                200,
+                message=ngettext(
+                    "Deleted %(num)d saved query",
+                    "Deleted %(num)d saved queries",
+                    num=len(item_ids),
+                ),
+            )
+        except SavedQueryNotFoundError:
+            return self.response_404()
+        except SavedQueryBulkDeleteFailedError as ex:
+            return self.response_422(message=str(ex))
diff --git a/superset/queries/savedqueries/__init__.py 
b/superset/queries/saved_queries/commands/__init__.py
similarity index 100%
copy from superset/queries/savedqueries/__init__.py
copy to superset/queries/saved_queries/commands/__init__.py
diff --git a/superset/queries/saved_queries/commands/bulk_delete.py 
b/superset/queries/saved_queries/commands/bulk_delete.py
new file mode 100644
index 0000000..cf02144
--- /dev/null
+++ b/superset/queries/saved_queries/commands/bulk_delete.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.
+import logging
+from typing import List, Optional
+
+from flask_appbuilder.security.sqla.models import User
+
+from superset.commands.base import BaseCommand
+from superset.dao.exceptions import DAODeleteFailedError
+from superset.models.dashboard import Dashboard
+from superset.queries.saved_queries.commands.exceptions import (
+    SavedQueryBulkDeleteFailedError,
+    SavedQueryNotFoundError,
+)
+from superset.queries.saved_queries.dao import SavedQueryDAO
+
+logger = logging.getLogger(__name__)
+
+
+class BulkDeleteSavedQueryCommand(BaseCommand):
+    def __init__(self, user: User, model_ids: List[int]):
+        self._actor = user
+        self._model_ids = model_ids
+        self._models: Optional[List[Dashboard]] = None
+
+    def run(self) -> None:
+        self.validate()
+        try:
+            SavedQueryDAO.bulk_delete(self._models)
+            return None
+        except DAODeleteFailedError as ex:
+            logger.exception(ex.exception)
+            raise SavedQueryBulkDeleteFailedError()
+
+    def validate(self) -> None:
+        # Validate/populate model exists
+        self._models = SavedQueryDAO.find_by_ids(self._model_ids)
+        if not self._models or len(self._models) != len(self._model_ids):
+            raise SavedQueryNotFoundError()
diff --git a/superset/queries/savedqueries/__init__.py 
b/superset/queries/saved_queries/commands/exceptions.py
similarity index 70%
copy from superset/queries/savedqueries/__init__.py
copy to superset/queries/saved_queries/commands/exceptions.py
index 13a8339..0e03dc7 100644
--- a/superset/queries/savedqueries/__init__.py
+++ b/superset/queries/saved_queries/commands/exceptions.py
@@ -14,3 +14,14 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+from flask_babel import lazy_gettext as _
+
+from superset.commands.exceptions import CommandException, DeleteFailedError
+
+
+class SavedQueryBulkDeleteFailedError(DeleteFailedError):
+    message = _("Saved queries could not be deleted.")
+
+
+class SavedQueryNotFoundError(CommandException):
+    message = _("Saved query not found.")
diff --git a/superset/queries/saved_queries/dao.py 
b/superset/queries/saved_queries/dao.py
new file mode 100644
index 0000000..cd20fe6
--- /dev/null
+++ b/superset/queries/saved_queries/dao.py
@@ -0,0 +1,47 @@
+# 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 logging
+from typing import List, Optional
+
+from sqlalchemy.exc import SQLAlchemyError
+
+from superset.dao.base import BaseDAO
+from superset.dao.exceptions import DAODeleteFailedError
+from superset.extensions import db
+from superset.models.sql_lab import SavedQuery
+from superset.queries.saved_queries.filters import SavedQueryFilter
+
+logger = logging.getLogger(__name__)
+
+
+class SavedQueryDAO(BaseDAO):
+    model_cls = SavedQuery
+    base_filter = SavedQueryFilter
+
+    @staticmethod
+    def bulk_delete(models: Optional[List[SavedQuery]], commit: bool = True) 
-> None:
+        item_ids = [model.id for model in models] if models else []
+        try:
+            
db.session.query(SavedQuery).filter(SavedQuery.id.in_(item_ids)).delete(
+                synchronize_session="fetch"
+            )
+            if commit:
+                db.session.commit()
+        except SQLAlchemyError:
+            if commit:
+                db.session.rollback()
+            raise DAODeleteFailedError()
diff --git a/superset/queries/savedqueries/__init__.py 
b/superset/queries/saved_queries/filters.py
similarity index 57%
copy from superset/queries/savedqueries/__init__.py
copy to superset/queries/saved_queries/filters.py
index 13a8339..498a061 100644
--- a/superset/queries/savedqueries/__init__.py
+++ b/superset/queries/saved_queries/filters.py
@@ -14,3 +14,22 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+from typing import Any
+
+from flask import g
+from flask_sqlalchemy import BaseQuery
+
+from superset.models.sql_lab import SavedQuery
+from superset.views.base import BaseFilter
+
+
+class SavedQueryFilter(BaseFilter):  # pylint: disable=too-few-public-methods
+    def apply(self, query: BaseQuery, value: Any) -> BaseQuery:
+        """
+        Filter saved queries to only those created by current user.
+
+        :returns: flask-sqlalchemy query
+        """
+        return query.filter(
+            SavedQuery.created_by == g.user  # pylint: 
disable=comparison-with-callable
+        )
diff --git a/superset/queries/savedqueries/__init__.py 
b/superset/queries/saved_queries/schemas.py
similarity index 55%
rename from superset/queries/savedqueries/__init__.py
rename to superset/queries/saved_queries/schemas.py
index 13a8339..d875e1a 100644
--- a/superset/queries/savedqueries/__init__.py
+++ b/superset/queries/saved_queries/schemas.py
@@ -14,3 +14,20 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+openapi_spec_methods_override = {
+    "get": {"get": {"description": "Get a saved query",}},
+    "get_list": {
+        "get": {
+            "description": "Get a list of saved queries, use Rison or JSON "
+            "query parameters for filtering, sorting,"
+            " pagination and for selecting specific"
+            " columns and metadata.",
+        }
+    },
+    "post": {"post": {"description": "Create a saved query"}},
+    "put": {"put": {"description": "Update a saved query"}},
+    "delete": {"delete": {"description": "Delete saved query"}},
+}
+
+get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
diff --git a/superset/queries/savedqueries/api.py 
b/superset/queries/savedqueries/api.py
deleted file mode 100644
index 0b62d37..0000000
--- a/superset/queries/savedqueries/api.py
+++ /dev/null
@@ -1,103 +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 logging
-
-from flask_appbuilder.models.sqla.interface import SQLAInterface
-
-from superset.constants import RouteMethod
-from superset.databases.filters import DatabaseFilter
-from superset.models.sql_lab import SavedQuery
-from superset.views.base_api import BaseSupersetModelRestApi
-
-logger = logging.getLogger(__name__)
-
-
-class SavedQueryRestApi(BaseSupersetModelRestApi):
-    datamodel = SQLAInterface(SavedQuery)
-
-    include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
-        RouteMethod.RELATED,
-        RouteMethod.DISTINCT,
-    }
-    class_permission_name = "SavedQueryView"
-    resource_name = "saved_query"
-    allow_browser_login = True
-    show_columns = [
-        "id",
-        "schema",
-        "label",
-        "description",
-        "sql",
-        "user.first_name",
-        "user.last_name",
-        "user.id",
-        "database.database_name",
-        "database.id",
-    ]
-    list_columns = [
-        "user_id",
-        "db_id",
-        "schema",
-        "label",
-        "description",
-        "sql",
-        "user.first_name",
-        "user.last_name",
-        "user.id",
-        "database.database_name",
-        "database.id",
-    ]
-    add_columns = [
-        "schema",
-        "label",
-        "description",
-        "sql",
-        "user_id",
-        "db_id",
-    ]
-    edit_columns = add_columns
-    order_columns = [
-        "schema",
-        "label",
-        "description",
-        "sql",
-        "user.first_name",
-        "database.database_name",
-    ]
-
-    openapi_spec_tag = "Queries"
-    openapi_spec_methods = {
-        "get": {"get": {"description": "Get a saved query",}},
-        "get_list": {
-            "get": {
-                "description": "Get a list of saved queries, use Rison or JSON 
"
-                "query parameters for filtering, sorting,"
-                " pagination and for selecting specific"
-                " columns and metadata.",
-            }
-        },
-        "post": {"post": {"description": "Create a saved query"}},
-        "put": {"put": {"description": "Update a saved query"}},
-        "delete": {"delete": {"description": "Delete saved query"}},
-    }
-
-    related_field_filters = {
-        "database": "database_name",
-    }
-    filter_rel_fields = {"database": [["id", DatabaseFilter, lambda: []]]}
-    allowed_rel_fields = {"database"}
-    allowed_distinct_fields = {"schema"}
diff --git a/tests/queries/saved_queries/api_tests.py 
b/tests/queries/saved_queries/api_tests.py
index 73d3730..b3ce625 100644
--- a/tests/queries/saved_queries/api_tests.py
+++ b/tests/queries/saved_queries/api_tests.py
@@ -32,52 +32,63 @@ from superset.utils.core import get_example_database
 from tests.base_tests import SupersetTestCase
 
 
+SAVED_QUERIES_FIXTURE_COUNT = 5
+
+
 class TestSavedQueryApi(SupersetTestCase):
     def insert_saved_query(
         self,
         label: str,
         sql: str,
         db_id: Optional[int] = None,
-        user_id: Optional[int] = None,
+        created_by=None,
         schema: Optional[str] = "",
     ) -> SavedQuery:
         database = None
-        user = None
         if db_id:
             database = db.session.query(Database).get(db_id)
-        if user_id:
-            user = db.session.query(security_manager.user_model).get(user_id)
         query = SavedQuery(
-            database=database, user=user, sql=sql, label=label, schema=schema
+            database=database,
+            created_by=created_by,
+            sql=sql,
+            label=label,
+            schema=schema,
         )
         db.session.add(query)
         db.session.commit()
         return query
 
     def insert_default_saved_query(
-        self, label: str = "saved1", schema: str = "schema1",
+        self, label: str = "saved1", schema: str = "schema1", username: str = 
"admin"
     ) -> SavedQuery:
-        admin = self.get_user("admin")
+        admin = self.get_user(username)
         example_db = get_example_database()
         return self.insert_saved_query(
             label,
             "SELECT col1, col2 from table1",
             db_id=example_db.id,
-            user_id=admin.id,
+            created_by=admin,
             schema=schema,
         )
 
     @pytest.fixture()
     def create_saved_queries(self):
         with self.create_app().app_context():
-            num_saved_queries = 5
             saved_queries = []
-            for cx in range(num_saved_queries):
+            for cx in range(SAVED_QUERIES_FIXTURE_COUNT - 1):
                 saved_queries.append(
                     self.insert_default_saved_query(
                         label=f"label{cx}", schema=f"schema{cx}"
                     )
                 )
+            saved_queries.append(
+                self.insert_default_saved_query(
+                    label=f"label{SAVED_QUERIES_FIXTURE_COUNT}",
+                    schema=f"schema{SAVED_QUERIES_FIXTURE_COUNT}",
+                    username="gamma",
+                )
+            )
+
             yield saved_queries
 
             # rollback changes
@@ -90,34 +101,55 @@ class TestSavedQueryApi(SupersetTestCase):
         """
         Saved Query API: Test get list saved query
         """
-        queries = db.session.query(SavedQuery).all()
+        admin = self.get_user("admin")
+        saved_queries = (
+            db.session.query(SavedQuery).filter(SavedQuery.created_by == 
admin).all()
+        )
 
         self.login(username="admin")
         uri = f"api/v1/saved_query/"
         rv = self.get_assert_metric(uri, "get_list")
         assert rv.status_code == 200
         data = json.loads(rv.data.decode("utf-8"))
-        assert data["count"] == len(queries)
+        assert data["count"] == len(saved_queries)
         expected_columns = [
-            "user_id",
+            "created_by",
+            "database",
             "db_id",
-            "schema",
-            "label",
             "description",
+            "label",
+            "schema",
             "sql",
-            "user",
-            "database",
+            "sql_tables",
         ]
         for expected_column in expected_columns:
             assert expected_column in data["result"][0]
 
     @pytest.mark.usefixtures("create_saved_queries")
+    def test_get_list_saved_query_gamma(self):
+        """
+        Saved Query API: Test get list saved query
+        """
+        gamma = self.get_user("gamma")
+        saved_queries = (
+            db.session.query(SavedQuery).filter(SavedQuery.created_by == 
gamma).all()
+        )
+
+        self.login(username="gamma")
+        uri = f"api/v1/saved_query/"
+        rv = self.get_assert_metric(uri, "get_list")
+        assert rv.status_code == 200
+        data = json.loads(rv.data.decode("utf-8"))
+        assert data["count"] == len(saved_queries)
+
+    @pytest.mark.usefixtures("create_saved_queries")
     def test_get_list_sort_saved_query(self):
         """
         Saved Query API: Test get list and sort saved query
         """
-        all_queries = (
-            db.session.query(SavedQuery).order_by(asc(SavedQuery.schema)).all()
+        admin = self.get_user("admin")
+        saved_queries = (
+            db.session.query(SavedQuery).filter(SavedQuery.created_by == 
admin).all()
         )
         self.login(username="admin")
         query_string = {"order_column": "schema", "order_direction": "asc"}
@@ -125,8 +157,8 @@ class TestSavedQueryApi(SupersetTestCase):
         rv = self.get_assert_metric(uri, "get_list")
         assert rv.status_code == 200
         data = json.loads(rv.data.decode("utf-8"))
-        assert data["count"] == len(all_queries)
-        for i, query in enumerate(all_queries):
+        assert data["count"] == len(saved_queries)
+        for i, query in enumerate(saved_queries):
             assert query.schema == data["result"][i]["schema"]
 
         query_string = {
@@ -137,7 +169,10 @@ class TestSavedQueryApi(SupersetTestCase):
         rv = self.get_assert_metric(uri, "get_list")
         assert rv.status_code == 200
 
-        query_string = {"order_column": "user.first_name", "order_direction": 
"asc"}
+        query_string = {
+            "order_column": "created_by.first_name",
+            "order_direction": "asc",
+        }
         uri = f"api/v1/saved_query/?q={prison.dumps(query_string)}"
         rv = self.get_assert_metric(uri, "get_list")
         assert rv.status_code == 200
@@ -202,14 +237,22 @@ class TestSavedQueryApi(SupersetTestCase):
         """
         SavedQuery API: Test distinct schemas
         """
+        admin = self.get_user("admin")
+        saved_queries = (
+            db.session.query(SavedQuery).filter(SavedQuery.created_by == 
admin).all()
+        )
+
         self.login(username="admin")
         uri = f"api/v1/saved_query/distinct/schema"
         rv = self.client.get(uri)
         assert rv.status_code == 200
         data = json.loads(rv.data.decode("utf-8"))
         expected_response = {
-            "count": 5,
-            "result": [{"text": f"schema{i}", "value": f"schema{i}"} for i in 
range(5)],
+            "count": len(saved_queries),
+            "result": [
+                {"text": f"schema{i}", "value": f"schema{i}"}
+                for i in range(len(saved_queries))
+            ],
         }
         assert data == expected_response
 
@@ -227,20 +270,25 @@ class TestSavedQueryApi(SupersetTestCase):
         """
         Saved Query API: Test get saved query
         """
-        query = (
+        saved_query = (
             db.session.query(SavedQuery).filter(SavedQuery.label == 
"label1").all()[0]
         )
         self.login(username="admin")
-        uri = f"api/v1/saved_query/{query.id}"
+        uri = f"api/v1/saved_query/{saved_query.id}"
         rv = self.get_assert_metric(uri, "get")
         assert rv.status_code == 200
 
         expected_result = {
-            "id": query.id,
-            "database": {"id": query.database.id, "database_name": "examples"},
+            "id": saved_query.id,
+            "database": {"id": saved_query.database.id, "database_name": 
"examples"},
             "description": None,
-            "user": {"first_name": "admin", "id": query.user_id, "last_name": 
"user"},
+            "created_by": {
+                "first_name": saved_query.created_by.first_name,
+                "id": saved_query.created_by.id,
+                "last_name": saved_query.created_by.last_name,
+            },
             "sql": "SELECT col1, col2 from table1",
+            "sql_tables": [{"catalog": None, "schema": None, "table": 
"table1"}],
             "schema": "schema1",
             "label": "label1",
         }
@@ -271,7 +319,6 @@ class TestSavedQueryApi(SupersetTestCase):
             "label": "label1",
             "description": "some description",
             "sql": "SELECT col1, col2 from table1",
-            "user_id": admin.id,
             "db_id": example_db.id,
         }
 
@@ -357,3 +404,67 @@ class TestSavedQueryApi(SupersetTestCase):
         uri = f"api/v1/saved_query/{max_id + 1}"
         rv = self.client.delete(uri)
         assert rv.status_code == 404
+
+    @pytest.mark.usefixtures("create_saved_queries")
+    def test_delete_bulk_saved_queries(self):
+        """
+        Saved Query API: Test delete bulk
+        """
+        admin = self.get_user("admin")
+        saved_queries = (
+            db.session.query(SavedQuery).filter(SavedQuery.created_by == 
admin).all()
+        )
+        saved_query_ids = [saved_query.id for saved_query in saved_queries]
+
+        self.login(username="admin")
+        uri = f"api/v1/saved_query/?q={prison.dumps(saved_query_ids)}"
+        rv = self.delete_assert_metric(uri, "bulk_delete")
+        assert rv.status_code == 200
+        response = json.loads(rv.data.decode("utf-8"))
+        expected_response = {"message": f"Deleted {len(saved_query_ids)} saved 
queries"}
+        assert response == expected_response
+        saved_queries = (
+            db.session.query(SavedQuery).filter(SavedQuery.created_by == 
admin).all()
+        )
+        assert saved_queries == []
+
+    @pytest.mark.usefixtures("create_saved_queries")
+    def test_delete_one_bulk_saved_queries(self):
+        """
+        Saved Query API: Test delete one in bulk
+        """
+        saved_query = db.session.query(SavedQuery).first()
+        saved_query_ids = [saved_query.id]
+
+        self.login(username="admin")
+        uri = f"api/v1/saved_query/?q={prison.dumps(saved_query_ids)}"
+        rv = self.delete_assert_metric(uri, "bulk_delete")
+        assert rv.status_code == 200
+        response = json.loads(rv.data.decode("utf-8"))
+        expected_response = {"message": f"Deleted {len(saved_query_ids)} saved 
query"}
+        assert response == expected_response
+        saved_query_ = db.session.query(SavedQuery).get(saved_query_ids[0])
+        assert saved_query_ is None
+
+    def test_delete_bulk_saved_query_bad_request(self):
+        """
+        Saved Query API: Test delete bulk bad request
+        """
+        saved_query_ids = [1, "a"]
+        self.login(username="admin")
+        uri = f"api/v1/saved_query/?q={prison.dumps(saved_query_ids)}"
+        rv = self.delete_assert_metric(uri, "bulk_delete")
+        assert rv.status_code == 400
+
+    @pytest.mark.usefixtures("create_saved_queries")
+    def test_delete_bulk_saved_query_not_found(self):
+        """
+        Saved Query API: Test delete bulk not found
+        """
+        max_id = db.session.query(func.max(SavedQuery.id)).scalar()
+
+        saved_query_ids = [max_id + 1, max_id + 2]
+        self.login(username="admin")
+        uri = f"api/v1/saved_query/?q={prison.dumps(saved_query_ids)}"
+        rv = self.delete_assert_metric(uri, "bulk_delete")
+        assert rv.status_code == 404

Reply via email to