mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
feat(filter-set): Add filterset resource (#14015)
* Add filterset resource * fix: fix pre-commit * add tests * add tests and fixes based of failures * Fix pre-commit errors * chore init filterset resource under ff constraint * Fix migration conflicts * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * Fix pylint and migrations issues * add tests and fixes based of failures * Fix missing license * fix down revision * update down_revision * fix: update down_revision * chore: add description to migration * fix: type * refactor: is_user_admin * fix: use get_public_role * fix: move import to the relevant location * chore: add openSpec api schema * chore: cover all openspec API * fix: pre-commit and lint * fix: put and post schemas * fix: undo superset_test_config.py * fix: limit filterSetsApi to include_route_methods = {"get_list", "put", "post", "delete"} * renaming some params * chore: add debug in test config * fix: rename database to different name * fix: try to make conftest.py harmless * fix: pre-commit * fix: new down_revision ref * fix: bad ref * fix: bad ref 2 * fix: bad ref 3 * fix: add api in initiatior * fix: open spec * fix: convert name to str to include int usecases * fix: pylint * fix: pylint * Update superset/common/request_contexed_based.py Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> * chore: resolve PR comments * chore: resolve PR comments * chore: resolve PR comments * fix failed tests * fix pylint * Update conftest.py * chore remove BaseCommand to remove abstraction * chore remove BaseCommand to remove abstraction * chore remove BaseCommand to remove abstraction * chore remove BaseCommand to remove abstraction * chore fix migration Co-authored-by: Ofeknielsen <ofek.israel@nieslen.com> Co-authored-by: amitmiran137 <amit.miran@nielsen.com> Co-authored-by: Amit Miran <47772523+amitmiran137@users.noreply.github.com> Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
parent
997320ac1a
commit
84f7614e97
@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
@ -31,6 +31,26 @@ class CommandException(SupersetException):
|
|||||||
return repr(self)
|
return repr(self)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectNotFoundError(CommandException):
|
||||||
|
status = 404
|
||||||
|
message_format = "{} {}not found."
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
object_type: str,
|
||||||
|
object_id: Optional[str] = None,
|
||||||
|
exception: Optional[Exception] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
_(
|
||||||
|
self.message_format.format(
|
||||||
|
object_type, '"%s" ' % object_id if object_id else ""
|
||||||
|
)
|
||||||
|
),
|
||||||
|
exception,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandInvalidError(CommandException):
|
class CommandInvalidError(CommandException):
|
||||||
""" Common base class for Command Invalid errors. """
|
""" Common base class for Command Invalid errors. """
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
from typing import Iterator, List, Tuple
|
from typing import Iterator, List, Tuple, Type
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from flask_appbuilder import Model
|
from flask_appbuilder import Model
|
||||||
@ -33,8 +33,8 @@ METADATA_FILE_NAME = "metadata.yaml"
|
|||||||
|
|
||||||
class ExportModelsCommand(BaseCommand):
|
class ExportModelsCommand(BaseCommand):
|
||||||
|
|
||||||
dao = BaseDAO
|
dao: Type[BaseDAO] = BaseDAO
|
||||||
not_found = CommandException
|
not_found: Type[CommandException] = CommandException
|
||||||
|
|
||||||
def __init__(self, model_ids: List[int]):
|
def __init__(self, model_ids: List[int]):
|
||||||
self.model_ids = model_ids
|
self.model_ids = model_ids
|
||||||
|
39
superset/common/not_authrized_object.py
Normal file
39
superset/common/not_authrized_object.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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, Optional
|
||||||
|
|
||||||
|
from superset.exceptions import SupersetException
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorizedObject:
|
||||||
|
def __init__(self, what_not_authorized: str):
|
||||||
|
self._what_not_authorized = what_not_authorized
|
||||||
|
|
||||||
|
def __getattr__(self, item: Any) -> None:
|
||||||
|
raise NotAuthorizedException(self._what_not_authorized)
|
||||||
|
|
||||||
|
def __getitem__(self, item: Any) -> None:
|
||||||
|
raise NotAuthorizedException(self._what_not_authorized)
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorizedException(SupersetException):
|
||||||
|
def __init__(
|
||||||
|
self, what_not_authorized: str = "", exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
"The user is not authorized to " + what_not_authorized, exception
|
||||||
|
)
|
39
superset/common/request_contexed_based.py
Normal file
39
superset/common/request_contexed_based.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
from superset import conf, security_manager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from flask_appbuilder.security.sqla.models import Role
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_roles() -> List[Role]:
|
||||||
|
if g.user.is_anonymous:
|
||||||
|
public_role = conf.get("AUTH_ROLE_PUBLIC")
|
||||||
|
return [security_manager.get_public_role()] if public_role else []
|
||||||
|
return g.user.roles
|
||||||
|
|
||||||
|
|
||||||
|
def is_user_admin() -> bool:
|
||||||
|
user_roles = [role.name.lower() for role in get_user_roles()]
|
||||||
|
admin_role = conf.get("AUTH_ROLE_ADMIN").lower()
|
||||||
|
return admin_role in user_roles
|
@ -14,16 +14,18 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from marshmallow.validate import ValidationError
|
from marshmallow.validate import ValidationError
|
||||||
|
|
||||||
from superset.commands.exceptions import (
|
from superset.commands.exceptions import (
|
||||||
CommandException,
|
|
||||||
CommandInvalidError,
|
CommandInvalidError,
|
||||||
CreateFailedError,
|
CreateFailedError,
|
||||||
DeleteFailedError,
|
DeleteFailedError,
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
ImportFailedError,
|
ImportFailedError,
|
||||||
|
ObjectNotFoundError,
|
||||||
UpdateFailedError,
|
UpdateFailedError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,8 +43,11 @@ class DashboardInvalidError(CommandInvalidError):
|
|||||||
message = _("Dashboard parameters are invalid.")
|
message = _("Dashboard parameters are invalid.")
|
||||||
|
|
||||||
|
|
||||||
class DashboardNotFoundError(CommandException):
|
class DashboardNotFoundError(ObjectNotFoundError):
|
||||||
message = _("Dashboard not found.")
|
def __init__(
|
||||||
|
self, dashboard_id: Optional[str] = None, exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__("Dashboard", dashboard_id, exception)
|
||||||
|
|
||||||
|
|
||||||
class DashboardCreateFailedError(CreateFailedError):
|
class DashboardCreateFailedError(CreateFailedError):
|
||||||
|
16
superset/dashboards/filter_sets/__init__.py
Normal file
16
superset/dashboards/filter_sets/__init__.py
Normal 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.
|
374
superset/dashboards/filter_sets/api.py
Normal file
374
superset/dashboards/filter_sets/api.py
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
# 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, cast
|
||||||
|
|
||||||
|
from flask import g, request, Response
|
||||||
|
from flask_appbuilder.api import (
|
||||||
|
expose,
|
||||||
|
get_list_schema,
|
||||||
|
permission_name,
|
||||||
|
protect,
|
||||||
|
rison,
|
||||||
|
safe,
|
||||||
|
)
|
||||||
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
|
from superset.commands.exceptions import ObjectNotFoundError
|
||||||
|
from superset.dashboards.commands.exceptions import DashboardNotFoundError
|
||||||
|
from superset.dashboards.dao import DashboardDAO
|
||||||
|
from superset.dashboards.filter_sets.commands.create import CreateFilterSetCommand
|
||||||
|
from superset.dashboards.filter_sets.commands.delete import DeleteFilterSetCommand
|
||||||
|
from superset.dashboards.filter_sets.commands.exceptions import (
|
||||||
|
FilterSetCreateFailedError,
|
||||||
|
FilterSetDeleteFailedError,
|
||||||
|
FilterSetForbiddenError,
|
||||||
|
FilterSetNotFoundError,
|
||||||
|
FilterSetUpdateFailedError,
|
||||||
|
UserIsNotDashboardOwnerError,
|
||||||
|
)
|
||||||
|
from superset.dashboards.filter_sets.commands.update import UpdateFilterSetCommand
|
||||||
|
from superset.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_FIELD,
|
||||||
|
DASHBOARD_ID_FIELD,
|
||||||
|
DESCRIPTION_FIELD,
|
||||||
|
FILTER_SET_API_PERMISSIONS_NAME,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
NAME_FIELD,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
OWNER_OBJECT_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
PARAMS_PROPERTY,
|
||||||
|
)
|
||||||
|
from superset.dashboards.filter_sets.filters import FilterSetFilter
|
||||||
|
from superset.dashboards.filter_sets.schemas import (
|
||||||
|
FilterSetPostSchema,
|
||||||
|
FilterSetPutSchema,
|
||||||
|
)
|
||||||
|
from superset.extensions import event_logger
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetRestApi(BaseSupersetModelRestApi):
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
include_route_methods = {"get_list", "put", "post", "delete"}
|
||||||
|
datamodel = SQLAInterface(FilterSet)
|
||||||
|
resource_name = "dashboard"
|
||||||
|
class_permission_name = FILTER_SET_API_PERMISSIONS_NAME
|
||||||
|
allow_browser_login = True
|
||||||
|
csrf_exempt = False
|
||||||
|
add_exclude_columns = [
|
||||||
|
"id",
|
||||||
|
OWNER_OBJECT_FIELD,
|
||||||
|
DASHBOARD_FIELD,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
]
|
||||||
|
add_model_schema = FilterSetPostSchema()
|
||||||
|
edit_model_schema = FilterSetPutSchema()
|
||||||
|
edit_exclude_columns = [
|
||||||
|
"id",
|
||||||
|
OWNER_OBJECT_FIELD,
|
||||||
|
DASHBOARD_FIELD,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
]
|
||||||
|
list_columns = [
|
||||||
|
"created_on",
|
||||||
|
"changed_on",
|
||||||
|
"created_by_fk",
|
||||||
|
"changed_by_fk",
|
||||||
|
NAME_FIELD,
|
||||||
|
DESCRIPTION_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
DASHBOARD_ID_FIELD,
|
||||||
|
PARAMS_PROPERTY,
|
||||||
|
]
|
||||||
|
show_exclude_columns = [OWNER_OBJECT_FIELD, DASHBOARD_FIELD, JSON_METADATA_FIELD]
|
||||||
|
search_columns = ["id", NAME_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD]
|
||||||
|
base_filters = [[OWNER_ID_FIELD, FilterSetFilter, ""]]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.datamodel.get_search_columns_list = lambda: []
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def _init_properties(self) -> None:
|
||||||
|
# pylint: disable=bad-super-call
|
||||||
|
super(BaseSupersetModelRestApi, self)._init_properties()
|
||||||
|
|
||||||
|
@expose("/<int:dashboard_id>/filtersets", methods=["GET"])
|
||||||
|
@protect()
|
||||||
|
@safe
|
||||||
|
@permission_name("get")
|
||||||
|
@rison(get_list_schema)
|
||||||
|
def get_list(self, dashboard_id: int, **kwargs: Any) -> Response:
|
||||||
|
"""
|
||||||
|
Gets a dashboard's Filter sets
|
||||||
|
---
|
||||||
|
get:
|
||||||
|
description: >-
|
||||||
|
Get a dashboard's list of filter sets
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
name: dashboard_id
|
||||||
|
description: The id of the dashboard
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: FilterSets
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of the Filter set
|
||||||
|
type: string
|
||||||
|
json_metadata:
|
||||||
|
description: metadata of the filter set
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
description: A description field of the filter set
|
||||||
|
type: string
|
||||||
|
owner_id:
|
||||||
|
description: A description field of the filter set
|
||||||
|
type: integer
|
||||||
|
owner_type:
|
||||||
|
description: the Type of the owner ( Dashboard/User)
|
||||||
|
type: integer
|
||||||
|
parameters:
|
||||||
|
description: JSON schema defining the needed parameters
|
||||||
|
302:
|
||||||
|
description: Redirects to the current digest
|
||||||
|
400:
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
404:
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
"""
|
||||||
|
if not DashboardDAO.find_by_id(cast(int, dashboard_id)):
|
||||||
|
return self.response(404, message="dashboard '%s' not found" % dashboard_id)
|
||||||
|
rison_data = kwargs.setdefault("rison", {})
|
||||||
|
rison_data.setdefault("filters", [])
|
||||||
|
rison_data["filters"].append(
|
||||||
|
{"col": "dashboard_id", "opr": "eq", "value": str(dashboard_id)}
|
||||||
|
)
|
||||||
|
return self.get_list_headless(**kwargs)
|
||||||
|
|
||||||
|
@expose("/<int:dashboard_id>/filtersets", methods=["POST"])
|
||||||
|
@protect()
|
||||||
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
|
@event_logger.log_this_with_context(
|
||||||
|
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
|
||||||
|
log_to_statsd=False,
|
||||||
|
)
|
||||||
|
def post(self, dashboard_id: int) -> Response:
|
||||||
|
"""
|
||||||
|
Creates a new Dashboard's Filter Set
|
||||||
|
---
|
||||||
|
post:
|
||||||
|
description: >-
|
||||||
|
Create a new Dashboard's Filter Set.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
name: dashboard_id
|
||||||
|
description: The id of the dashboard
|
||||||
|
requestBody:
|
||||||
|
description: Filter set schema
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: Filter set added
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
result:
|
||||||
|
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
|
||||||
|
302:
|
||||||
|
description: Redirects to the current digest
|
||||||
|
400:
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
404:
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
500:
|
||||||
|
$ref: '#/components/responses/500'
|
||||||
|
"""
|
||||||
|
if not request.is_json:
|
||||||
|
return self.response_400(message="Request is not JSON")
|
||||||
|
try:
|
||||||
|
item = self.add_model_schema.load(request.json)
|
||||||
|
new_model = CreateFilterSetCommand(g.user, dashboard_id, item).run()
|
||||||
|
return self.response(201, id=new_model.id, result=item)
|
||||||
|
except ValidationError as error:
|
||||||
|
return self.response_400(message=error.messages)
|
||||||
|
except UserIsNotDashboardOwnerError:
|
||||||
|
return self.response_403()
|
||||||
|
except FilterSetCreateFailedError as error:
|
||||||
|
return self.response_400(message=error.message)
|
||||||
|
except DashboardNotFoundError:
|
||||||
|
return self.response_404()
|
||||||
|
|
||||||
|
@expose("/<int:dashboard_id>/filtersets/<int:pk>", methods=["PUT"])
|
||||||
|
@protect()
|
||||||
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
|
@event_logger.log_this_with_context(
|
||||||
|
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put",
|
||||||
|
log_to_statsd=False,
|
||||||
|
)
|
||||||
|
def put(self, dashboard_id: int, pk: int) -> Response:
|
||||||
|
"""Changes a Dashboard's Filter set
|
||||||
|
---
|
||||||
|
put:
|
||||||
|
description: >-
|
||||||
|
Changes a Dashboard's Filter set.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
name: dashboard_id
|
||||||
|
- in: path
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
name: pk
|
||||||
|
requestBody:
|
||||||
|
description: Filter set schema
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/{{self.__class__.__name__}}.put'
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Filter set changed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
result:
|
||||||
|
$ref: '#/components/schemas/{{self.__class__.__name__}}.put'
|
||||||
|
400:
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
403:
|
||||||
|
$ref: '#/components/responses/403'
|
||||||
|
404:
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
422:
|
||||||
|
$ref: '#/components/responses/422'
|
||||||
|
500:
|
||||||
|
$ref: '#/components/responses/500'
|
||||||
|
"""
|
||||||
|
if not request.is_json:
|
||||||
|
return self.response_400(message="Request is not JSON")
|
||||||
|
try:
|
||||||
|
item = self.edit_model_schema.load(request.json)
|
||||||
|
changed_model = UpdateFilterSetCommand(g.user, dashboard_id, pk, item).run()
|
||||||
|
return self.response(200, id=changed_model.id, result=item)
|
||||||
|
except ValidationError as error:
|
||||||
|
return self.response_400(message=error.messages)
|
||||||
|
except (
|
||||||
|
ObjectNotFoundError,
|
||||||
|
FilterSetForbiddenError,
|
||||||
|
FilterSetUpdateFailedError,
|
||||||
|
) as err:
|
||||||
|
logger.error(err)
|
||||||
|
return self.response(err.status)
|
||||||
|
|
||||||
|
@expose("/<int:dashboard_id>/filtersets/<int:pk>", methods=["DELETE"])
|
||||||
|
@protect()
|
||||||
|
@safe
|
||||||
|
@statsd_metrics
|
||||||
|
@event_logger.log_this_with_context(
|
||||||
|
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete",
|
||||||
|
log_to_statsd=False,
|
||||||
|
)
|
||||||
|
def delete(self, dashboard_id: int, pk: int) -> Response:
|
||||||
|
"""
|
||||||
|
Deletes a Dashboard's FilterSet
|
||||||
|
---
|
||||||
|
delete:
|
||||||
|
description: >-
|
||||||
|
Deletes a Dashboard.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
name: dashboard_id
|
||||||
|
- in: path
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
name: pk
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Filter set deleted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
403:
|
||||||
|
$ref: '#/components/responses/403'
|
||||||
|
404:
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
422:
|
||||||
|
$ref: '#/components/responses/422'
|
||||||
|
500:
|
||||||
|
$ref: '#/components/responses/500'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
changed_model = DeleteFilterSetCommand(g.user, dashboard_id, pk).run()
|
||||||
|
return self.response(200, id=changed_model.id)
|
||||||
|
except ValidationError as error:
|
||||||
|
return self.response_400(message=error.messages)
|
||||||
|
except FilterSetNotFoundError:
|
||||||
|
return self.response(200)
|
||||||
|
except (
|
||||||
|
ObjectNotFoundError,
|
||||||
|
FilterSetForbiddenError,
|
||||||
|
FilterSetDeleteFailedError,
|
||||||
|
) as err:
|
||||||
|
logger.error(err)
|
||||||
|
return self.response(err.status)
|
16
superset/dashboards/filter_sets/commands/__init__.py
Normal file
16
superset/dashboards/filter_sets/commands/__init__.py
Normal 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.
|
91
superset/dashboards/filter_sets/commands/base.py
Normal file
91
superset/dashboards/filter_sets/commands/base.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# 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 cast, Optional
|
||||||
|
|
||||||
|
from flask_appbuilder.models.sqla import Model
|
||||||
|
from flask_appbuilder.security.sqla.models import User
|
||||||
|
|
||||||
|
from superset.common.not_authrized_object import NotAuthorizedException
|
||||||
|
from superset.common.request_contexed_based import is_user_admin
|
||||||
|
from superset.dashboards.commands.exceptions import DashboardNotFoundError
|
||||||
|
from superset.dashboards.dao import DashboardDAO
|
||||||
|
from superset.dashboards.filter_sets.commands.exceptions import (
|
||||||
|
FilterSetForbiddenError,
|
||||||
|
FilterSetNotFoundError,
|
||||||
|
)
|
||||||
|
from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE
|
||||||
|
from superset.models.dashboard import Dashboard
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFilterSetCommand:
|
||||||
|
# pylint: disable=C0103
|
||||||
|
_dashboard: Dashboard
|
||||||
|
_filter_set_id: Optional[int]
|
||||||
|
_filter_set: Optional[FilterSet]
|
||||||
|
|
||||||
|
def __init__(self, user: User, dashboard_id: int):
|
||||||
|
self._actor = user
|
||||||
|
self._is_actor_admin = is_user_admin()
|
||||||
|
self._dashboard_id = dashboard_id
|
||||||
|
|
||||||
|
def run(self) -> Model:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _validate_filterset_dashboard_exists(self) -> None:
|
||||||
|
self._dashboard = DashboardDAO.get_by_id_or_slug(str(self._dashboard_id))
|
||||||
|
if not self._dashboard:
|
||||||
|
raise DashboardNotFoundError()
|
||||||
|
|
||||||
|
def is_user_dashboard_owner(self) -> bool:
|
||||||
|
return self._is_actor_admin or self._dashboard.is_actor_owner()
|
||||||
|
|
||||||
|
def validate_exist_filter_use_cases_set(self) -> None: # pylint: disable=C0103
|
||||||
|
self._validate_filter_set_exists_and_set_when_exists()
|
||||||
|
self.check_ownership()
|
||||||
|
|
||||||
|
def _validate_filter_set_exists_and_set_when_exists(self) -> None:
|
||||||
|
self._filter_set = self._dashboard.filter_sets.get(
|
||||||
|
cast(int, self._filter_set_id), None
|
||||||
|
)
|
||||||
|
if not self._filter_set:
|
||||||
|
raise FilterSetNotFoundError(str(self._filter_set_id))
|
||||||
|
|
||||||
|
def check_ownership(self) -> None:
|
||||||
|
try:
|
||||||
|
if not self._is_actor_admin:
|
||||||
|
filter_set: FilterSet = cast(FilterSet, self._filter_set)
|
||||||
|
if filter_set.owner_type == USER_OWNER_TYPE:
|
||||||
|
if self._actor.id != filter_set.owner_id:
|
||||||
|
raise FilterSetForbiddenError(
|
||||||
|
str(self._filter_set_id),
|
||||||
|
"The user is not the owner of the filter_set",
|
||||||
|
)
|
||||||
|
elif not self.is_user_dashboard_owner():
|
||||||
|
raise FilterSetForbiddenError(
|
||||||
|
str(self._filter_set_id),
|
||||||
|
"The user is not an owner of the filter_set's dashboard",
|
||||||
|
)
|
||||||
|
except NotAuthorizedException as err:
|
||||||
|
raise FilterSetForbiddenError(
|
||||||
|
str(self._filter_set_id), "user not authorized to access the filterset",
|
||||||
|
) from err
|
||||||
|
except FilterSetForbiddenError as err:
|
||||||
|
raise err
|
78
superset/dashboards/filter_sets/commands/create.py
Normal file
78
superset/dashboards/filter_sets/commands/create.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# 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, Dict
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
from flask_appbuilder.models.sqla import Model
|
||||||
|
from flask_appbuilder.security.sqla.models import User
|
||||||
|
|
||||||
|
from superset import security_manager
|
||||||
|
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
|
||||||
|
from superset.dashboards.filter_sets.commands.exceptions import (
|
||||||
|
DashboardIdInconsistencyError,
|
||||||
|
FilterSetCreateFailedError,
|
||||||
|
UserIsNotDashboardOwnerError,
|
||||||
|
)
|
||||||
|
from superset.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_ID_FIELD,
|
||||||
|
DASHBOARD_OWNER_TYPE,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
)
|
||||||
|
from superset.dashboards.filter_sets.dao import FilterSetDAO
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateFilterSetCommand(BaseFilterSetCommand):
|
||||||
|
# pylint: disable=C0103
|
||||||
|
def __init__(self, user: User, dashboard_id: int, data: Dict[str, Any]):
|
||||||
|
super().__init__(user, dashboard_id)
|
||||||
|
self._properties = data.copy()
|
||||||
|
|
||||||
|
def run(self) -> Model:
|
||||||
|
self.validate()
|
||||||
|
self._properties[DASHBOARD_ID_FIELD] = self._dashboard.id
|
||||||
|
filter_set = FilterSetDAO.create(self._properties, commit=True)
|
||||||
|
return filter_set
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
self._validate_filterset_dashboard_exists()
|
||||||
|
if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE:
|
||||||
|
self._validate_owner_id_is_dashboard_id()
|
||||||
|
self._validate_user_is_the_dashboard_owner()
|
||||||
|
else:
|
||||||
|
self._validate_owner_id_exists()
|
||||||
|
|
||||||
|
def _validate_owner_id_exists(self) -> None:
|
||||||
|
owner_id = self._properties[OWNER_ID_FIELD]
|
||||||
|
if not (g.user.id == owner_id or security_manager.get_user_by_id(owner_id)):
|
||||||
|
raise FilterSetCreateFailedError(
|
||||||
|
str(self._dashboard_id), "owner_id does not exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_user_is_the_dashboard_owner(self) -> None:
|
||||||
|
if not self.is_user_dashboard_owner():
|
||||||
|
raise UserIsNotDashboardOwnerError(str(self._dashboard_id))
|
||||||
|
|
||||||
|
def _validate_owner_id_is_dashboard_id(self) -> None:
|
||||||
|
if (
|
||||||
|
self._properties.get(OWNER_ID_FIELD, self._dashboard_id)
|
||||||
|
!= self._dashboard_id
|
||||||
|
):
|
||||||
|
raise DashboardIdInconsistencyError(str(self._dashboard_id))
|
56
superset/dashboards/filter_sets/commands/delete.py
Normal file
56
superset/dashboards/filter_sets/commands/delete.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 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 import Model
|
||||||
|
from flask_appbuilder.security.sqla.models import User
|
||||||
|
|
||||||
|
from superset.dao.exceptions import DAODeleteFailedError
|
||||||
|
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
|
||||||
|
from superset.dashboards.filter_sets.commands.exceptions import (
|
||||||
|
FilterSetDeleteFailedError,
|
||||||
|
FilterSetForbiddenError,
|
||||||
|
FilterSetNotFoundError,
|
||||||
|
)
|
||||||
|
from superset.dashboards.filter_sets.dao import FilterSetDAO
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteFilterSetCommand(BaseFilterSetCommand):
|
||||||
|
def __init__(self, user: User, dashboard_id: int, filter_set_id: int):
|
||||||
|
super().__init__(user, dashboard_id)
|
||||||
|
self._filter_set_id = filter_set_id
|
||||||
|
|
||||||
|
def run(self) -> Model:
|
||||||
|
try:
|
||||||
|
self.validate()
|
||||||
|
return FilterSetDAO.delete(self._filter_set, commit=True)
|
||||||
|
except DAODeleteFailedError as err:
|
||||||
|
raise FilterSetDeleteFailedError(str(self._filter_set_id), "") from err
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
self._validate_filterset_dashboard_exists()
|
||||||
|
try:
|
||||||
|
self.validate_exist_filter_use_cases_set()
|
||||||
|
except FilterSetNotFoundError as err:
|
||||||
|
if FilterSetDAO.find_by_id(self._filter_set_id): # type: ignore
|
||||||
|
raise FilterSetForbiddenError(
|
||||||
|
'the filter-set does not related to dashboard "%s"'
|
||||||
|
% str(self._dashboard_id)
|
||||||
|
) from err
|
||||||
|
raise err
|
94
superset/dashboards/filter_sets/commands/exceptions.py
Normal file
94
superset/dashboards/filter_sets/commands/exceptions.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 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 Optional
|
||||||
|
|
||||||
|
from flask_babel import lazy_gettext as _
|
||||||
|
|
||||||
|
from superset.commands.exceptions import (
|
||||||
|
CreateFailedError,
|
||||||
|
DeleteFailedError,
|
||||||
|
ForbiddenError,
|
||||||
|
ObjectNotFoundError,
|
||||||
|
UpdateFailedError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetNotFoundError(ObjectNotFoundError):
|
||||||
|
def __init__(
|
||||||
|
self, filterset_id: Optional[str] = None, exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__("FilterSet", filterset_id, exception)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetCreateFailedError(CreateFailedError):
|
||||||
|
base_message = 'CreateFilterSetCommand of dashboard "%s" failed: '
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, dashboard_id: str, reason: str = "", exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__((self.base_message % dashboard_id) + reason, exception)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetUpdateFailedError(UpdateFailedError):
|
||||||
|
base_message = 'UpdateFilterSetCommand of filter_set "%s" failed: '
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__((self.base_message % filterset_id) + reason, exception)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetDeleteFailedError(DeleteFailedError):
|
||||||
|
base_message = 'DeleteFilterSetCommand of filter_set "%s" failed: '
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__((self.base_message % filterset_id) + reason, exception)
|
||||||
|
|
||||||
|
|
||||||
|
class UserIsNotDashboardOwnerError(FilterSetCreateFailedError):
|
||||||
|
reason = (
|
||||||
|
"cannot create dashboard owner filterset based when"
|
||||||
|
" the user is not the dashboard owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, dashboard_id: str, exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(dashboard_id, self.reason, exception)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardIdInconsistencyError(FilterSetCreateFailedError):
|
||||||
|
reason = (
|
||||||
|
"cannot create dashboard owner filterset based when the"
|
||||||
|
" ownerid is not the dashboard id"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, dashboard_id: str, exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(dashboard_id, self.reason, exception)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetForbiddenError(ForbiddenError):
|
||||||
|
message_format = 'Changing FilterSet "{}" is forbidden: {}'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(_(self.message_format.format(filterset_id, reason)), exception)
|
56
superset/dashboards/filter_sets/commands/update.py
Normal file
56
superset/dashboards/filter_sets/commands/update.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 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, Dict
|
||||||
|
|
||||||
|
from flask_appbuilder.models.sqla import Model
|
||||||
|
from flask_appbuilder.security.sqla.models import User
|
||||||
|
|
||||||
|
from superset.dao.exceptions import DAOUpdateFailedError
|
||||||
|
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
|
||||||
|
from superset.dashboards.filter_sets.commands.exceptions import (
|
||||||
|
FilterSetUpdateFailedError,
|
||||||
|
)
|
||||||
|
from superset.dashboards.filter_sets.consts import OWNER_ID_FIELD, OWNER_TYPE_FIELD
|
||||||
|
from superset.dashboards.filter_sets.dao import FilterSetDAO
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFilterSetCommand(BaseFilterSetCommand):
|
||||||
|
def __init__(
|
||||||
|
self, user: User, dashboard_id: int, filter_set_id: int, data: Dict[str, Any]
|
||||||
|
):
|
||||||
|
super().__init__(user, dashboard_id)
|
||||||
|
self._filter_set_id = filter_set_id
|
||||||
|
self._properties = data.copy()
|
||||||
|
|
||||||
|
def run(self) -> Model:
|
||||||
|
try:
|
||||||
|
self.validate()
|
||||||
|
if (
|
||||||
|
OWNER_TYPE_FIELD in self._properties
|
||||||
|
and self._properties[OWNER_TYPE_FIELD] == "Dashboard"
|
||||||
|
):
|
||||||
|
self._properties[OWNER_ID_FIELD] = self._dashboard_id
|
||||||
|
return FilterSetDAO.update(self._filter_set, self._properties, commit=True)
|
||||||
|
except DAOUpdateFailedError as err:
|
||||||
|
raise FilterSetUpdateFailedError(str(self._filter_set_id), "") from err
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
self._validate_filterset_dashboard_exists()
|
||||||
|
self.validate_exist_filter_use_cases_set()
|
30
superset/dashboards/filter_sets/consts.py
Normal file
30
superset/dashboards/filter_sets/consts.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# 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.
|
||||||
|
USER_OWNER_TYPE = "User"
|
||||||
|
DASHBOARD_OWNER_TYPE = "Dashboard"
|
||||||
|
|
||||||
|
NAME_FIELD = "name"
|
||||||
|
DESCRIPTION_FIELD = "description"
|
||||||
|
JSON_METADATA_FIELD = "json_metadata"
|
||||||
|
OWNER_ID_FIELD = "owner_id"
|
||||||
|
OWNER_TYPE_FIELD = "owner_type"
|
||||||
|
DASHBOARD_ID_FIELD = "dashboard_id"
|
||||||
|
OWNER_OBJECT_FIELD = "owner_object"
|
||||||
|
DASHBOARD_FIELD = "dashboard"
|
||||||
|
PARAMS_PROPERTY = "params"
|
||||||
|
|
||||||
|
FILTER_SET_API_PERMISSIONS_NAME = "FilterSets"
|
64
superset/dashboards/filter_sets/dao.py
Normal file
64
superset/dashboards/filter_sets/dao.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# 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, Dict
|
||||||
|
|
||||||
|
from flask_appbuilder.models.sqla import Model
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from superset.dao.base import BaseDAO
|
||||||
|
from superset.dao.exceptions import DAOConfigError, DAOCreateFailedError
|
||||||
|
from superset.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_ID_FIELD,
|
||||||
|
DESCRIPTION_FIELD,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
NAME_FIELD,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
)
|
||||||
|
from superset.extensions import db
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetDAO(BaseDAO):
|
||||||
|
model_cls = FilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model:
|
||||||
|
if cls.model_cls is None:
|
||||||
|
raise DAOConfigError()
|
||||||
|
model = FilterSet()
|
||||||
|
setattr(model, NAME_FIELD, properties[NAME_FIELD])
|
||||||
|
setattr(model, JSON_METADATA_FIELD, properties[JSON_METADATA_FIELD])
|
||||||
|
setattr(model, DESCRIPTION_FIELD, properties.get(DESCRIPTION_FIELD, None))
|
||||||
|
setattr(
|
||||||
|
model,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
properties.get(OWNER_ID_FIELD, properties[DASHBOARD_ID_FIELD]),
|
||||||
|
)
|
||||||
|
setattr(model, OWNER_TYPE_FIELD, properties[OWNER_TYPE_FIELD])
|
||||||
|
setattr(model, DASHBOARD_ID_FIELD, properties[DASHBOARD_ID_FIELD])
|
||||||
|
try:
|
||||||
|
db.session.add(model)
|
||||||
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
|
except SQLAlchemyError as ex: # pragma: no cover
|
||||||
|
db.session.rollback()
|
||||||
|
raise DAOCreateFailedError() from ex
|
||||||
|
return model
|
58
superset/dashboards/filter_sets/filters.py
Normal file
58
superset/dashboards/filter_sets/filters.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
|
|
||||||
|
from superset.dashboards.filter_sets.consts import DASHBOARD_OWNER_TYPE, USER_OWNER_TYPE
|
||||||
|
from superset.models.dashboard import dashboard_user
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
from superset.views.base import BaseFilter, is_user_admin
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sqlalchemy.orm.query import Query
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetFilter(BaseFilter): # pylint: disable=too-few-public-methods)
|
||||||
|
def apply(self, query: Query, value: Any) -> Query:
|
||||||
|
if is_user_admin():
|
||||||
|
return query
|
||||||
|
current_user_id = g.user.id
|
||||||
|
|
||||||
|
filter_set_ids_by_dashboard_owners = ( # pylint: disable=C0103
|
||||||
|
query.from_self(FilterSet.id)
|
||||||
|
.join(dashboard_user, FilterSet.owner_id == dashboard_user.c.dashboard_id)
|
||||||
|
.filter(
|
||||||
|
and_(
|
||||||
|
FilterSet.owner_type == DASHBOARD_OWNER_TYPE,
|
||||||
|
dashboard_user.c.user_id == current_user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return query.filter(
|
||||||
|
or_(
|
||||||
|
and_(
|
||||||
|
FilterSet.owner_type == USER_OWNER_TYPE,
|
||||||
|
FilterSet.owner_id == current_user_id,
|
||||||
|
),
|
||||||
|
FilterSet.id.in_(filter_set_ids_by_dashboard_owners),
|
||||||
|
)
|
||||||
|
)
|
93
superset/dashboards/filter_sets/schemas.py
Normal file
93
superset/dashboards/filter_sets/schemas.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# 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, cast, Dict, Mapping
|
||||||
|
|
||||||
|
from marshmallow import fields, post_load, Schema, ValidationError
|
||||||
|
from marshmallow.validate import Length, OneOf
|
||||||
|
|
||||||
|
from superset.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_OWNER_TYPE,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
USER_OWNER_TYPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonMetadataSchema(Schema):
|
||||||
|
nativeFilters = fields.Mapping(required=True, allow_none=False)
|
||||||
|
dataMask = fields.Mapping(required=False, allow_none=False)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetSchema(Schema):
|
||||||
|
json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
|
||||||
|
|
||||||
|
def _validate_json_meta_data(self, json_meta_data: str) -> None:
|
||||||
|
try:
|
||||||
|
self.json_metadata_schema.loads(json_meta_data)
|
||||||
|
except Exception as ex:
|
||||||
|
raise ValidationError("failed to parse json_metadata to json") from ex
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetPostSchema(FilterSetSchema):
|
||||||
|
json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
|
||||||
|
# pylint: disable=W0613
|
||||||
|
name = fields.String(required=True, allow_none=False, validate=Length(0, 500),)
|
||||||
|
description = fields.String(
|
||||||
|
required=False, allow_none=True, validate=[Length(1, 1000)]
|
||||||
|
)
|
||||||
|
json_metadata = fields.String(allow_none=False, required=True)
|
||||||
|
|
||||||
|
owner_type = fields.String(
|
||||||
|
required=True, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])
|
||||||
|
)
|
||||||
|
owner_id = fields.Int(required=False)
|
||||||
|
|
||||||
|
@post_load
|
||||||
|
def validate(
|
||||||
|
self, data: Mapping[Any, Any], *, many: Any, partial: Any
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
self._validate_json_meta_data(data[JSON_METADATA_FIELD])
|
||||||
|
if data[OWNER_TYPE_FIELD] == USER_OWNER_TYPE and OWNER_ID_FIELD not in data:
|
||||||
|
raise ValidationError("owner_id is mandatory when owner_type is User")
|
||||||
|
return cast(Dict[str, Any], data)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetPutSchema(FilterSetSchema):
|
||||||
|
name = fields.String(required=False, allow_none=False, validate=Length(0, 500))
|
||||||
|
description = fields.String(
|
||||||
|
required=False, allow_none=False, validate=[Length(1, 1000)]
|
||||||
|
)
|
||||||
|
json_metadata = fields.String(required=False, allow_none=False)
|
||||||
|
owner_type = fields.String(
|
||||||
|
allow_none=False, required=False, validate=OneOf([DASHBOARD_OWNER_TYPE])
|
||||||
|
)
|
||||||
|
|
||||||
|
@post_load
|
||||||
|
def validate( # pylint: disable=unused-argument
|
||||||
|
self, data: Mapping[Any, Any], *, many: Any, partial: Any
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
if JSON_METADATA_FIELD in data:
|
||||||
|
self._validate_json_meta_data(data[JSON_METADATA_FIELD])
|
||||||
|
return cast(Dict[str, Any], data)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_pair(first_field: str, second_field: str, data: Dict[str, Any]) -> None:
|
||||||
|
if first_field in data and second_field not in data:
|
||||||
|
raise ValidationError(
|
||||||
|
"{} must be included alongside {}".format(first_field, second_field)
|
||||||
|
)
|
@ -141,6 +141,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||||||
from superset.queries.saved_queries.api import SavedQueryRestApi
|
from superset.queries.saved_queries.api import SavedQueryRestApi
|
||||||
from superset.reports.api import ReportScheduleRestApi
|
from superset.reports.api import ReportScheduleRestApi
|
||||||
from superset.reports.logs.api import ReportExecutionLogRestApi
|
from superset.reports.logs.api import ReportExecutionLogRestApi
|
||||||
|
from superset.dashboards.filter_sets.api import FilterSetRestApi
|
||||||
from superset.views.access_requests import AccessRequestsModelView
|
from superset.views.access_requests import AccessRequestsModelView
|
||||||
from superset.views.alerts import (
|
from superset.views.alerts import (
|
||||||
AlertLogModelView,
|
AlertLogModelView,
|
||||||
@ -208,6 +209,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||||||
appbuilder.add_api(SavedQueryRestApi)
|
appbuilder.add_api(SavedQueryRestApi)
|
||||||
appbuilder.add_api(ReportScheduleRestApi)
|
appbuilder.add_api(ReportScheduleRestApi)
|
||||||
appbuilder.add_api(ReportExecutionLogRestApi)
|
appbuilder.add_api(ReportExecutionLogRestApi)
|
||||||
|
appbuilder.add_api(FilterSetRestApi)
|
||||||
#
|
#
|
||||||
# Setup regular views
|
# Setup regular views
|
||||||
#
|
#
|
||||||
|
56
superset/migrations/versions/3ebe0993c770_filterset_table.py
Normal file
56
superset/migrations/versions/3ebe0993c770_filterset_table.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 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.
|
||||||
|
"""add filter set model
|
||||||
|
|
||||||
|
Revision ID: 3ebe0993c770
|
||||||
|
Revises: 07071313dd52
|
||||||
|
Create Date: 2021-03-29 11:15:48.831225
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "3ebe0993c770"
|
||||||
|
down_revision = "181091c0ef16"
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
"filter_sets",
|
||||||
|
sa.Column("created_on", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("changed_on", sa.DateTime(), nullable=True),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("name", sa.VARCHAR(500), nullable=False),
|
||||||
|
sa.Column("description", sa.Text(), nullable=True),
|
||||||
|
sa.Column("json_metadata", sa.Text(), nullable=False),
|
||||||
|
sa.Column("owner_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("owner_type", sa.VARCHAR(255), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"dashboard_id", sa.Integer(), sa.ForeignKey("dashboards.id"), nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("created_by_fk", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("changed_by_fk", sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(["changed_by_fk"], ["ab_user.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table("filter_sets")
|
@ -23,6 +23,7 @@ from functools import partial
|
|||||||
from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union
|
from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union
|
||||||
|
|
||||||
import sqlalchemy as sqla
|
import sqlalchemy as sqla
|
||||||
|
from flask import g
|
||||||
from flask_appbuilder import Model
|
from flask_appbuilder import Model
|
||||||
from flask_appbuilder.models.decorators import renders
|
from flask_appbuilder.models.decorators import renders
|
||||||
from flask_appbuilder.security.sqla.models import User
|
from flask_appbuilder.security.sqla.models import User
|
||||||
@ -46,10 +47,12 @@ from sqlalchemy.sql import join, select
|
|||||||
from sqlalchemy.sql.elements import BinaryExpression
|
from sqlalchemy.sql.elements import BinaryExpression
|
||||||
|
|
||||||
from superset import app, ConnectorRegistry, db, is_feature_enabled, security_manager
|
from superset import app, ConnectorRegistry, db, is_feature_enabled, security_manager
|
||||||
|
from superset.common.request_contexed_based import is_user_admin
|
||||||
from superset.connectors.base.models import BaseDatasource
|
from superset.connectors.base.models import BaseDatasource
|
||||||
from superset.connectors.druid.models import DruidColumn, DruidMetric
|
from superset.connectors.druid.models import DruidColumn, DruidMetric
|
||||||
from superset.connectors.sqla.models import SqlMetric, TableColumn
|
from superset.connectors.sqla.models import SqlMetric, TableColumn
|
||||||
from superset.extensions import cache_manager
|
from superset.extensions import cache_manager
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
|
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.models.tags import DashboardUpdater
|
from superset.models.tags import DashboardUpdater
|
||||||
@ -129,6 +132,7 @@ DashboardRoles = Table(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
||||||
"""The dashboard object!"""
|
"""The dashboard object!"""
|
||||||
|
|
||||||
@ -144,6 +148,9 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
|||||||
owners = relationship(security_manager.user_model, secondary=dashboard_user)
|
owners = relationship(security_manager.user_model, secondary=dashboard_user)
|
||||||
published = Column(Boolean, default=False)
|
published = Column(Boolean, default=False)
|
||||||
roles = relationship(security_manager.role_model, secondary=DashboardRoles)
|
roles = relationship(security_manager.role_model, secondary=DashboardRoles)
|
||||||
|
_filter_sets = relationship(
|
||||||
|
"FilterSet", back_populates="dashboard", cascade="all, delete"
|
||||||
|
)
|
||||||
export_fields = [
|
export_fields = [
|
||||||
"dashboard_title",
|
"dashboard_title",
|
||||||
"position_json",
|
"position_json",
|
||||||
@ -178,6 +185,29 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
|||||||
.all()
|
.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter_sets(self) -> Dict[int, FilterSet]:
|
||||||
|
return {fs.id: fs for fs in self._filter_sets}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter_sets_lst(self) -> Dict[int, FilterSet]:
|
||||||
|
if is_user_admin():
|
||||||
|
return self._filter_sets
|
||||||
|
current_user = g.user.id
|
||||||
|
filter_sets_by_owner_type: Dict[str, List[Any]] = {"Dashboard": [], "User": []}
|
||||||
|
for fs in self._filter_sets:
|
||||||
|
filter_sets_by_owner_type[fs.owner_type].append(fs)
|
||||||
|
user_filter_sets = list(
|
||||||
|
filter(
|
||||||
|
lambda filter_set: filter_set.owner_id == current_user,
|
||||||
|
filter_sets_by_owner_type["User"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
fs.id: fs
|
||||||
|
for fs in user_filter_sets + filter_sets_by_owner_type["Dashboard"]
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def charts(self) -> List[BaseDatasource]:
|
def charts(self) -> List[BaseDatasource]:
|
||||||
return [slc.chart for slc in self.slices]
|
return [slc.chart for slc in self.slices]
|
||||||
@ -397,6 +427,11 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
|||||||
qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug))
|
qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug))
|
||||||
return qry.one_or_none()
|
return qry.one_or_none()
|
||||||
|
|
||||||
|
def is_actor_owner(self) -> bool:
|
||||||
|
if g.user is None or g.user.is_anonymous or not g.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
return g.user.id in set(map(lambda user: user.id, self.owners))
|
||||||
|
|
||||||
|
|
||||||
def id_or_slug_filter(id_or_slug: str) -> BinaryExpression:
|
def id_or_slug_filter(id_or_slug: str) -> BinaryExpression:
|
||||||
if id_or_slug.isdigit():
|
if id_or_slug.isdigit():
|
||||||
|
106
superset/models/filter_set.py
Normal file
106
superset/models/filter_set.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from flask_appbuilder import Model
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy_utils import generic_relationship
|
||||||
|
|
||||||
|
from superset import app, db
|
||||||
|
from superset.models.helpers import AuditMixinNullable
|
||||||
|
|
||||||
|
metadata = Model.metadata # pylint: disable=no-member
|
||||||
|
config = app.config
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSet(Model, AuditMixinNullable):
|
||||||
|
__tablename__ = "filter_sets"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(500), nullable=False, unique=True)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
json_metadata = Column(Text, nullable=False)
|
||||||
|
dashboard_id = Column(Integer, ForeignKey("dashboards.id"))
|
||||||
|
dashboard = relationship("Dashboard", back_populates="_filter_sets")
|
||||||
|
owner_id = Column(Integer, nullable=False)
|
||||||
|
owner_type = Column(String(255), nullable=False)
|
||||||
|
owner_object = generic_relationship(owner_type, owner_id)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"FilterSet<{self.name or self.id}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
return f"/api/filtersets/{self.id}/"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sqla_metadata(self) -> None:
|
||||||
|
# pylint: disable=no-member
|
||||||
|
meta = MetaData(bind=self.get_sqla_engine())
|
||||||
|
meta.reflect()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changed_by_name(self) -> str:
|
||||||
|
if not self.changed_by:
|
||||||
|
return ""
|
||||||
|
return str(self.changed_by)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changed_by_url(self) -> str:
|
||||||
|
if not self.changed_by:
|
||||||
|
return ""
|
||||||
|
return f"/superset/profile/{self.changed_by.username}"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"params": self.params,
|
||||||
|
"dashboard_id": self.dashboard_id,
|
||||||
|
"owner_type": self.owner_type,
|
||||||
|
"owner_id": self.owner_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, _id: int) -> FilterSet:
|
||||||
|
session = db.session()
|
||||||
|
qry = session.query(FilterSet).filter(_id)
|
||||||
|
return qry.one_or_none()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_name(cls, name: str) -> FilterSet:
|
||||||
|
session = db.session()
|
||||||
|
qry = session.query(FilterSet).filter(FilterSet.name == name)
|
||||||
|
return qry.one_or_none()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_dashboard_id(cls, dashboard_id: int) -> FilterSet:
|
||||||
|
session = db.session()
|
||||||
|
qry = session.query(FilterSet).filter(FilterSet.dashboard_id == dashboard_id)
|
||||||
|
return qry.all()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def params(self) -> Dict[str, Any]:
|
||||||
|
if self.json_metadata:
|
||||||
|
return json.loads(self.json_metadata)
|
||||||
|
return {}
|
16
tests/integration_tests/dashboards/filter_sets/__init__.py
Normal file
16
tests/integration_tests/dashboards/filter_sets/__init__.py
Normal 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.
|
321
tests/integration_tests/dashboards/filter_sets/conftest.py
Normal file
321
tests/integration_tests/dashboards/filter_sets/conftest.py
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict, Generator, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from superset import security_manager as sm
|
||||||
|
from superset.dashboards.filter_sets.consts import (
|
||||||
|
DESCRIPTION_FIELD,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
NAME_FIELD,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
USER_OWNER_TYPE,
|
||||||
|
)
|
||||||
|
from superset.models.dashboard import Dashboard
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.consts import (
|
||||||
|
ADMIN_USERNAME_FOR_TEST,
|
||||||
|
DASHBOARD_OWNER_USERNAME,
|
||||||
|
FILTER_SET_OWNER_USERNAME,
|
||||||
|
REGULAR_USER,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.dashboards.superset_factory_util import (
|
||||||
|
create_dashboard,
|
||||||
|
create_database,
|
||||||
|
create_datasource_table,
|
||||||
|
create_slice,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.test_app import app
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from flask.ctx import AppContext
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
from flask_appbuilder.security.sqla.models import (
|
||||||
|
Role,
|
||||||
|
User,
|
||||||
|
ViewMenu,
|
||||||
|
PermissionView,
|
||||||
|
)
|
||||||
|
from flask_appbuilder.security.manager import BaseSecurityManager
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from superset.models.slice import Slice
|
||||||
|
from superset.connectors.sqla.models import SqlaTable
|
||||||
|
from superset.models.core import Database
|
||||||
|
|
||||||
|
|
||||||
|
security_manager: BaseSecurityManager = sm
|
||||||
|
|
||||||
|
|
||||||
|
# @pytest.fixture(autouse=True, scope="session")
|
||||||
|
# def setup_sample_data() -> Any:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def expire_on_commit_true() -> Generator[None, None, None]:
|
||||||
|
ctx: AppContext
|
||||||
|
with app.app_context() as ctx:
|
||||||
|
ctx.app.appbuilder.get_session.configure(expire_on_commit=False)
|
||||||
|
yield
|
||||||
|
ctx.app.appbuilder.get_session.configure(expire_on_commit=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope="module")
|
||||||
|
def test_users() -> Generator[Dict[str, int], None, None]:
|
||||||
|
usernames = [
|
||||||
|
ADMIN_USERNAME_FOR_TEST,
|
||||||
|
DASHBOARD_OWNER_USERNAME,
|
||||||
|
FILTER_SET_OWNER_USERNAME,
|
||||||
|
REGULAR_USER,
|
||||||
|
]
|
||||||
|
with app.app_context():
|
||||||
|
filter_set_role = build_filter_set_role()
|
||||||
|
admin_role: Role = security_manager.find_role("Admin")
|
||||||
|
usernames_to_ids = create_test_users(admin_role, filter_set_role, usernames)
|
||||||
|
yield usernames_to_ids
|
||||||
|
ctx: AppContext
|
||||||
|
delete_users(usernames_to_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_users(usernames_to_ids: Dict[str, int]) -> None:
|
||||||
|
with app.app_context() as ctx:
|
||||||
|
session: Session = ctx.app.appbuilder.get_session
|
||||||
|
for username in usernames_to_ids.keys():
|
||||||
|
session.delete(security_manager.find_user(username))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_users(
|
||||||
|
admin_role: Role, filter_set_role: Role, usernames: List[str]
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
users: List[User] = []
|
||||||
|
for username in usernames:
|
||||||
|
user = build_user(username, filter_set_role, admin_role)
|
||||||
|
users.append(user)
|
||||||
|
return {user.username: user.id for user in users}
|
||||||
|
|
||||||
|
|
||||||
|
def build_user(username: str, filter_set_role: Role, admin_role: Role) -> User:
|
||||||
|
roles_to_add = (
|
||||||
|
[admin_role] if username == ADMIN_USERNAME_FOR_TEST else [filter_set_role]
|
||||||
|
)
|
||||||
|
user: User = security_manager.add_user(
|
||||||
|
username, "test", "test", username, roles_to_add, password="general"
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
user = security_manager.find_user(username)
|
||||||
|
if user is None:
|
||||||
|
raise Exception("Failed to build the user {}".format(username))
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def build_filter_set_role() -> Role:
|
||||||
|
filter_set_role: Role = security_manager.add_role("filter_set_role")
|
||||||
|
filterset_view_name: ViewMenu = security_manager.find_view_menu("FilterSets")
|
||||||
|
all_datasource_view_name: ViewMenu = security_manager.find_view_menu(
|
||||||
|
"all_datasource_access"
|
||||||
|
)
|
||||||
|
pvms: List[PermissionView] = security_manager.find_permissions_view_menu(
|
||||||
|
filterset_view_name
|
||||||
|
) + security_manager.find_permissions_view_menu(all_datasource_view_name)
|
||||||
|
for pvm in pvms:
|
||||||
|
security_manager.add_permission_role(filter_set_role, pvm)
|
||||||
|
return filter_set_role
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> Generator[FlaskClient[Any], None, None]:
|
||||||
|
with app.test_client() as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dashboard() -> Generator[Dashboard, None, None]:
|
||||||
|
dashboard: Dashboard
|
||||||
|
slice_: Slice
|
||||||
|
datasource: SqlaTable
|
||||||
|
database: Database
|
||||||
|
session: Session
|
||||||
|
try:
|
||||||
|
with app.app_context() as ctx:
|
||||||
|
dashboard_owner_user = security_manager.find_user(DASHBOARD_OWNER_USERNAME)
|
||||||
|
database = create_database("test_database_filter_sets")
|
||||||
|
datasource = create_datasource_table(
|
||||||
|
name="test_datasource", database=database, owners=[dashboard_owner_user]
|
||||||
|
)
|
||||||
|
slice_ = create_slice(
|
||||||
|
datasource=datasource, name="test_slice", owners=[dashboard_owner_user]
|
||||||
|
)
|
||||||
|
dashboard = create_dashboard(
|
||||||
|
dashboard_title="test_dashboard",
|
||||||
|
published=True,
|
||||||
|
slices=[slice_],
|
||||||
|
owners=[dashboard_owner_user],
|
||||||
|
)
|
||||||
|
session = ctx.app.appbuilder.get_session
|
||||||
|
session.add(dashboard)
|
||||||
|
session.commit()
|
||||||
|
yield dashboard
|
||||||
|
except Exception as ex:
|
||||||
|
print(str(ex))
|
||||||
|
finally:
|
||||||
|
with app.app_context() as ctx:
|
||||||
|
session = ctx.app.appbuilder.get_session
|
||||||
|
try:
|
||||||
|
dashboard.owners = []
|
||||||
|
slice_.owners = []
|
||||||
|
datasource.owners = []
|
||||||
|
session.merge(dashboard)
|
||||||
|
session.merge(slice_)
|
||||||
|
session.merge(datasource)
|
||||||
|
session.commit()
|
||||||
|
session.delete(dashboard)
|
||||||
|
session.delete(slice_)
|
||||||
|
session.delete(datasource)
|
||||||
|
session.delete(database)
|
||||||
|
session.commit()
|
||||||
|
except Exception as ex:
|
||||||
|
print(str(ex))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dashboard_id(dashboard) -> int:
|
||||||
|
return dashboard.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def filtersets(
|
||||||
|
dashboard_id: int, test_users: Dict[str, int], dumped_valid_json_metadata: str
|
||||||
|
) -> Generator[Dict[str, List[FilterSet]], None, None]:
|
||||||
|
try:
|
||||||
|
with app.app_context() as ctx:
|
||||||
|
session: Session = ctx.app.appbuilder.get_session
|
||||||
|
first_filter_set = FilterSet(
|
||||||
|
name="filter_set_1_of_" + str(dashboard_id),
|
||||||
|
dashboard_id=dashboard_id,
|
||||||
|
json_metadata=dumped_valid_json_metadata,
|
||||||
|
owner_id=dashboard_id,
|
||||||
|
owner_type="Dashboard",
|
||||||
|
)
|
||||||
|
second_filter_set = FilterSet(
|
||||||
|
name="filter_set_2_of_" + str(dashboard_id),
|
||||||
|
json_metadata=dumped_valid_json_metadata,
|
||||||
|
dashboard_id=dashboard_id,
|
||||||
|
owner_id=dashboard_id,
|
||||||
|
owner_type="Dashboard",
|
||||||
|
)
|
||||||
|
third_filter_set = FilterSet(
|
||||||
|
name="filter_set_3_of_" + str(dashboard_id),
|
||||||
|
json_metadata=dumped_valid_json_metadata,
|
||||||
|
dashboard_id=dashboard_id,
|
||||||
|
owner_id=test_users[FILTER_SET_OWNER_USERNAME],
|
||||||
|
owner_type="User",
|
||||||
|
)
|
||||||
|
forth_filter_set = FilterSet(
|
||||||
|
name="filter_set_4_of_" + str(dashboard_id),
|
||||||
|
json_metadata=dumped_valid_json_metadata,
|
||||||
|
dashboard_id=dashboard_id,
|
||||||
|
owner_id=test_users[FILTER_SET_OWNER_USERNAME],
|
||||||
|
owner_type="User",
|
||||||
|
)
|
||||||
|
session.add(first_filter_set)
|
||||||
|
session.add(second_filter_set)
|
||||||
|
session.add(third_filter_set)
|
||||||
|
session.add(forth_filter_set)
|
||||||
|
session.commit()
|
||||||
|
yv = {
|
||||||
|
"Dashboard": [first_filter_set, second_filter_set],
|
||||||
|
FILTER_SET_OWNER_USERNAME: [third_filter_set, forth_filter_set],
|
||||||
|
}
|
||||||
|
yield yv
|
||||||
|
except Exception as ex:
|
||||||
|
print(str(ex))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def filterset_id(filtersets: Dict[str, List[FilterSet]]) -> int:
|
||||||
|
return filtersets["Dashboard"][0].id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def valid_json_metadata() -> Dict[str, Any]:
|
||||||
|
return {"nativeFilters": {}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dumped_valid_json_metadata(valid_json_metadata: Dict[str, Any]) -> str:
|
||||||
|
return json.dumps(valid_json_metadata)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def exists_user_id() -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def valid_filter_set_data_for_create(
|
||||||
|
dashboard_id: int, dumped_valid_json_metadata: str, exists_user_id: int
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
name = "test_filter_set_of_dashboard_" + str(dashboard_id)
|
||||||
|
return {
|
||||||
|
NAME_FIELD: name,
|
||||||
|
DESCRIPTION_FIELD: "description of " + name,
|
||||||
|
JSON_METADATA_FIELD: dumped_valid_json_metadata,
|
||||||
|
OWNER_TYPE_FIELD: USER_OWNER_TYPE,
|
||||||
|
OWNER_ID_FIELD: exists_user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def valid_filter_set_data_for_update(
|
||||||
|
dashboard_id: int, dumped_valid_json_metadata: str, exists_user_id: int
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
name = "name_changed_test_filter_set_of_dashboard_" + str(dashboard_id)
|
||||||
|
return {
|
||||||
|
NAME_FIELD: name,
|
||||||
|
DESCRIPTION_FIELD: "changed description of " + name,
|
||||||
|
JSON_METADATA_FIELD: dumped_valid_json_metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def not_exists_dashboard(dashboard_id: int) -> int:
|
||||||
|
return dashboard_id + 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def not_exists_user_id() -> int:
|
||||||
|
return 99999
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def dashboard_based_filter_set_dict(
|
||||||
|
filtersets: Dict[str, List[FilterSet]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return filtersets["Dashboard"][0].to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def user_based_filter_set_dict(
|
||||||
|
filtersets: Dict[str, List[FilterSet]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return filtersets[FILTER_SET_OWNER_USERNAME][0].to_dict()
|
22
tests/integration_tests/dashboards/filter_sets/consts.py
Normal file
22
tests/integration_tests/dashboards/filter_sets/consts.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 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.
|
||||||
|
FILTER_SET_URI = "api/v1/dashboard/{dashboard_id}/filtersets"
|
||||||
|
|
||||||
|
ADMIN_USERNAME_FOR_TEST = "admin@filterset.com"
|
||||||
|
DASHBOARD_OWNER_USERNAME = "dash_owner_user@filterset.com"
|
||||||
|
FILTER_SET_OWNER_USERNAME = "fs_owner_user@filterset.com"
|
||||||
|
REGULAR_USER = "regular_user@filterset.com"
|
@ -0,0 +1,630 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
|
from superset.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_OWNER_TYPE,
|
||||||
|
DESCRIPTION_FIELD,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
NAME_FIELD,
|
||||||
|
OWNER_ID_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
USER_OWNER_TYPE,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.base_tests import login
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.consts import (
|
||||||
|
ADMIN_USERNAME_FOR_TEST,
|
||||||
|
DASHBOARD_OWNER_USERNAME,
|
||||||
|
FILTER_SET_OWNER_USERNAME,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.utils import (
|
||||||
|
call_create_filter_set,
|
||||||
|
get_filter_set_by_dashboard_id,
|
||||||
|
get_filter_set_by_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
|
||||||
|
|
||||||
|
def assert_filterset_was_not_created(filter_set_data: Dict[str, Any]) -> None:
|
||||||
|
assert get_filter_set_by_name(str(filter_set_data["name"])) is None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_filterset_was_created(filter_set_data: Dict[str, Any]) -> None:
|
||||||
|
assert get_filter_set_by_name(filter_set_data["name"]) is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateFilterSetsApi:
|
||||||
|
def test_with_extra_field__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create["extra"] = "val"
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json["message"]["extra"][0] == "Unknown field."
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_id_field__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create["id"] = 1
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json["message"]["id"][0] == "Unknown field."
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_dashboard_not_exists__404(
|
||||||
|
self,
|
||||||
|
not_exists_dashboard: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# act
|
||||||
|
login(client, "admin")
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, not_exists_dashboard, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_without_name__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create.pop(NAME_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert get_filter_set_by_dashboard_id(dashboard_id) == []
|
||||||
|
|
||||||
|
def test_with_none_name__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[NAME_FIELD] = None
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_int_as_name__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[NAME_FIELD] = 4
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_without_description__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create.pop(DESCRIPTION_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_none_description__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[DESCRIPTION_FIELD] = None
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_int_as_description__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[DESCRIPTION_FIELD] = 1
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_without_json_metadata__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create.pop(JSON_METADATA_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_invalid_json_metadata__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[DESCRIPTION_FIELD] = {}
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_without_owner_type__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create.pop(OWNER_TYPE_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_invalid_owner_type__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = "OTHER_TYPE"
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_without_owner_id_when_owner_type_is_user__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create.pop(OWNER_ID_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_without_owner_id_when_owner_type_is_dashboard__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create.pop(OWNER_ID_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_with_not_exists_owner__400(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
not_exists_user_id: int,
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = not_exists_user_id
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_is_admin__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
ADMIN_USERNAME_FOR_TEST
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_is_dashboard_owner__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
DASHBOARD_OWNER_USERNAME
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_is_regular_user__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
FILTER_SET_OWNER_USERNAME
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_type_is_dashboard__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_is_admin__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
ADMIN_USERNAME_FOR_TEST
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
DASHBOARD_OWNER_USERNAME
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
FILTER_SET_OWNER_USERNAME
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_is_admin__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, FILTER_SET_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
ADMIN_USERNAME_FOR_TEST
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, FILTER_SET_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
DASHBOARD_OWNER_USERNAME
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_is_regular_user__201(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, FILTER_SET_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = USER_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = test_users[
|
||||||
|
FILTER_SET_OWNER_USERNAME
|
||||||
|
]
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert_filterset_was_created(valid_filter_set_data_for_create)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
valid_filter_set_data_for_create: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, FILTER_SET_OWNER_USERNAME)
|
||||||
|
valid_filter_set_data_for_create[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE
|
||||||
|
valid_filter_set_data_for_create[OWNER_ID_FIELD] = dashboard_id
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_create_filter_set(
|
||||||
|
client, dashboard_id, valid_filter_set_data_for_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert_filterset_was_not_created(valid_filter_set_data_for_create)
|
@ -0,0 +1,209 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from tests.integration_tests.base_tests import login
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_OWNER_USERNAME,
|
||||||
|
FILTER_SET_OWNER_USERNAME,
|
||||||
|
REGULAR_USER,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.utils import (
|
||||||
|
call_delete_filter_set,
|
||||||
|
collect_all_ids,
|
||||||
|
get_filter_set_by_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
|
||||||
|
|
||||||
|
def assert_filterset_was_not_deleted(filter_set_dict: Dict[str, Any]) -> None:
|
||||||
|
assert get_filter_set_by_name(filter_set_dict["name"]) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_filterset_deleted(filter_set_dict: Dict[str, Any]) -> None:
|
||||||
|
assert get_filter_set_by_name(filter_set_dict["name"]) is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeleteFilterSet:
|
||||||
|
def test_with_dashboard_exists_filterset_not_exists__200(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
filtersets: Dict[str, List[FilterSet]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
||||||
|
|
||||||
|
response = call_delete_filter_set(client, {"id": filter_set_id}, dashboard_id)
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_with_dashboard_not_exists_filterset_not_exists__404(
|
||||||
|
self,
|
||||||
|
not_exists_dashboard: int,
|
||||||
|
filtersets: Dict[str, List[FilterSet]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
||||||
|
|
||||||
|
response = call_delete_filter_set(
|
||||||
|
client, {"id": filter_set_id}, not_exists_dashboard
|
||||||
|
)
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_with_dashboard_not_exists_filterset_exists__404(
|
||||||
|
self,
|
||||||
|
not_exists_dashboard: int,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, not_exists_dashboard
|
||||||
|
)
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert_filterset_was_not_deleted(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_type_is_user__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_deleted(user_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_type_is_dashboard__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(client, dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_deleted(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_is_other_user_403(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert_filterset_was_not_deleted(user_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(client, dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_deleted(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_filterset_owner__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, FILTER_SET_OWNER_USERNAME)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_deleted(user_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_type_is_user__403(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, REGULAR_USER)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(client, user_based_filter_set_dict)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert_filterset_was_not_deleted(user_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, REGULAR_USER)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_delete_filter_set(client, dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert_filterset_was_not_deleted(dashboard_based_filter_set_dict)
|
129
tests/integration_tests/dashboards/filter_sets/get_api_tests.py
Normal file
129
tests/integration_tests/dashboards/filter_sets/get_api_tests.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Set, TYPE_CHECKING
|
||||||
|
|
||||||
|
from tests.integration_tests.base_tests import login
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_OWNER_USERNAME,
|
||||||
|
FILTER_SET_OWNER_USERNAME,
|
||||||
|
REGULAR_USER,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.utils import (
|
||||||
|
call_get_filter_sets,
|
||||||
|
collect_all_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetFilterSetsApi:
|
||||||
|
def test_with_dashboard_not_exists__404(
|
||||||
|
self, not_exists_dashboard: int, client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_get_filter_sets(client, not_exists_dashboard)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_dashboards_without_filtersets__200(
|
||||||
|
self, dashboard_id: int, client: FlaskClient[Any]
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_get_filter_sets(client, dashboard_id)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.is_json and response.json["count"] == 0
|
||||||
|
|
||||||
|
def test_when_caller_admin__200(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
filtersets: Dict[str, List[FilterSet]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
expected_ids: Set[int] = collect_all_ids(filtersets)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_get_filter_sets(client, dashboard_id)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.is_json and set(response.json["ids"]) == expected_ids
|
||||||
|
|
||||||
|
def test_when_caller_dashboard_owner__200(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
filtersets: Dict[str, List[FilterSet]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
expected_ids = collect_all_ids(filtersets["Dashboard"])
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_get_filter_sets(client, dashboard_id)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.is_json and set(response.json["ids"]) == expected_ids
|
||||||
|
|
||||||
|
def test_when_caller_filterset_owner__200(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
filtersets: Dict[str, List[FilterSet]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, FILTER_SET_OWNER_USERNAME)
|
||||||
|
expected_ids = collect_all_ids(filtersets[FILTER_SET_OWNER_USERNAME])
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_get_filter_sets(client, dashboard_id)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.is_json and set(response.json["ids"]) == expected_ids
|
||||||
|
|
||||||
|
def test_when_caller_regular_user__200(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
filtersets: Dict[str, List[int]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, REGULAR_USER)
|
||||||
|
expected_ids: Set[int] = set()
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_get_filter_sets(client, dashboard_id)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.is_json and set(response.json["ids"]) == expected_ids
|
@ -0,0 +1,519 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from superset.dashboards.filter_sets.consts import (
|
||||||
|
DESCRIPTION_FIELD,
|
||||||
|
JSON_METADATA_FIELD,
|
||||||
|
NAME_FIELD,
|
||||||
|
OWNER_TYPE_FIELD,
|
||||||
|
PARAMS_PROPERTY,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.base_tests import login
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.consts import (
|
||||||
|
DASHBOARD_OWNER_USERNAME,
|
||||||
|
FILTER_SET_OWNER_USERNAME,
|
||||||
|
REGULAR_USER,
|
||||||
|
)
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.utils import (
|
||||||
|
call_update_filter_set,
|
||||||
|
collect_all_ids,
|
||||||
|
get_filter_set_by_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
|
||||||
|
|
||||||
|
def merge_two_filter_set_dict(
|
||||||
|
first: Dict[Any, Any], second: Dict[Any, Any]
|
||||||
|
) -> Dict[Any, Any]:
|
||||||
|
for d in [first, second]:
|
||||||
|
if JSON_METADATA_FIELD in d:
|
||||||
|
if PARAMS_PROPERTY not in d:
|
||||||
|
d.setdefault(PARAMS_PROPERTY, json.loads(d[JSON_METADATA_FIELD]))
|
||||||
|
d.pop(JSON_METADATA_FIELD)
|
||||||
|
return {**first, **second}
|
||||||
|
|
||||||
|
|
||||||
|
def assert_filterset_was_not_updated(filter_set_dict: Dict[str, Any]) -> None:
|
||||||
|
assert filter_set_dict == get_filter_set_by_name(filter_set_dict["name"]).to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def assert_filterset_updated(
|
||||||
|
filter_set_dict_before: Dict[str, Any], data_updated: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
expected_data = merge_two_filter_set_dict(filter_set_dict_before, data_updated)
|
||||||
|
assert expected_data == get_filter_set_by_name(expected_data["name"]).to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdateFilterSet:
|
||||||
|
def test_with_dashboard_exists_filterset_not_exists__404(
|
||||||
|
self,
|
||||||
|
dashboard_id: int,
|
||||||
|
filtersets: Dict[str, List[FilterSet]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
||||||
|
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, {"id": filter_set_id}, {}, dashboard_id
|
||||||
|
)
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_with_dashboard_not_exists_filterset_not_exists__404(
|
||||||
|
self,
|
||||||
|
not_exists_dashboard: int,
|
||||||
|
filtersets: Dict[str, List[FilterSet]],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
filter_set_id = max(collect_all_ids(filtersets)) + 1
|
||||||
|
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, {"id": filter_set_id}, {}, not_exists_dashboard
|
||||||
|
)
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
def test_with_dashboard_not_exists_filterset_exists__404(
|
||||||
|
self,
|
||||||
|
not_exists_dashboard: int,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, {}, not_exists_dashboard
|
||||||
|
)
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_extra_field__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update["extra"] = "val"
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json["message"]["extra"][0] == "Unknown field."
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_id_field__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update["id"] = 1
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json["message"]["id"][0] == "Unknown field."
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_none_name__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[NAME_FIELD] = None
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_int_as_name__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[NAME_FIELD] = 4
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_without_name__200(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update.pop(NAME_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_updated(
|
||||||
|
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_none_description__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[DESCRIPTION_FIELD] = None
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_int_as_description__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[DESCRIPTION_FIELD] = 1
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_without_description__200(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update.pop(DESCRIPTION_FIELD, None)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_updated(
|
||||||
|
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_invalid_json_metadata__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[DESCRIPTION_FIELD] = {}
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_json_metadata__200(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
valid_json_metadata: Dict[Any, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_json_metadata["nativeFilters"] = {"changed": "changed"}
|
||||||
|
valid_filter_set_data_for_update[JSON_METADATA_FIELD] = json.dumps(
|
||||||
|
valid_json_metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_updated(
|
||||||
|
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_invalid_owner_type__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "OTHER_TYPE"
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_user_owner_type__400(
|
||||||
|
self,
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "User"
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_with_dashboard_owner_type__200(
|
||||||
|
self,
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
valid_filter_set_data_for_update[OWNER_TYPE_FIELD] = "Dashboard"
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
user_based_filter_set_dict["owner_id"] = user_based_filter_set_dict[
|
||||||
|
"dashboard_id"
|
||||||
|
]
|
||||||
|
assert_filterset_updated(
|
||||||
|
user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_type_is_user__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_updated(
|
||||||
|
user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_when_caller_is_admin_and_owner_type_is_dashboard__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, "admin")
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_updated(
|
||||||
|
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_is_other_user_403(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert_filterset_was_not_updated(user_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, DASHBOARD_OWNER_USERNAME)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_updated(
|
||||||
|
dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_when_caller_is_filterset_owner__200(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, FILTER_SET_OWNER_USERNAME)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_filterset_updated(
|
||||||
|
user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_type_is_user__403(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
user_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, REGULAR_USER)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, user_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert_filterset_was_not_updated(user_based_filter_set_dict)
|
||||||
|
|
||||||
|
def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(
|
||||||
|
self,
|
||||||
|
test_users: Dict[str, int],
|
||||||
|
dashboard_based_filter_set_dict: Dict[str, Any],
|
||||||
|
valid_filter_set_data_for_update: Dict[str, Any],
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
):
|
||||||
|
# arrange
|
||||||
|
login(client, REGULAR_USER)
|
||||||
|
|
||||||
|
# act
|
||||||
|
response = call_update_filter_set(
|
||||||
|
client, dashboard_based_filter_set_dict, valid_filter_set_data_for_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert_filterset_was_not_updated(dashboard_based_filter_set_dict)
|
102
tests/integration_tests/dashboards/filter_sets/utils.py
Normal file
102
tests/integration_tests/dashboards/filter_sets/utils.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# 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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
from superset.models.filter_set import FilterSet
|
||||||
|
from tests.integration_tests.dashboards.filter_sets.consts import FILTER_SET_URI
|
||||||
|
from tests.integration_tests.test_app import app
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from flask import Response
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
|
||||||
|
|
||||||
|
def call_create_filter_set(
|
||||||
|
client: FlaskClient[Any], dashboard_id: int, data: Dict[str, Any]
|
||||||
|
) -> Response:
|
||||||
|
uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
|
||||||
|
return client.post(uri, json=data)
|
||||||
|
|
||||||
|
|
||||||
|
def call_get_filter_sets(client: FlaskClient[Any], dashboard_id: int) -> Response:
|
||||||
|
uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
|
||||||
|
return client.get(uri)
|
||||||
|
|
||||||
|
|
||||||
|
def call_delete_filter_set(
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
filter_set_dict_to_update: Dict[str, Any],
|
||||||
|
dashboard_id: Optional[int] = None,
|
||||||
|
) -> Response:
|
||||||
|
dashboard_id = (
|
||||||
|
dashboard_id
|
||||||
|
if dashboard_id is not None
|
||||||
|
else filter_set_dict_to_update["dashboard_id"]
|
||||||
|
)
|
||||||
|
uri = "{}/{}".format(
|
||||||
|
FILTER_SET_URI.format(dashboard_id=dashboard_id),
|
||||||
|
filter_set_dict_to_update["id"],
|
||||||
|
)
|
||||||
|
return client.delete(uri)
|
||||||
|
|
||||||
|
|
||||||
|
def call_update_filter_set(
|
||||||
|
client: FlaskClient[Any],
|
||||||
|
filter_set_dict_to_update: Dict[str, Any],
|
||||||
|
data: Dict[str, Any],
|
||||||
|
dashboard_id: Optional[int] = None,
|
||||||
|
) -> Response:
|
||||||
|
dashboard_id = (
|
||||||
|
dashboard_id
|
||||||
|
if dashboard_id is not None
|
||||||
|
else filter_set_dict_to_update["dashboard_id"]
|
||||||
|
)
|
||||||
|
uri = "{}/{}".format(
|
||||||
|
FILTER_SET_URI.format(dashboard_id=dashboard_id),
|
||||||
|
filter_set_dict_to_update["id"],
|
||||||
|
)
|
||||||
|
return client.put(uri, json=data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_filter_set_by_name(name: str) -> FilterSet:
|
||||||
|
with app.app_context():
|
||||||
|
return FilterSet.get_by_name(name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_filter_set_by_id(id_: int) -> FilterSet:
|
||||||
|
with app.app_context():
|
||||||
|
return FilterSet.get(id_)
|
||||||
|
|
||||||
|
|
||||||
|
def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet:
|
||||||
|
with app.app_context():
|
||||||
|
return FilterSet.get_by_dashboard_id(dashboard_id)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_all_ids(
|
||||||
|
filtersets: Union[Dict[str, List[FilterSet]], List[FilterSet]]
|
||||||
|
) -> Set[int]:
|
||||||
|
if isinstance(filtersets, dict):
|
||||||
|
filtersets_lists: List[List[FilterSet]] = list(filtersets.values())
|
||||||
|
ids: Set[int] = set()
|
||||||
|
lst: List[FilterSet]
|
||||||
|
for lst in filtersets_lists:
|
||||||
|
ids.update(set(map(lambda fs: fs.id, lst)))
|
||||||
|
return ids
|
||||||
|
return set(map(lambda fs: fs.id, filtersets))
|
@ -82,10 +82,10 @@ def create_dashboard(
|
|||||||
json_metadata: str = "",
|
json_metadata: str = "",
|
||||||
position_json: str = "",
|
position_json: str = "",
|
||||||
) -> Dashboard:
|
) -> Dashboard:
|
||||||
dashboard_title = dashboard_title or random_title()
|
dashboard_title = dashboard_title if dashboard_title is not None else random_title()
|
||||||
slug = slug or random_slug()
|
slug = slug if slug is not None else random_slug()
|
||||||
owners = owners or []
|
owners = owners if owners is not None else []
|
||||||
slices = slices or []
|
slices = slices if slices is not None else []
|
||||||
return Dashboard(
|
return Dashboard(
|
||||||
dashboard_title=dashboard_title,
|
dashboard_title=dashboard_title,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
@ -109,25 +109,40 @@ def create_slice_to_db(
|
|||||||
datasource_id: Optional[int] = None,
|
datasource_id: Optional[int] = None,
|
||||||
owners: Optional[List[User]] = None,
|
owners: Optional[List[User]] = None,
|
||||||
) -> Slice:
|
) -> Slice:
|
||||||
slice_ = create_slice(datasource_id, name, owners)
|
slice_ = create_slice(datasource_id, name=name, owners=owners)
|
||||||
insert_model(slice_)
|
insert_model(slice_)
|
||||||
inserted_slices_ids.append(slice_.id)
|
inserted_slices_ids.append(slice_.id)
|
||||||
return slice_
|
return slice_
|
||||||
|
|
||||||
|
|
||||||
def create_slice(
|
def create_slice(
|
||||||
datasource_id: Optional[int], name: Optional[str], owners: Optional[List[User]]
|
datasource_id: Optional[int] = None,
|
||||||
|
datasource: Optional[SqlaTable] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
owners: Optional[List[User]] = None,
|
||||||
) -> Slice:
|
) -> Slice:
|
||||||
name = name or random_str()
|
name = name if name is not None else random_str()
|
||||||
owners = owners or []
|
owners = owners if owners is not None else []
|
||||||
datasource_id = (
|
datasource_type = "table"
|
||||||
datasource_id or create_datasource_table_to_db(name=name + "_table").id
|
if datasource:
|
||||||
|
return Slice(
|
||||||
|
slice_name=name,
|
||||||
|
table=datasource,
|
||||||
|
owners=owners,
|
||||||
|
datasource_type=datasource_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
datasource_id = (
|
||||||
|
datasource_id
|
||||||
|
if datasource_id is not None
|
||||||
|
else create_datasource_table_to_db(name=name + "_table").id
|
||||||
|
)
|
||||||
|
|
||||||
return Slice(
|
return Slice(
|
||||||
slice_name=name,
|
slice_name=name,
|
||||||
datasource_id=datasource_id,
|
datasource_id=datasource_id,
|
||||||
owners=owners,
|
owners=owners,
|
||||||
datasource_type="table",
|
datasource_type=datasource_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -136,7 +151,7 @@ def create_datasource_table_to_db(
|
|||||||
db_id: Optional[int] = None,
|
db_id: Optional[int] = None,
|
||||||
owners: Optional[List[User]] = None,
|
owners: Optional[List[User]] = None,
|
||||||
) -> SqlaTable:
|
) -> SqlaTable:
|
||||||
sqltable = create_datasource_table(name, db_id, owners)
|
sqltable = create_datasource_table(name, db_id, owners=owners)
|
||||||
insert_model(sqltable)
|
insert_model(sqltable)
|
||||||
inserted_sqltables_ids.append(sqltable.id)
|
inserted_sqltables_ids.append(sqltable.id)
|
||||||
return sqltable
|
return sqltable
|
||||||
@ -145,11 +160,14 @@ def create_datasource_table_to_db(
|
|||||||
def create_datasource_table(
|
def create_datasource_table(
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
db_id: Optional[int] = None,
|
db_id: Optional[int] = None,
|
||||||
|
database: Optional[Database] = None,
|
||||||
owners: Optional[List[User]] = None,
|
owners: Optional[List[User]] = None,
|
||||||
) -> SqlaTable:
|
) -> SqlaTable:
|
||||||
name = name or random_str()
|
name = name if name is not None else random_str()
|
||||||
owners = owners or []
|
owners = owners if owners is not None else []
|
||||||
db_id = db_id or create_database_to_db(name=name + "_db").id
|
if database:
|
||||||
|
return SqlaTable(table_name=name, database=database, owners=owners)
|
||||||
|
db_id = db_id if db_id is not None else create_database_to_db(name=name + "_db").id
|
||||||
return SqlaTable(table_name=name, database_id=db_id, owners=owners)
|
return SqlaTable(table_name=name, database_id=db_id, owners=owners)
|
||||||
|
|
||||||
|
|
||||||
@ -161,7 +179,7 @@ def create_database_to_db(name: Optional[str] = None) -> Database:
|
|||||||
|
|
||||||
|
|
||||||
def create_database(name: Optional[str] = None) -> Database:
|
def create_database(name: Optional[str] = None) -> Database:
|
||||||
name = name or random_str()
|
name = name if name is not None else random_str()
|
||||||
return Database(database_name=name, sqlalchemy_uri="sqlite:///:memory:")
|
return Database(database_name=name, sqlalchemy_uri="sqlite:///:memory:")
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(
|
|||||||
)
|
)
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
SUPERSET_WEBSERVER_PORT = 8081
|
SUPERSET_WEBSERVER_PORT = 8081
|
||||||
|
SILENCE_FAB = False
|
||||||
# Allowing SQLALCHEMY_DATABASE_URI and SQLALCHEMY_EXAMPLES_URI to be defined as an env vars for
|
# Allowing SQLALCHEMY_DATABASE_URI and SQLALCHEMY_EXAMPLES_URI to be defined as an env vars for
|
||||||
# continuous integration
|
# continuous integration
|
||||||
if "SUPERSET__SQLALCHEMY_DATABASE_URI" in os.environ:
|
if "SUPERSET__SQLALCHEMY_DATABASE_URI" in os.environ:
|
||||||
|
Loading…
Reference in New Issue
Block a user