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:
ofekisr 2021-09-23 11:27:59 +03:00 committed by GitHub
parent 997320ac1a
commit 84f7614e97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 3317 additions and 23 deletions

View File

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

View File

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

View File

@ -0,0 +1,39 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Any, Optional
from superset.exceptions import SupersetException
class NotAuthorizedObject:
def __init__(self, what_not_authorized: str):
self._what_not_authorized = what_not_authorized
def __getattr__(self, item: Any) -> None:
raise NotAuthorizedException(self._what_not_authorized)
def __getitem__(self, item: Any) -> None:
raise NotAuthorizedException(self._what_not_authorized)
class NotAuthorizedException(SupersetException):
def __init__(
self, what_not_authorized: str = "", exception: Optional[Exception] = None
) -> None:
super().__init__(
"The user is not authorized to " + what_not_authorized, exception
)

View File

@ -0,0 +1,39 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import List, TYPE_CHECKING
from flask import g
from superset import conf, security_manager
if TYPE_CHECKING:
from flask_appbuilder.security.sqla.models import Role
def get_user_roles() -> List[Role]:
if g.user.is_anonymous:
public_role = conf.get("AUTH_ROLE_PUBLIC")
return [security_manager.get_public_role()] if public_role else []
return g.user.roles
def is_user_admin() -> bool:
user_roles = [role.name.lower() for role in get_user_roles()]
admin_role = conf.get("AUTH_ROLE_ADMIN").lower()
return admin_role in user_roles

View File

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

View File

@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

View File

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

View File

@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

View File

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

View File

@ -0,0 +1,78 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
from typing import Any, Dict
from flask import g
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset import security_manager
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
from superset.dashboards.filter_sets.commands.exceptions import (
DashboardIdInconsistencyError,
FilterSetCreateFailedError,
UserIsNotDashboardOwnerError,
)
from superset.dashboards.filter_sets.consts import (
DASHBOARD_ID_FIELD,
DASHBOARD_OWNER_TYPE,
OWNER_ID_FIELD,
OWNER_TYPE_FIELD,
)
from superset.dashboards.filter_sets.dao import FilterSetDAO
logger = logging.getLogger(__name__)
class CreateFilterSetCommand(BaseFilterSetCommand):
# pylint: disable=C0103
def __init__(self, user: User, dashboard_id: int, data: Dict[str, Any]):
super().__init__(user, dashboard_id)
self._properties = data.copy()
def run(self) -> Model:
self.validate()
self._properties[DASHBOARD_ID_FIELD] = self._dashboard.id
filter_set = FilterSetDAO.create(self._properties, commit=True)
return filter_set
def validate(self) -> None:
self._validate_filterset_dashboard_exists()
if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE:
self._validate_owner_id_is_dashboard_id()
self._validate_user_is_the_dashboard_owner()
else:
self._validate_owner_id_exists()
def _validate_owner_id_exists(self) -> None:
owner_id = self._properties[OWNER_ID_FIELD]
if not (g.user.id == owner_id or security_manager.get_user_by_id(owner_id)):
raise FilterSetCreateFailedError(
str(self._dashboard_id), "owner_id does not exists"
)
def _validate_user_is_the_dashboard_owner(self) -> None:
if not self.is_user_dashboard_owner():
raise UserIsNotDashboardOwnerError(str(self._dashboard_id))
def _validate_owner_id_is_dashboard_id(self) -> None:
if (
self._properties.get(OWNER_ID_FIELD, self._dashboard_id)
!= self._dashboard_id
):
raise DashboardIdInconsistencyError(str(self._dashboard_id))

View File

@ -0,0 +1,56 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset.dao.exceptions import DAODeleteFailedError
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
from superset.dashboards.filter_sets.commands.exceptions import (
FilterSetDeleteFailedError,
FilterSetForbiddenError,
FilterSetNotFoundError,
)
from superset.dashboards.filter_sets.dao import FilterSetDAO
logger = logging.getLogger(__name__)
class DeleteFilterSetCommand(BaseFilterSetCommand):
def __init__(self, user: User, dashboard_id: int, filter_set_id: int):
super().__init__(user, dashboard_id)
self._filter_set_id = filter_set_id
def run(self) -> Model:
try:
self.validate()
return FilterSetDAO.delete(self._filter_set, commit=True)
except DAODeleteFailedError as err:
raise FilterSetDeleteFailedError(str(self._filter_set_id), "") from err
def validate(self) -> None:
self._validate_filterset_dashboard_exists()
try:
self.validate_exist_filter_use_cases_set()
except FilterSetNotFoundError as err:
if FilterSetDAO.find_by_id(self._filter_set_id): # type: ignore
raise FilterSetForbiddenError(
'the filter-set does not related to dashboard "%s"'
% str(self._dashboard_id)
) from err
raise err

View File

@ -0,0 +1,94 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Optional
from flask_babel import lazy_gettext as _
from superset.commands.exceptions import (
CreateFailedError,
DeleteFailedError,
ForbiddenError,
ObjectNotFoundError,
UpdateFailedError,
)
class FilterSetNotFoundError(ObjectNotFoundError):
def __init__(
self, filterset_id: Optional[str] = None, exception: Optional[Exception] = None
) -> None:
super().__init__("FilterSet", filterset_id, exception)
class FilterSetCreateFailedError(CreateFailedError):
base_message = 'CreateFilterSetCommand of dashboard "%s" failed: '
def __init__(
self, dashboard_id: str, reason: str = "", exception: Optional[Exception] = None
) -> None:
super().__init__((self.base_message % dashboard_id) + reason, exception)
class FilterSetUpdateFailedError(UpdateFailedError):
base_message = 'UpdateFilterSetCommand of filter_set "%s" failed: '
def __init__(
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
) -> None:
super().__init__((self.base_message % filterset_id) + reason, exception)
class FilterSetDeleteFailedError(DeleteFailedError):
base_message = 'DeleteFilterSetCommand of filter_set "%s" failed: '
def __init__(
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
) -> None:
super().__init__((self.base_message % filterset_id) + reason, exception)
class UserIsNotDashboardOwnerError(FilterSetCreateFailedError):
reason = (
"cannot create dashboard owner filterset based when"
" the user is not the dashboard owner"
)
def __init__(
self, dashboard_id: str, exception: Optional[Exception] = None
) -> None:
super().__init__(dashboard_id, self.reason, exception)
class DashboardIdInconsistencyError(FilterSetCreateFailedError):
reason = (
"cannot create dashboard owner filterset based when the"
" ownerid is not the dashboard id"
)
def __init__(
self, dashboard_id: str, exception: Optional[Exception] = None
) -> None:
super().__init__(dashboard_id, self.reason, exception)
class FilterSetForbiddenError(ForbiddenError):
message_format = 'Changing FilterSet "{}" is forbidden: {}'
def __init__(
self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None
) -> None:
super().__init__(_(self.message_format.format(filterset_id, reason)), exception)

View File

@ -0,0 +1,56 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
from typing import Any, Dict
from flask_appbuilder.models.sqla import Model
from flask_appbuilder.security.sqla.models import User
from superset.dao.exceptions import DAOUpdateFailedError
from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand
from superset.dashboards.filter_sets.commands.exceptions import (
FilterSetUpdateFailedError,
)
from superset.dashboards.filter_sets.consts import OWNER_ID_FIELD, OWNER_TYPE_FIELD
from superset.dashboards.filter_sets.dao import FilterSetDAO
logger = logging.getLogger(__name__)
class UpdateFilterSetCommand(BaseFilterSetCommand):
def __init__(
self, user: User, dashboard_id: int, filter_set_id: int, data: Dict[str, Any]
):
super().__init__(user, dashboard_id)
self._filter_set_id = filter_set_id
self._properties = data.copy()
def run(self) -> Model:
try:
self.validate()
if (
OWNER_TYPE_FIELD in self._properties
and self._properties[OWNER_TYPE_FIELD] == "Dashboard"
):
self._properties[OWNER_ID_FIELD] = self._dashboard_id
return FilterSetDAO.update(self._filter_set, self._properties, commit=True)
except DAOUpdateFailedError as err:
raise FilterSetUpdateFailedError(str(self._filter_set_id), "") from err
def validate(self) -> None:
self._validate_filterset_dashboard_exists()
self.validate_exist_filter_use_cases_set()

View File

@ -0,0 +1,30 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
USER_OWNER_TYPE = "User"
DASHBOARD_OWNER_TYPE = "Dashboard"
NAME_FIELD = "name"
DESCRIPTION_FIELD = "description"
JSON_METADATA_FIELD = "json_metadata"
OWNER_ID_FIELD = "owner_id"
OWNER_TYPE_FIELD = "owner_type"
DASHBOARD_ID_FIELD = "dashboard_id"
OWNER_OBJECT_FIELD = "owner_object"
DASHBOARD_FIELD = "dashboard"
PARAMS_PROPERTY = "params"
FILTER_SET_API_PERMISSIONS_NAME = "FilterSets"

View File

@ -0,0 +1,64 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
from typing import Any, Dict
from flask_appbuilder.models.sqla import Model
from sqlalchemy.exc import SQLAlchemyError
from superset.dao.base import BaseDAO
from superset.dao.exceptions import DAOConfigError, DAOCreateFailedError
from superset.dashboards.filter_sets.consts import (
DASHBOARD_ID_FIELD,
DESCRIPTION_FIELD,
JSON_METADATA_FIELD,
NAME_FIELD,
OWNER_ID_FIELD,
OWNER_TYPE_FIELD,
)
from superset.extensions import db
from superset.models.filter_set import FilterSet
logger = logging.getLogger(__name__)
class FilterSetDAO(BaseDAO):
model_cls = FilterSet
@classmethod
def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model:
if cls.model_cls is None:
raise DAOConfigError()
model = FilterSet()
setattr(model, NAME_FIELD, properties[NAME_FIELD])
setattr(model, JSON_METADATA_FIELD, properties[JSON_METADATA_FIELD])
setattr(model, DESCRIPTION_FIELD, properties.get(DESCRIPTION_FIELD, None))
setattr(
model,
OWNER_ID_FIELD,
properties.get(OWNER_ID_FIELD, properties[DASHBOARD_ID_FIELD]),
)
setattr(model, OWNER_TYPE_FIELD, properties[OWNER_TYPE_FIELD])
setattr(model, DASHBOARD_ID_FIELD, properties[DASHBOARD_ID_FIELD])
try:
db.session.add(model)
if commit:
db.session.commit()
except SQLAlchemyError as ex: # pragma: no cover
db.session.rollback()
raise DAOCreateFailedError() from ex
return model

View File

@ -0,0 +1,58 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import Any, TYPE_CHECKING
from flask import g
from sqlalchemy import and_, or_
from superset.dashboards.filter_sets.consts import DASHBOARD_OWNER_TYPE, USER_OWNER_TYPE
from superset.models.dashboard import dashboard_user
from superset.models.filter_set import FilterSet
from superset.views.base import BaseFilter, is_user_admin
if TYPE_CHECKING:
from sqlalchemy.orm.query import Query
class FilterSetFilter(BaseFilter): # pylint: disable=too-few-public-methods)
def apply(self, query: Query, value: Any) -> Query:
if is_user_admin():
return query
current_user_id = g.user.id
filter_set_ids_by_dashboard_owners = ( # pylint: disable=C0103
query.from_self(FilterSet.id)
.join(dashboard_user, FilterSet.owner_id == dashboard_user.c.dashboard_id)
.filter(
and_(
FilterSet.owner_type == DASHBOARD_OWNER_TYPE,
dashboard_user.c.user_id == current_user_id,
)
)
)
return query.filter(
or_(
and_(
FilterSet.owner_type == USER_OWNER_TYPE,
FilterSet.owner_id == current_user_id,
),
FilterSet.id.in_(filter_set_ids_by_dashboard_owners),
)
)

View File

@ -0,0 +1,93 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Any, cast, Dict, Mapping
from marshmallow import fields, post_load, Schema, ValidationError
from marshmallow.validate import Length, OneOf
from superset.dashboards.filter_sets.consts import (
DASHBOARD_OWNER_TYPE,
JSON_METADATA_FIELD,
OWNER_ID_FIELD,
OWNER_TYPE_FIELD,
USER_OWNER_TYPE,
)
class JsonMetadataSchema(Schema):
nativeFilters = fields.Mapping(required=True, allow_none=False)
dataMask = fields.Mapping(required=False, allow_none=False)
class FilterSetSchema(Schema):
json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
def _validate_json_meta_data(self, json_meta_data: str) -> None:
try:
self.json_metadata_schema.loads(json_meta_data)
except Exception as ex:
raise ValidationError("failed to parse json_metadata to json") from ex
class FilterSetPostSchema(FilterSetSchema):
json_metadata_schema: JsonMetadataSchema = JsonMetadataSchema()
# pylint: disable=W0613
name = fields.String(required=True, allow_none=False, validate=Length(0, 500),)
description = fields.String(
required=False, allow_none=True, validate=[Length(1, 1000)]
)
json_metadata = fields.String(allow_none=False, required=True)
owner_type = fields.String(
required=True, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])
)
owner_id = fields.Int(required=False)
@post_load
def validate(
self, data: Mapping[Any, Any], *, many: Any, partial: Any
) -> Dict[str, Any]:
self._validate_json_meta_data(data[JSON_METADATA_FIELD])
if data[OWNER_TYPE_FIELD] == USER_OWNER_TYPE and OWNER_ID_FIELD not in data:
raise ValidationError("owner_id is mandatory when owner_type is User")
return cast(Dict[str, Any], data)
class FilterSetPutSchema(FilterSetSchema):
name = fields.String(required=False, allow_none=False, validate=Length(0, 500))
description = fields.String(
required=False, allow_none=False, validate=[Length(1, 1000)]
)
json_metadata = fields.String(required=False, allow_none=False)
owner_type = fields.String(
allow_none=False, required=False, validate=OneOf([DASHBOARD_OWNER_TYPE])
)
@post_load
def validate( # pylint: disable=unused-argument
self, data: Mapping[Any, Any], *, many: Any, partial: Any
) -> Dict[str, Any]:
if JSON_METADATA_FIELD in data:
self._validate_json_meta_data(data[JSON_METADATA_FIELD])
return cast(Dict[str, Any], data)
def validate_pair(first_field: str, second_field: str, data: Dict[str, Any]) -> None:
if first_field in data and second_field not in data:
raise ValidationError(
"{} must be included alongside {}".format(first_field, second_field)
)

View File

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

View File

@ -0,0 +1,56 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""add filter set model
Revision ID: 3ebe0993c770
Revises: 07071313dd52
Create Date: 2021-03-29 11:15:48.831225
"""
# revision identifiers, used by Alembic.
revision = "3ebe0993c770"
down_revision = "181091c0ef16"
import sqlalchemy as sa
from alembic import op
def upgrade():
op.create_table(
"filter_sets",
sa.Column("created_on", sa.DateTime(), nullable=True),
sa.Column("changed_on", sa.DateTime(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.VARCHAR(500), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("json_metadata", sa.Text(), nullable=False),
sa.Column("owner_id", sa.Integer(), nullable=False),
sa.Column("owner_type", sa.VARCHAR(255), nullable=False),
sa.Column(
"dashboard_id", sa.Integer(), sa.ForeignKey("dashboards.id"), nullable=False
),
sa.Column("created_by_fk", sa.Integer(), nullable=True),
sa.Column("changed_by_fk", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["changed_by_fk"], ["ab_user.id"]),
sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]),
sa.PrimaryKeyConstraint("id"),
)
def downgrade():
op.drop_table("filter_sets")

View File

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

View File

@ -0,0 +1,106 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import json
import logging
from typing import Any, Dict
from flask_appbuilder import Model
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text
from sqlalchemy.orm import relationship
from sqlalchemy_utils import generic_relationship
from superset import app, db
from superset.models.helpers import AuditMixinNullable
metadata = Model.metadata # pylint: disable=no-member
config = app.config
logger = logging.getLogger(__name__)
class FilterSet(Model, AuditMixinNullable):
__tablename__ = "filter_sets"
id = Column(Integer, primary_key=True)
name = Column(String(500), nullable=False, unique=True)
description = Column(Text, nullable=True)
json_metadata = Column(Text, nullable=False)
dashboard_id = Column(Integer, ForeignKey("dashboards.id"))
dashboard = relationship("Dashboard", back_populates="_filter_sets")
owner_id = Column(Integer, nullable=False)
owner_type = Column(String(255), nullable=False)
owner_object = generic_relationship(owner_type, owner_id)
def __repr__(self) -> str:
return f"FilterSet<{self.name or self.id}>"
@property
def url(self) -> str:
return f"/api/filtersets/{self.id}/"
@property
def sqla_metadata(self) -> None:
# pylint: disable=no-member
meta = MetaData(bind=self.get_sqla_engine())
meta.reflect()
@property
def changed_by_name(self) -> str:
if not self.changed_by:
return ""
return str(self.changed_by)
@property
def changed_by_url(self) -> str:
if not self.changed_by:
return ""
return f"/superset/profile/{self.changed_by.username}"
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"description": self.description,
"params": self.params,
"dashboard_id": self.dashboard_id,
"owner_type": self.owner_type,
"owner_id": self.owner_id,
}
@classmethod
def get(cls, _id: int) -> FilterSet:
session = db.session()
qry = session.query(FilterSet).filter(_id)
return qry.one_or_none()
@classmethod
def get_by_name(cls, name: str) -> FilterSet:
session = db.session()
qry = session.query(FilterSet).filter(FilterSet.name == name)
return qry.one_or_none()
@classmethod
def get_by_dashboard_id(cls, dashboard_id: int) -> FilterSet:
session = db.session()
qry = session.query(FilterSet).filter(FilterSet.dashboard_id == dashboard_id)
return qry.all()
@property
def params(self) -> Dict[str, Any]:
if self.json_metadata:
return json.loads(self.json_metadata)
return {}

View File

@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

View File

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

View File

@ -0,0 +1,22 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
FILTER_SET_URI = "api/v1/dashboard/{dashboard_id}/filtersets"
ADMIN_USERNAME_FOR_TEST = "admin@filterset.com"
DASHBOARD_OWNER_USERNAME = "dash_owner_user@filterset.com"
FILTER_SET_OWNER_USERNAME = "fs_owner_user@filterset.com"
REGULAR_USER = "regular_user@filterset.com"

View File

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

View File

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

View File

@ -0,0 +1,129 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import Any, Dict, List, Set, TYPE_CHECKING
from tests.integration_tests.base_tests import login
from tests.integration_tests.dashboards.filter_sets.consts import (
DASHBOARD_OWNER_USERNAME,
FILTER_SET_OWNER_USERNAME,
REGULAR_USER,
)
from tests.integration_tests.dashboards.filter_sets.utils import (
call_get_filter_sets,
collect_all_ids,
)
if TYPE_CHECKING:
from flask.testing import FlaskClient
from superset.models.filter_set import FilterSet
class TestGetFilterSetsApi:
def test_with_dashboard_not_exists__404(
self, not_exists_dashboard: int, client: FlaskClient[Any],
):
# arrange
login(client, "admin")
# act
response = call_get_filter_sets(client, not_exists_dashboard)
# assert
assert response.status_code == 404
def test_dashboards_without_filtersets__200(
self, dashboard_id: int, client: FlaskClient[Any]
):
# arrange
login(client, "admin")
# act
response = call_get_filter_sets(client, dashboard_id)
# assert
assert response.status_code == 200
assert response.is_json and response.json["count"] == 0
def test_when_caller_admin__200(
self,
dashboard_id: int,
filtersets: Dict[str, List[FilterSet]],
client: FlaskClient[Any],
):
# arrange
login(client, "admin")
expected_ids: Set[int] = collect_all_ids(filtersets)
# act
response = call_get_filter_sets(client, dashboard_id)
# assert
assert response.status_code == 200
assert response.is_json and set(response.json["ids"]) == expected_ids
def test_when_caller_dashboard_owner__200(
self,
dashboard_id: int,
filtersets: Dict[str, List[FilterSet]],
client: FlaskClient[Any],
):
# arrange
login(client, DASHBOARD_OWNER_USERNAME)
expected_ids = collect_all_ids(filtersets["Dashboard"])
# act
response = call_get_filter_sets(client, dashboard_id)
# assert
assert response.status_code == 200
assert response.is_json and set(response.json["ids"]) == expected_ids
def test_when_caller_filterset_owner__200(
self,
dashboard_id: int,
filtersets: Dict[str, List[FilterSet]],
client: FlaskClient[Any],
):
# arrange
login(client, FILTER_SET_OWNER_USERNAME)
expected_ids = collect_all_ids(filtersets[FILTER_SET_OWNER_USERNAME])
# act
response = call_get_filter_sets(client, dashboard_id)
# assert
assert response.status_code == 200
assert response.is_json and set(response.json["ids"]) == expected_ids
def test_when_caller_regular_user__200(
self,
dashboard_id: int,
filtersets: Dict[str, List[int]],
client: FlaskClient[Any],
):
# arrange
login(client, REGULAR_USER)
expected_ids: Set[int] = set()
# act
response = call_get_filter_sets(client, dashboard_id)
# assert
assert response.status_code == 200
assert response.is_json and set(response.json["ids"]) == expected_ids

View File

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

View File

@ -0,0 +1,102 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union
from superset.models.filter_set import FilterSet
from tests.integration_tests.dashboards.filter_sets.consts import FILTER_SET_URI
from tests.integration_tests.test_app import app
if TYPE_CHECKING:
from flask import Response
from flask.testing import FlaskClient
def call_create_filter_set(
client: FlaskClient[Any], dashboard_id: int, data: Dict[str, Any]
) -> Response:
uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
return client.post(uri, json=data)
def call_get_filter_sets(client: FlaskClient[Any], dashboard_id: int) -> Response:
uri = FILTER_SET_URI.format(dashboard_id=dashboard_id)
return client.get(uri)
def call_delete_filter_set(
client: FlaskClient[Any],
filter_set_dict_to_update: Dict[str, Any],
dashboard_id: Optional[int] = None,
) -> Response:
dashboard_id = (
dashboard_id
if dashboard_id is not None
else filter_set_dict_to_update["dashboard_id"]
)
uri = "{}/{}".format(
FILTER_SET_URI.format(dashboard_id=dashboard_id),
filter_set_dict_to_update["id"],
)
return client.delete(uri)
def call_update_filter_set(
client: FlaskClient[Any],
filter_set_dict_to_update: Dict[str, Any],
data: Dict[str, Any],
dashboard_id: Optional[int] = None,
) -> Response:
dashboard_id = (
dashboard_id
if dashboard_id is not None
else filter_set_dict_to_update["dashboard_id"]
)
uri = "{}/{}".format(
FILTER_SET_URI.format(dashboard_id=dashboard_id),
filter_set_dict_to_update["id"],
)
return client.put(uri, json=data)
def get_filter_set_by_name(name: str) -> FilterSet:
with app.app_context():
return FilterSet.get_by_name(name)
def get_filter_set_by_id(id_: int) -> FilterSet:
with app.app_context():
return FilterSet.get(id_)
def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet:
with app.app_context():
return FilterSet.get_by_dashboard_id(dashboard_id)
def collect_all_ids(
filtersets: Union[Dict[str, List[FilterSet]], List[FilterSet]]
) -> Set[int]:
if isinstance(filtersets, dict):
filtersets_lists: List[List[FilterSet]] = list(filtersets.values())
ids: Set[int] = set()
lst: List[FilterSet]
for lst in filtersets_lists:
ids.update(set(map(lambda fs: fs.id, lst)))
return ids
return set(map(lambda fs: fs.id, filtersets))

View File

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

View File

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