mirror of
https://github.com/apache/superset.git
synced 2026-08-12 11:11:01 +00:00
chore(command): Condense delete/bulk-delete operations (#24607)
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
co-authored by
Michael S. Molina
parent
6ae6fc60f8
commit
a156816064
@@ -24,9 +24,6 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask_babel import ngettext
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from superset.annotation_layers.annotations.commands.bulk_delete import (
|
||||
BulkDeleteAnnotationCommand,
|
||||
)
|
||||
from superset.annotation_layers.annotations.commands.create import (
|
||||
CreateAnnotationCommand,
|
||||
)
|
||||
@@ -34,7 +31,6 @@ from superset.annotation_layers.annotations.commands.delete import (
|
||||
DeleteAnnotationCommand,
|
||||
)
|
||||
from superset.annotation_layers.annotations.commands.exceptions import (
|
||||
AnnotationBulkDeleteFailedError,
|
||||
AnnotationCreateFailedError,
|
||||
AnnotationDeleteFailedError,
|
||||
AnnotationInvalidError,
|
||||
@@ -438,7 +434,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
DeleteAnnotationCommand(annotation_id).run()
|
||||
DeleteAnnotationCommand([annotation_id]).run()
|
||||
return self.response(200, message="OK")
|
||||
except AnnotationNotFoundError:
|
||||
return self.response_404()
|
||||
@@ -495,7 +491,7 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteAnnotationCommand(item_ids).run()
|
||||
DeleteAnnotationCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -506,5 +502,5 @@ class AnnotationRestApi(BaseSupersetModelRestApi):
|
||||
)
|
||||
except AnnotationNotFoundError:
|
||||
return self.response_404()
|
||||
except AnnotationBulkDeleteFailedError as ex:
|
||||
except AnnotationDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
@@ -1,51 +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 typing import Optional
|
||||
|
||||
from superset.annotation_layers.annotations.commands.exceptions import (
|
||||
AnnotationBulkDeleteFailedError,
|
||||
AnnotationNotFoundError,
|
||||
)
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.daos.annotation import AnnotationDAO
|
||||
from superset.daos.exceptions import DAODeleteFailedError
|
||||
from superset.models.annotations import Annotation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteAnnotationCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[Annotation]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
AnnotationDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise AnnotationBulkDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._models = AnnotationDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise AnnotationNotFoundError()
|
||||
@@ -30,22 +30,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteAnnotationCommand(BaseCommand):
|
||||
def __init__(self, model_id: int):
|
||||
self._model_id = model_id
|
||||
self._model: Optional[Annotation] = None
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[Annotation]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._model
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
AnnotationDAO.delete(self._model)
|
||||
AnnotationDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise AnnotationDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._model = AnnotationDAO.find_by_id(self._model_id)
|
||||
if not self._model:
|
||||
self._models = AnnotationDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise AnnotationNotFoundError()
|
||||
|
||||
@@ -48,10 +48,6 @@ class AnnotationUniquenessValidationError(ValidationError):
|
||||
)
|
||||
|
||||
|
||||
class AnnotationBulkDeleteFailedError(DeleteFailedError):
|
||||
message = _("Annotations could not be deleted.")
|
||||
|
||||
|
||||
class AnnotationNotFoundError(CommandException):
|
||||
message = _("Annotation not found.")
|
||||
|
||||
@@ -68,5 +64,5 @@ class AnnotationUpdateFailedError(CreateFailedError):
|
||||
message = _("Annotation could not be updated.")
|
||||
|
||||
|
||||
class AnnotationDeleteFailedError(CommandException):
|
||||
message = _("Annotation delete failed.")
|
||||
class AnnotationDeleteFailedError(DeleteFailedError):
|
||||
message = _("Annotations could not be deleted.")
|
||||
|
||||
@@ -23,14 +23,9 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask_babel import ngettext
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from superset.annotation_layers.commands.bulk_delete import (
|
||||
BulkDeleteAnnotationLayerCommand,
|
||||
)
|
||||
from superset.annotation_layers.commands.create import CreateAnnotationLayerCommand
|
||||
from superset.annotation_layers.commands.delete import DeleteAnnotationLayerCommand
|
||||
from superset.annotation_layers.commands.exceptions import (
|
||||
AnnotationLayerBulkDeleteFailedError,
|
||||
AnnotationLayerBulkDeleteIntegrityError,
|
||||
AnnotationLayerCreateFailedError,
|
||||
AnnotationLayerDeleteFailedError,
|
||||
AnnotationLayerDeleteIntegrityError,
|
||||
@@ -151,7 +146,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
DeleteAnnotationLayerCommand(pk).run()
|
||||
DeleteAnnotationLayerCommand([pk]).run()
|
||||
return self.response(200, message="OK")
|
||||
except AnnotationLayerNotFoundError:
|
||||
return self.response_404()
|
||||
@@ -346,7 +341,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteAnnotationLayerCommand(item_ids).run()
|
||||
DeleteAnnotationLayerCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -357,7 +352,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
|
||||
)
|
||||
except AnnotationLayerNotFoundError:
|
||||
return self.response_404()
|
||||
except AnnotationLayerBulkDeleteIntegrityError as ex:
|
||||
except AnnotationLayerDeleteIntegrityError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
except AnnotationLayerBulkDeleteFailedError as ex:
|
||||
except AnnotationLayerDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
@@ -1,54 +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 typing import Optional
|
||||
|
||||
from superset.annotation_layers.commands.exceptions import (
|
||||
AnnotationLayerBulkDeleteFailedError,
|
||||
AnnotationLayerBulkDeleteIntegrityError,
|
||||
AnnotationLayerNotFoundError,
|
||||
)
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.daos.annotation import AnnotationLayerDAO
|
||||
from superset.daos.exceptions import DAODeleteFailedError
|
||||
from superset.models.annotations import AnnotationLayer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteAnnotationLayerCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[AnnotationLayer]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
AnnotationLayerDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise AnnotationLayerBulkDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._models = AnnotationLayerDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise AnnotationLayerNotFoundError()
|
||||
if AnnotationLayerDAO.has_annotations(self._model_ids):
|
||||
raise AnnotationLayerBulkDeleteIntegrityError()
|
||||
@@ -31,24 +31,24 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteAnnotationLayerCommand(BaseCommand):
|
||||
def __init__(self, model_id: int):
|
||||
self._model_id = model_id
|
||||
self._model: Optional[AnnotationLayer] = None
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[AnnotationLayer]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._model
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
AnnotationLayerDAO.delete(self._model)
|
||||
AnnotationLayerDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise AnnotationLayerDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._model = AnnotationLayerDAO.find_by_id(self._model_id)
|
||||
if not self._model:
|
||||
self._models = AnnotationLayerDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise AnnotationLayerNotFoundError()
|
||||
if AnnotationLayerDAO.has_annotations(self._model.id):
|
||||
if AnnotationLayerDAO.has_annotations(self._model_ids):
|
||||
raise AnnotationLayerDeleteIntegrityError()
|
||||
|
||||
@@ -29,10 +29,6 @@ class AnnotationLayerInvalidError(CommandInvalidError):
|
||||
message = _("Annotation layer parameters are invalid.")
|
||||
|
||||
|
||||
class AnnotationLayerBulkDeleteFailedError(DeleteFailedError):
|
||||
message = _("Annotation layer could not be deleted.")
|
||||
|
||||
|
||||
class AnnotationLayerCreateFailedError(CreateFailedError):
|
||||
message = _("Annotation layer could not be created.")
|
||||
|
||||
@@ -45,18 +41,14 @@ class AnnotationLayerNotFoundError(CommandException):
|
||||
message = _("Annotation layer not found.")
|
||||
|
||||
|
||||
class AnnotationLayerDeleteFailedError(CommandException):
|
||||
message = _("Annotation layer delete failed.")
|
||||
class AnnotationLayerDeleteFailedError(DeleteFailedError):
|
||||
message = _("Annotation layers could not be deleted.")
|
||||
|
||||
|
||||
class AnnotationLayerDeleteIntegrityError(CommandException):
|
||||
message = _("Annotation layer has associated annotations.")
|
||||
|
||||
|
||||
class AnnotationLayerBulkDeleteIntegrityError(CommandException):
|
||||
message = _("Annotation layer has associated annotations.")
|
||||
|
||||
|
||||
class AnnotationLayerNameUniquenessValidationError(ValidationError):
|
||||
"""
|
||||
Marshmallow validation error for annotation layer name already exists
|
||||
|
||||
@@ -32,11 +32,9 @@ from werkzeug.wrappers import Response as WerkzeugResponse
|
||||
from werkzeug.wsgi import FileWrapper
|
||||
|
||||
from superset import app, is_feature_enabled, thumbnail_cache
|
||||
from superset.charts.commands.bulk_delete import BulkDeleteChartCommand
|
||||
from superset.charts.commands.create import CreateChartCommand
|
||||
from superset.charts.commands.delete import DeleteChartCommand
|
||||
from superset.charts.commands.exceptions import (
|
||||
ChartBulkDeleteFailedError,
|
||||
ChartCreateFailedError,
|
||||
ChartDeleteFailedError,
|
||||
ChartForbiddenError,
|
||||
@@ -461,7 +459,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
DeleteChartCommand(pk).run()
|
||||
DeleteChartCommand([pk]).run()
|
||||
return self.response(200, message="OK")
|
||||
except ChartNotFoundError:
|
||||
return self.response_404()
|
||||
@@ -521,7 +519,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteChartCommand(item_ids).run()
|
||||
DeleteChartCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -532,7 +530,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
return self.response_404()
|
||||
except ChartForbiddenError:
|
||||
return self.response_403()
|
||||
except ChartBulkDeleteFailedError as ex:
|
||||
except ChartDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
@expose("/<pk>/cache_screenshot/", methods=("GET",))
|
||||
|
||||
@@ -1,70 +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 typing import Optional
|
||||
|
||||
from flask_babel import lazy_gettext as _
|
||||
|
||||
from superset import security_manager
|
||||
from superset.charts.commands.exceptions import (
|
||||
ChartBulkDeleteFailedError,
|
||||
ChartBulkDeleteFailedReportsExistError,
|
||||
ChartForbiddenError,
|
||||
ChartNotFoundError,
|
||||
)
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.commands.exceptions import DeleteFailedError
|
||||
from superset.daos.chart import ChartDAO
|
||||
from superset.daos.report import ReportScheduleDAO
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.models.slice import Slice
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteChartCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[Slice]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
ChartDAO.delete(self._models)
|
||||
except DeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise ChartBulkDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._models = ChartDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise ChartNotFoundError()
|
||||
# Check there are no associated ReportSchedules
|
||||
if reports := ReportScheduleDAO.find_by_chart_ids(self._model_ids):
|
||||
report_names = [report.name for report in reports]
|
||||
raise ChartBulkDeleteFailedReportsExistError(
|
||||
_("There are associated alerts or reports: %s" % ",".join(report_names))
|
||||
)
|
||||
# Check ownership
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise ChartForbiddenError() from ex
|
||||
@@ -15,7 +15,7 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
from typing import cast, Optional
|
||||
from typing import Optional
|
||||
|
||||
from flask_babel import lazy_gettext as _
|
||||
|
||||
@@ -38,33 +38,37 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteChartCommand(BaseCommand):
|
||||
def __init__(self, model_id: int):
|
||||
self._model_id = model_id
|
||||
self._model: Optional[Slice] = None
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[Slice]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
self._model = cast(Slice, self._model)
|
||||
assert self._models
|
||||
|
||||
for model_id in self._model_ids:
|
||||
Dashboard.clear_cache_for_slice(slice_id=model_id)
|
||||
|
||||
try:
|
||||
Dashboard.clear_cache_for_slice(slice_id=self._model_id)
|
||||
ChartDAO.delete(self._model)
|
||||
ChartDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise ChartDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._model = ChartDAO.find_by_id(self._model_id)
|
||||
if not self._model:
|
||||
self._models = ChartDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise ChartNotFoundError()
|
||||
# Check there are no associated ReportSchedules
|
||||
if reports := ReportScheduleDAO.find_by_chart_id(self._model_id):
|
||||
if reports := ReportScheduleDAO.find_by_chart_ids(self._model_ids):
|
||||
report_names = [report.name for report in reports]
|
||||
raise ChartDeleteFailedReportsExistError(
|
||||
_("There are associated alerts or reports: %s" % ",".join(report_names))
|
||||
)
|
||||
# Check ownership
|
||||
try:
|
||||
security_manager.raise_for_ownership(self._model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise ChartForbiddenError() from ex
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise ChartForbiddenError() from ex
|
||||
|
||||
@@ -120,7 +120,7 @@ class ChartUpdateFailedError(UpdateFailedError):
|
||||
|
||||
|
||||
class ChartDeleteFailedError(DeleteFailedError):
|
||||
message = _("Chart could not be deleted.")
|
||||
message = _("Charts could not be deleted.")
|
||||
|
||||
|
||||
class ChartDeleteFailedReportsExistError(ChartDeleteFailedError):
|
||||
@@ -135,10 +135,6 @@ class ChartForbiddenError(ForbiddenError):
|
||||
message = _("Changing this chart is forbidden")
|
||||
|
||||
|
||||
class ChartBulkDeleteFailedError(DeleteFailedError):
|
||||
message = _("Charts could not be deleted.")
|
||||
|
||||
|
||||
class ChartDataQueryFailedError(CommandException):
|
||||
pass
|
||||
|
||||
@@ -147,10 +143,6 @@ class ChartDataCacheLoadError(CommandException):
|
||||
pass
|
||||
|
||||
|
||||
class ChartBulkDeleteFailedReportsExistError(ChartBulkDeleteFailedError):
|
||||
message = _("There are associated alerts or reports")
|
||||
|
||||
|
||||
class ChartImportError(ImportFailedError):
|
||||
message = _("Import chart failed for an unknown reason")
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask_babel import ngettext
|
||||
|
||||
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
|
||||
from superset.css_templates.commands.bulk_delete import BulkDeleteCssTemplateCommand
|
||||
from superset.css_templates.commands.delete import DeleteCssTemplateCommand
|
||||
from superset.css_templates.commands.exceptions import (
|
||||
CssTemplateBulkDeleteFailedError,
|
||||
CssTemplateDeleteFailedError,
|
||||
CssTemplateNotFoundError,
|
||||
)
|
||||
from superset.css_templates.filters import CssTemplateAllTextFilter
|
||||
@@ -130,7 +130,7 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteCssTemplateCommand(item_ids).run()
|
||||
DeleteCssTemplateCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -141,5 +141,5 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
|
||||
)
|
||||
except CssTemplateNotFoundError:
|
||||
return self.response_404()
|
||||
except CssTemplateBulkDeleteFailedError as ex:
|
||||
except CssTemplateDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@ from typing import Optional
|
||||
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.css_templates.commands.exceptions import (
|
||||
CssTemplateBulkDeleteFailedError,
|
||||
CssTemplateDeleteFailedError,
|
||||
CssTemplateNotFoundError,
|
||||
)
|
||||
from superset.daos.css import CssTemplateDAO
|
||||
@@ -29,7 +29,7 @@ from superset.models.core import CssTemplate
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteCssTemplateCommand(BaseCommand):
|
||||
class DeleteCssTemplateCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[CssTemplate]] = None
|
||||
@@ -42,7 +42,7 @@ class BulkDeleteCssTemplateCommand(BaseCommand):
|
||||
CssTemplateDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise CssTemplateBulkDeleteFailedError() from ex
|
||||
raise CssTemplateDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
@@ -19,8 +19,8 @@ from flask_babel import lazy_gettext as _
|
||||
from superset.commands.exceptions import CommandException, DeleteFailedError
|
||||
|
||||
|
||||
class CssTemplateBulkDeleteFailedError(DeleteFailedError):
|
||||
message = _("CSS template could not be deleted.")
|
||||
class CssTemplateDeleteFailedError(DeleteFailedError):
|
||||
message = _("CSS templates could not be deleted.")
|
||||
|
||||
|
||||
class CssTemplateNotFoundError(CommandException):
|
||||
|
||||
@@ -39,12 +39,10 @@ from superset.commands.importers.exceptions import NoValidFilesFoundError
|
||||
from superset.commands.importers.v1.utils import get_contents_from_bundle
|
||||
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
|
||||
from superset.daos.dashboard import DashboardDAO, EmbeddedDashboardDAO
|
||||
from superset.dashboards.commands.bulk_delete import BulkDeleteDashboardCommand
|
||||
from superset.dashboards.commands.create import CreateDashboardCommand
|
||||
from superset.dashboards.commands.delete import DeleteDashboardCommand
|
||||
from superset.dashboards.commands.exceptions import (
|
||||
DashboardAccessDeniedError,
|
||||
DashboardBulkDeleteFailedError,
|
||||
DashboardCreateFailedError,
|
||||
DashboardDeleteFailedError,
|
||||
DashboardForbiddenError,
|
||||
@@ -674,7 +672,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
DeleteDashboardCommand(pk).run()
|
||||
DeleteDashboardCommand([pk]).run()
|
||||
return self.response(200, message="OK")
|
||||
except DashboardNotFoundError:
|
||||
return self.response_404()
|
||||
@@ -734,7 +732,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteDashboardCommand(item_ids).run()
|
||||
DeleteDashboardCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -747,7 +745,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
return self.response_404()
|
||||
except DashboardForbiddenError:
|
||||
return self.response_403()
|
||||
except DashboardBulkDeleteFailedError as ex:
|
||||
except DashboardDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
@expose("/export/", methods=("GET",))
|
||||
|
||||
@@ -1,70 +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 typing import Optional
|
||||
|
||||
from flask_babel import lazy_gettext as _
|
||||
|
||||
from superset import security_manager
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.commands.exceptions import DeleteFailedError
|
||||
from superset.daos.dashboard import DashboardDAO
|
||||
from superset.daos.report import ReportScheduleDAO
|
||||
from superset.dashboards.commands.exceptions import (
|
||||
DashboardBulkDeleteFailedError,
|
||||
DashboardBulkDeleteFailedReportsExistError,
|
||||
DashboardForbiddenError,
|
||||
DashboardNotFoundError,
|
||||
)
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.models.dashboard import Dashboard
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteDashboardCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[Dashboard]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
DashboardDAO.delete(self._models)
|
||||
except DeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise DashboardBulkDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._models = DashboardDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise DashboardNotFoundError()
|
||||
# Check there are no associated ReportSchedules
|
||||
if reports := ReportScheduleDAO.find_by_dashboard_ids(self._model_ids):
|
||||
report_names = [report.name for report in reports]
|
||||
raise DashboardBulkDeleteFailedReportsExistError(
|
||||
_("There are associated alerts or reports: %s" % ",".join(report_names))
|
||||
)
|
||||
# Check ownership
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DashboardForbiddenError() from ex
|
||||
@@ -37,33 +37,34 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteDashboardCommand(BaseCommand):
|
||||
def __init__(self, model_id: int):
|
||||
self._model_id = model_id
|
||||
self._model: Optional[Dashboard] = None
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[Dashboard]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._model
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
DashboardDAO.delete(self._model)
|
||||
DashboardDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise DashboardDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._model = DashboardDAO.find_by_id(self._model_id)
|
||||
if not self._model:
|
||||
self._models = DashboardDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise DashboardNotFoundError()
|
||||
# Check there are no associated ReportSchedules
|
||||
if reports := ReportScheduleDAO.find_by_dashboard_id(self._model_id):
|
||||
if reports := ReportScheduleDAO.find_by_dashboard_ids(self._model_ids):
|
||||
report_names = [report.name for report in reports]
|
||||
raise DashboardDeleteFailedReportsExistError(
|
||||
_("There are associated alerts or reports: %s" % ",".join(report_names))
|
||||
)
|
||||
# Check ownership
|
||||
try:
|
||||
security_manager.raise_for_ownership(self._model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DashboardForbiddenError() from ex
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DashboardForbiddenError() from ex
|
||||
|
||||
@@ -51,15 +51,7 @@ class DashboardNotFoundError(ObjectNotFoundError):
|
||||
|
||||
|
||||
class DashboardCreateFailedError(CreateFailedError):
|
||||
message = _("Dashboard could not be created.")
|
||||
|
||||
|
||||
class DashboardBulkDeleteFailedError(CreateFailedError):
|
||||
message = _("Dashboards could not be deleted.")
|
||||
|
||||
|
||||
class DashboardBulkDeleteFailedReportsExistError(DashboardBulkDeleteFailedError):
|
||||
message = _("There are associated alerts or reports")
|
||||
message = _("Dashboards could not be created.")
|
||||
|
||||
|
||||
class DashboardUpdateFailedError(UpdateFailedError):
|
||||
|
||||
@@ -37,12 +37,10 @@ from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
|
||||
from superset.daos.dataset import DatasetDAO
|
||||
from superset.databases.filters import DatabaseFilter
|
||||
from superset.datasets.commands.bulk_delete import BulkDeleteDatasetCommand
|
||||
from superset.datasets.commands.create import CreateDatasetCommand
|
||||
from superset.datasets.commands.delete import DeleteDatasetCommand
|
||||
from superset.datasets.commands.duplicate import DuplicateDatasetCommand
|
||||
from superset.datasets.commands.exceptions import (
|
||||
DatasetBulkDeleteFailedError,
|
||||
DatasetCreateFailedError,
|
||||
DatasetDeleteFailedError,
|
||||
DatasetForbiddenError,
|
||||
@@ -453,7 +451,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
DeleteDatasetCommand(pk).run()
|
||||
DeleteDatasetCommand([pk]).run()
|
||||
return self.response(200, message="OK")
|
||||
except DatasetNotFoundError:
|
||||
return self.response_404()
|
||||
@@ -788,7 +786,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteDatasetCommand(item_ids).run()
|
||||
DeleteDatasetCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -801,7 +799,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
||||
return self.response_404()
|
||||
except DatasetForbiddenError:
|
||||
return self.response_403()
|
||||
except DatasetBulkDeleteFailedError as ex:
|
||||
except DatasetDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
@expose("/import/", methods=("POST",))
|
||||
|
||||
@@ -1,60 +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 typing import Optional
|
||||
|
||||
from superset import security_manager
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.commands.exceptions import DeleteFailedError
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.daos.dataset import DatasetDAO
|
||||
from superset.datasets.commands.exceptions import (
|
||||
DatasetBulkDeleteFailedError,
|
||||
DatasetForbiddenError,
|
||||
DatasetNotFoundError,
|
||||
)
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteDatasetCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[SqlaTable]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
DatasetDAO.delete(self._models)
|
||||
except DeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise DatasetBulkDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._models = DatasetDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise DatasetNotFoundError()
|
||||
# Check ownership
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DatasetForbiddenError() from ex
|
||||
@@ -33,27 +33,28 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteDatasetCommand(BaseCommand):
|
||||
def __init__(self, model_id: int):
|
||||
self._model_id = model_id
|
||||
self._model: Optional[SqlaTable] = None
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[SqlaTable]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._model
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
return DatasetDAO.delete(self._model)
|
||||
DatasetDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex)
|
||||
logger.exception(ex.exception)
|
||||
raise DatasetDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._model = DatasetDAO.find_by_id(self._model_id)
|
||||
if not self._model:
|
||||
self._models = DatasetDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise DatasetNotFoundError()
|
||||
# Check ownership
|
||||
try:
|
||||
security_manager.raise_for_ownership(self._model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DatasetForbiddenError() from ex
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise DatasetForbiddenError() from ex
|
||||
|
||||
@@ -179,11 +179,7 @@ class DatasetUpdateFailedError(UpdateFailedError):
|
||||
|
||||
|
||||
class DatasetDeleteFailedError(DeleteFailedError):
|
||||
message = _("Dataset could not be deleted.")
|
||||
|
||||
|
||||
class DatasetBulkDeleteFailedError(DeleteFailedError):
|
||||
message = _("Dataset(s) could not be bulk deleted.")
|
||||
message = _("Datasets could not be deleted.")
|
||||
|
||||
|
||||
class DatasetRefreshFailedError(UpdateFailedError):
|
||||
|
||||
@@ -36,11 +36,9 @@ from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
|
||||
from superset.databases.filters import DatabaseFilter
|
||||
from superset.extensions import event_logger
|
||||
from superset.models.sql_lab import SavedQuery
|
||||
from superset.queries.saved_queries.commands.bulk_delete import (
|
||||
BulkDeleteSavedQueryCommand,
|
||||
)
|
||||
from superset.queries.saved_queries.commands.delete import DeleteSavedQueryCommand
|
||||
from superset.queries.saved_queries.commands.exceptions import (
|
||||
SavedQueryBulkDeleteFailedError,
|
||||
SavedQueryDeleteFailedError,
|
||||
SavedQueryNotFoundError,
|
||||
)
|
||||
from superset.queries.saved_queries.commands.export import ExportSavedQueriesCommand
|
||||
@@ -213,7 +211,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteSavedQueryCommand(item_ids).run()
|
||||
DeleteSavedQueryCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -224,7 +222,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
|
||||
)
|
||||
except SavedQueryNotFoundError:
|
||||
return self.response_404()
|
||||
except SavedQueryBulkDeleteFailedError as ex:
|
||||
except SavedQueryDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
@expose("/export/", methods=("GET",))
|
||||
|
||||
+3
-3
@@ -22,14 +22,14 @@ from superset.daos.exceptions import DAODeleteFailedError
|
||||
from superset.daos.query import SavedQueryDAO
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.queries.saved_queries.commands.exceptions import (
|
||||
SavedQueryBulkDeleteFailedError,
|
||||
SavedQueryDeleteFailedError,
|
||||
SavedQueryNotFoundError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteSavedQueryCommand(BaseCommand):
|
||||
class DeleteSavedQueryCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[Dashboard]] = None
|
||||
@@ -42,7 +42,7 @@ class BulkDeleteSavedQueryCommand(BaseCommand):
|
||||
SavedQueryDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise SavedQueryBulkDeleteFailedError() from ex
|
||||
raise SavedQueryDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
@@ -24,7 +24,7 @@ from superset.commands.exceptions import (
|
||||
)
|
||||
|
||||
|
||||
class SavedQueryBulkDeleteFailedError(DeleteFailedError):
|
||||
class SavedQueryDeleteFailedError(DeleteFailedError):
|
||||
message = _("Saved queries could not be deleted.")
|
||||
|
||||
|
||||
|
||||
@@ -30,11 +30,9 @@ from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
|
||||
from superset.dashboards.filters import DashboardAccessFilter
|
||||
from superset.databases.filters import DatabaseFilter
|
||||
from superset.extensions import event_logger
|
||||
from superset.reports.commands.bulk_delete import BulkDeleteReportScheduleCommand
|
||||
from superset.reports.commands.create import CreateReportScheduleCommand
|
||||
from superset.reports.commands.delete import DeleteReportScheduleCommand
|
||||
from superset.reports.commands.exceptions import (
|
||||
ReportScheduleBulkDeleteFailedError,
|
||||
ReportScheduleCreateFailedError,
|
||||
ReportScheduleDeleteFailedError,
|
||||
ReportScheduleForbiddenError,
|
||||
@@ -278,7 +276,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
DeleteReportScheduleCommand(pk).run()
|
||||
DeleteReportScheduleCommand([pk]).run()
|
||||
return self.response(200, message="OK")
|
||||
except ReportScheduleNotFoundError:
|
||||
return self.response_404()
|
||||
@@ -495,7 +493,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteReportScheduleCommand(item_ids).run()
|
||||
DeleteReportScheduleCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
@@ -508,5 +506,5 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
||||
return self.response_404()
|
||||
except ReportScheduleForbiddenError:
|
||||
return self.response_403()
|
||||
except ReportScheduleBulkDeleteFailedError as ex:
|
||||
except ReportScheduleDeleteFailedError as ex:
|
||||
return self.response_422(message=str(ex))
|
||||
|
||||
@@ -1,61 +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 typing import Optional
|
||||
|
||||
from superset import security_manager
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.daos.exceptions import DAODeleteFailedError
|
||||
from superset.daos.report import ReportScheduleDAO
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.reports.commands.exceptions import (
|
||||
ReportScheduleBulkDeleteFailedError,
|
||||
ReportScheduleForbiddenError,
|
||||
ReportScheduleNotFoundError,
|
||||
)
|
||||
from superset.reports.models import ReportSchedule
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteReportScheduleCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[ReportSchedule]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
ReportScheduleDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise ReportScheduleBulkDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._models = ReportScheduleDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise ReportScheduleNotFoundError()
|
||||
|
||||
# Check ownership
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise ReportScheduleForbiddenError() from ex
|
||||
@@ -33,28 +33,29 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteReportScheduleCommand(BaseCommand):
|
||||
def __init__(self, model_id: int):
|
||||
self._model_id = model_id
|
||||
self._model: Optional[ReportSchedule] = None
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: Optional[list[ReportSchedule]] = None
|
||||
|
||||
def run(self) -> None:
|
||||
self.validate()
|
||||
assert self._model
|
||||
assert self._models
|
||||
|
||||
try:
|
||||
ReportScheduleDAO.delete(self._model)
|
||||
ReportScheduleDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise ReportScheduleDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
self._model = ReportScheduleDAO.find_by_id(self._model_id)
|
||||
if not self._model:
|
||||
self._models = ReportScheduleDAO.find_by_ids(self._model_ids)
|
||||
if not self._models or len(self._models) != len(self._model_ids):
|
||||
raise ReportScheduleNotFoundError()
|
||||
|
||||
# Check ownership
|
||||
try:
|
||||
security_manager.raise_for_ownership(self._model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise ReportScheduleForbiddenError() from ex
|
||||
for model in self._models:
|
||||
try:
|
||||
security_manager.raise_for_ownership(model)
|
||||
except SupersetSecurityException as ex:
|
||||
raise ReportScheduleForbiddenError() from ex
|
||||
|
||||
@@ -21,7 +21,6 @@ from superset.commands.exceptions import (
|
||||
CommandException,
|
||||
CommandInvalidError,
|
||||
CreateFailedError,
|
||||
DeleteFailedError,
|
||||
ForbiddenError,
|
||||
ValidationError,
|
||||
)
|
||||
@@ -125,10 +124,6 @@ class ReportScheduleInvalidError(CommandInvalidError):
|
||||
message = _("Report Schedule parameters are invalid.")
|
||||
|
||||
|
||||
class ReportScheduleBulkDeleteFailedError(DeleteFailedError):
|
||||
message = _("Report Schedule could not be deleted.")
|
||||
|
||||
|
||||
class ReportScheduleCreateFailedError(CreateFailedError):
|
||||
message = _("Report Schedule could not be created.")
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ from superset.connectors.sqla.models import RowLevelSecurityFilter
|
||||
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
|
||||
from superset.daos.exceptions import DAOCreateFailedError, DAOUpdateFailedError
|
||||
from superset.extensions import event_logger
|
||||
from superset.row_level_security.commands.bulk_delete import BulkDeleteRLSRuleCommand
|
||||
from superset.row_level_security.commands.create import CreateRLSRuleCommand
|
||||
from superset.row_level_security.commands.delete import DeleteRLSRuleCommand
|
||||
from superset.row_level_security.commands.exceptions import RLSRuleNotFoundError
|
||||
from superset.row_level_security.commands.update import UpdateRLSRuleCommand
|
||||
from superset.row_level_security.schemas import (
|
||||
@@ -340,7 +340,7 @@ class RLSRestApi(BaseSupersetModelRestApi):
|
||||
"""
|
||||
item_ids = kwargs["rison"]
|
||||
try:
|
||||
BulkDeleteRLSRuleCommand(item_ids).run()
|
||||
DeleteRLSRuleCommand(item_ids).run()
|
||||
return self.response(
|
||||
200,
|
||||
message=ngettext(
|
||||
|
||||
+3
-3
@@ -23,13 +23,13 @@ from superset.daos.security import RLSDAO
|
||||
from superset.reports.models import ReportSchedule
|
||||
from superset.row_level_security.commands.exceptions import (
|
||||
RLSRuleNotFoundError,
|
||||
RuleBulkDeleteFailedError,
|
||||
RuleDeleteFailedError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BulkDeleteRLSRuleCommand(BaseCommand):
|
||||
class DeleteRLSRuleCommand(BaseCommand):
|
||||
def __init__(self, model_ids: list[int]):
|
||||
self._model_ids = model_ids
|
||||
self._models: list[ReportSchedule] = []
|
||||
@@ -40,7 +40,7 @@ class BulkDeleteRLSRuleCommand(BaseCommand):
|
||||
RLSDAO.delete(self._models)
|
||||
except DAODeleteFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise RuleBulkDeleteFailedError() from ex
|
||||
raise RuleDeleteFailedError() from ex
|
||||
|
||||
def validate(self) -> None:
|
||||
# Validate/populate model exists
|
||||
@@ -25,5 +25,5 @@ class RLSRuleNotFoundError(CommandException):
|
||||
message = _("RLS Rule not found.")
|
||||
|
||||
|
||||
class RuleBulkDeleteFailedError(DeleteFailedError):
|
||||
message = _("RLS Rule could not be deleted.")
|
||||
class RuleDeleteFailedError(DeleteFailedError):
|
||||
message = _("RLS rules could not be deleted.")
|
||||
|
||||
@@ -1530,7 +1530,7 @@ class TestDatasetApi(SupersetTestCase):
|
||||
rv = self.delete_assert_metric(uri, "delete")
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
assert rv.status_code == 422
|
||||
assert data == {"message": "Dataset could not be deleted."}
|
||||
assert data == {"message": "Datasets could not be deleted."}
|
||||
db.session.delete(dataset)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ class TestRowLevelSecurityUpdateAPI(SupersetTestCase):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class TestRowLevelSecurityBulkDeleteAPI(SupersetTestCase):
|
||||
class TestRowLevelSecurityDeleteAPI(SupersetTestCase):
|
||||
def test_invalid_id_failure(self):
|
||||
self.login("Admin")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user