mirror of https://github.com/apache/superset.git
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
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from flask_babel import lazy_gettext as _
|
||||
from marshmallow import ValidationError
|
||||
|
@ -31,6 +31,26 @@ class CommandException(SupersetException):
|
|||
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):
|
||||
""" Common base class for Command Invalid errors. """
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from typing import Iterator, List, Tuple
|
||||
from typing import Iterator, List, Tuple, Type
|
||||
|
||||
import yaml
|
||||
from flask_appbuilder import Model
|
||||
|
@ -33,8 +33,8 @@ METADATA_FILE_NAME = "metadata.yaml"
|
|||
|
||||
class ExportModelsCommand(BaseCommand):
|
||||
|
||||
dao = BaseDAO
|
||||
not_found = CommandException
|
||||
dao: Type[BaseDAO] = BaseDAO
|
||||
not_found: Type[CommandException] = CommandException
|
||||
|
||||
def __init__(self, model_ids: List[int]):
|
||||
self.model_ids = model_ids
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from typing import Optional
|
||||
|
||||
from flask_babel import lazy_gettext as _
|
||||
from marshmallow.validate import ValidationError
|
||||
|
||||
from superset.commands.exceptions import (
|
||||
CommandException,
|
||||
CommandInvalidError,
|
||||
CreateFailedError,
|
||||
DeleteFailedError,
|
||||
ForbiddenError,
|
||||
ImportFailedError,
|
||||
ObjectNotFoundError,
|
||||
UpdateFailedError,
|
||||
)
|
||||
|
||||
|
@ -41,8 +43,11 @@ class DashboardInvalidError(CommandInvalidError):
|
|||
message = _("Dashboard parameters are invalid.")
|
||||
|
||||
|
||||
class DashboardNotFoundError(CommandException):
|
||||
message = _("Dashboard not found.")
|
||||
class DashboardNotFoundError(ObjectNotFoundError):
|
||||
def __init__(
|
||||
self, dashboard_id: Optional[str] = None, exception: Optional[Exception] = None
|
||||
) -> None:
|
||||
super().__init__("Dashboard", dashboard_id, exception)
|
||||
|
||||
|
||||
class DashboardCreateFailedError(CreateFailedError):
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
|
@ -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.
|
|
@ -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
|
|
@ -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))
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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"
|
|
@ -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
|
|
@ -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),
|
||||
)
|
||||
)
|
|
@ -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.reports.api import ReportScheduleRestApi
|
||||
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.alerts import (
|
||||
AlertLogModelView,
|
||||
|
@ -208,6 +209,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
|
|||
appbuilder.add_api(SavedQueryRestApi)
|
||||
appbuilder.add_api(ReportScheduleRestApi)
|
||||
appbuilder.add_api(ReportExecutionLogRestApi)
|
||||
appbuilder.add_api(FilterSetRestApi)
|
||||
#
|
||||
# Setup regular views
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
||||
import sqlalchemy as sqla
|
||||
from flask import g
|
||||
from flask_appbuilder import Model
|
||||
from flask_appbuilder.models.decorators import renders
|
||||
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 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.druid.models import DruidColumn, DruidMetric
|
||||
from superset.connectors.sqla.models import SqlMetric, TableColumn
|
||||
from superset.extensions import cache_manager
|
||||
from superset.models.filter_set import FilterSet
|
||||
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
|
||||
from superset.models.slice import Slice
|
||||
from superset.models.tags import DashboardUpdater
|
||||
|
@ -129,6 +132,7 @@ DashboardRoles = Table(
|
|||
)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
||||
"""The dashboard object!"""
|
||||
|
||||
|
@ -144,6 +148,9 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
|||
owners = relationship(security_manager.user_model, secondary=dashboard_user)
|
||||
published = Column(Boolean, default=False)
|
||||
roles = relationship(security_manager.role_model, secondary=DashboardRoles)
|
||||
_filter_sets = relationship(
|
||||
"FilterSet", back_populates="dashboard", cascade="all, delete"
|
||||
)
|
||||
export_fields = [
|
||||
"dashboard_title",
|
||||
"position_json",
|
||||
|
@ -178,6 +185,29 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
|||
.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
|
||||
def charts(self) -> List[BaseDatasource]:
|
||||
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))
|
||||
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:
|
||||
if id_or_slug.isdigit():
|
||||
|
|
|
@ -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 {}
|
|
@ -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.
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 = "",
|
||||
position_json: str = "",
|
||||
) -> Dashboard:
|
||||
dashboard_title = dashboard_title or random_title()
|
||||
slug = slug or random_slug()
|
||||
owners = owners or []
|
||||
slices = slices or []
|
||||
dashboard_title = dashboard_title if dashboard_title is not None else random_title()
|
||||
slug = slug if slug is not None else random_slug()
|
||||
owners = owners if owners is not None else []
|
||||
slices = slices if slices is not None else []
|
||||
return Dashboard(
|
||||
dashboard_title=dashboard_title,
|
||||
slug=slug,
|
||||
|
@ -109,25 +109,40 @@ def create_slice_to_db(
|
|||
datasource_id: Optional[int] = None,
|
||||
owners: Optional[List[User]] = None,
|
||||
) -> Slice:
|
||||
slice_ = create_slice(datasource_id, name, owners)
|
||||
slice_ = create_slice(datasource_id, name=name, owners=owners)
|
||||
insert_model(slice_)
|
||||
inserted_slices_ids.append(slice_.id)
|
||||
return 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:
|
||||
name = name or random_str()
|
||||
owners = owners or []
|
||||
name = name if name is not None else random_str()
|
||||
owners = owners if owners is not None else []
|
||||
datasource_type = "table"
|
||||
if datasource:
|
||||
return Slice(
|
||||
slice_name=name,
|
||||
table=datasource,
|
||||
owners=owners,
|
||||
datasource_type=datasource_type,
|
||||
)
|
||||
|
||||
datasource_id = (
|
||||
datasource_id or create_datasource_table_to_db(name=name + "_table").id
|
||||
datasource_id
|
||||
if datasource_id is not None
|
||||
else create_datasource_table_to_db(name=name + "_table").id
|
||||
)
|
||||
|
||||
return Slice(
|
||||
slice_name=name,
|
||||
datasource_id=datasource_id,
|
||||
owners=owners,
|
||||
datasource_type="table",
|
||||
datasource_type=datasource_type,
|
||||
)
|
||||
|
||||
|
||||
|
@ -136,7 +151,7 @@ def create_datasource_table_to_db(
|
|||
db_id: Optional[int] = None,
|
||||
owners: Optional[List[User]] = None,
|
||||
) -> SqlaTable:
|
||||
sqltable = create_datasource_table(name, db_id, owners)
|
||||
sqltable = create_datasource_table(name, db_id, owners=owners)
|
||||
insert_model(sqltable)
|
||||
inserted_sqltables_ids.append(sqltable.id)
|
||||
return sqltable
|
||||
|
@ -145,11 +160,14 @@ def create_datasource_table_to_db(
|
|||
def create_datasource_table(
|
||||
name: Optional[str] = None,
|
||||
db_id: Optional[int] = None,
|
||||
database: Optional[Database] = None,
|
||||
owners: Optional[List[User]] = None,
|
||||
) -> SqlaTable:
|
||||
name = name or random_str()
|
||||
owners = owners or []
|
||||
db_id = db_id or create_database_to_db(name=name + "_db").id
|
||||
name = name if name is not None else random_str()
|
||||
owners = owners if owners is not None else []
|
||||
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)
|
||||
|
||||
|
||||
|
@ -161,7 +179,7 @@ def create_database_to_db(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:")
|
||||
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(
|
|||
)
|
||||
DEBUG = False
|
||||
SUPERSET_WEBSERVER_PORT = 8081
|
||||
|
||||
SILENCE_FAB = False
|
||||
# Allowing SQLALCHEMY_DATABASE_URI and SQLALCHEMY_EXAMPLES_URI to be defined as an env vars for
|
||||
# continuous integration
|
||||
if "SUPERSET__SQLALCHEMY_DATABASE_URI" in os.environ:
|
||||
|
|
Loading…
Reference in New Issue