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
This commit is contained in:
Daniel Vaz Gaspar 2020-09-11 09:36:47 +01:00 committed by GitHub
parent a3e2e65121
commit 136f90f87c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 529 additions and 138 deletions

View File

@ -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,

View File

@ -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):

View File

@ -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))

View File

@ -0,0 +1,16 @@
# 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.

View File

@ -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()

View File

@ -0,0 +1,27 @@
# 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_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.")

View File

@ -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()

View File

@ -0,0 +1,35 @@
# 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 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
)

View File

@ -0,0 +1,33 @@
# 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.
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"}}

View File

@ -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"}

View File

@ -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",
"db_id",
"schema",
"label",
"description",
"sql",
"user",
"created_by",
"database",
"db_id",
"description",
"label",
"schema",
"sql",
"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