From b7865ada741b8af5a0789c4340db3cc67ed775f5 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Thu, 8 Apr 2021 11:09:11 +0300 Subject: [PATCH 01/60] Add filterset resource --- superset/app.py | 2 + superset/commands/exceptions.py | 13 +- superset/common/request_contexed_based.py | 37 +++++ superset/dashboards/commands/exceptions.py | 9 +- superset/dashboards/filter_sets/__init__.py | 16 ++ superset/dashboards/filter_sets/api.py | 143 ++++++++++++++++++ .../filter_sets/commands/__init__.py | 16 ++ .../dashboards/filter_sets/commands/base.py | 63 ++++++++ .../dashboards/filter_sets/commands/create.py | 47 ++++++ .../dashboards/filter_sets/commands/delete.py | 51 +++++++ .../filter_sets/commands/exceptions.py | 66 ++++++++ .../dashboards/filter_sets/commands/update.py | 48 ++++++ superset/dashboards/filter_sets/consts.py | 29 ++++ superset/dashboards/filter_sets/dao.py | 52 +++++++ superset/dashboards/filter_sets/filters.py | 52 +++++++ superset/dashboards/filter_sets/schemas.py | 63 ++++++++ .../versions/3ebe0993c770_filterset_table.py | 54 +++++++ superset/models/dashboard.py | 21 +++ superset/models/filter_set.py | 108 +++++++++++++ 19 files changed, 885 insertions(+), 5 deletions(-) create mode 100644 superset/common/request_contexed_based.py create mode 100644 superset/dashboards/filter_sets/__init__.py create mode 100644 superset/dashboards/filter_sets/api.py create mode 100644 superset/dashboards/filter_sets/commands/__init__.py create mode 100644 superset/dashboards/filter_sets/commands/base.py create mode 100644 superset/dashboards/filter_sets/commands/create.py create mode 100644 superset/dashboards/filter_sets/commands/delete.py create mode 100644 superset/dashboards/filter_sets/commands/exceptions.py create mode 100644 superset/dashboards/filter_sets/commands/update.py create mode 100644 superset/dashboards/filter_sets/consts.py create mode 100644 superset/dashboards/filter_sets/dao.py create mode 100644 superset/dashboards/filter_sets/filters.py create mode 100644 superset/dashboards/filter_sets/schemas.py create mode 100644 superset/migrations/versions/3ebe0993c770_filterset_table.py create mode 100644 superset/models/filter_set.py diff --git a/superset/app.py b/superset/app.py index f0f5dc1510689..cd6e69e7b2a2f 100644 --- a/superset/app.py +++ b/superset/app.py @@ -523,6 +523,8 @@ def init_views(self) -> None: icon="fa-cog", ) appbuilder.add_separator("Data") + from superset.dashboards.filter_sets.api import FilterSetRestApi + appbuilder.add_api(FilterSetRestApi) def init_app_in_ctx(self) -> None: """ diff --git a/superset/commands/exceptions.py b/superset/commands/exceptions.py index bb8992aeb0e26..394422d54dab8 100644 --- a/superset/commands/exceptions.py +++ b/superset/commands/exceptions.py @@ -14,11 +14,9 @@ # 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 - from superset.exceptions import SupersetException @@ -31,6 +29,15 @@ def __repr__(self) -> str: return repr(self) +class ObjectNotFoundError(CommandException): + status = 404 + message_format = '{} {}not found.' + + def __init__(self, object_type: str, object_id: 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. """ diff --git a/superset/common/request_contexed_based.py b/superset/common/request_contexed_based.py new file mode 100644 index 0000000000000..e099da17c97c7 --- /dev/null +++ b/superset/common/request_contexed_based.py @@ -0,0 +1,37 @@ +# 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.find_role(public_role)] if public_role else [] + return g.user.roles + + +def is_user_admin() -> bool: + user_roles = [role.name.lower() for role in list(get_user_roles())] + return "admin" in user_roles diff --git a/superset/dashboards/commands/exceptions.py b/superset/dashboards/commands/exceptions.py index ee85c1f391808..cb9bc3c419dd9 100644 --- a/superset/dashboards/commands/exceptions.py +++ b/superset/dashboards/commands/exceptions.py @@ -14,6 +14,8 @@ # 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 @@ -25,6 +27,7 @@ ForbiddenError, ImportFailedError, UpdateFailedError, + ObjectNotFoundError ) @@ -41,8 +44,10 @@ class DashboardInvalidError(CommandInvalidError): message = _("Dashboard parameters are invalid.") -class DashboardNotFoundError(CommandException): - message = _("Dashboard not found.") +class DashboardNotFoundError(ObjectNotFoundError): + def __init__(self, dashboard_id: str = None, exception: Optional[Exception] = None) -> None: + super().__init__('Dashboard', dashboard_id, exception) + class DashboardCreateFailedError(CreateFailedError): diff --git a/superset/dashboards/filter_sets/__init__.py b/superset/dashboards/filter_sets/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/superset/dashboards/filter_sets/__init__.py @@ -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. diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py new file mode 100644 index 0000000000000..0180307080835 --- /dev/null +++ b/superset/dashboards/filter_sets/api.py @@ -0,0 +1,143 @@ +# 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 import Response, request, g +from flask_appbuilder.api import expose, protect, safe, rison, permission_name, merge_response_func, \ + get_list_schema, API_ORDER_COLUMNS_RIS_KEY, API_LABEL_COLUMNS_RIS_KEY, \ + API_DESCRIPTION_COLUMNS_RIS_KEY, API_LIST_COLUMNS_RIS_KEY, API_LIST_TITLE_RIS_KEY, ModelRestApi +from flask_appbuilder.models.sqla.interface import SQLAInterface +from marshmallow import ValidationError +from superset import is_feature_enabled +from superset.commands.exceptions import ObjectNotFoundError +from superset.dashboards.commands.exceptions import DashboardNotFoundError +from superset.dashboards.filter_sets.commands.exceptions import FilterSetForbiddenError, FilterSetUpdateFailedError, FilterSetDeleteFailedError, FilterSetCreateFailedError +from superset.dashboards.filter_sets.commands.create import CreateFilterSetCommand +from superset.dashboards.filter_sets.commands.update import UpdateFilterSetCommand +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 +from superset.dashboards.dao import DashboardDAO +from superset.dashboards.filter_sets.consts import OWNER_OBJECT_FIELD, DASHBOARD_FIELD, \ + FILTER_SET_API_PERMISSIONS_NAME, NAME_FIELD, DESCRIPTION_FIELD, OWNER_TYPE_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD, JSON_METADATA_FIELD + +logger = logging.getLogger(__name__) + + +class FilterSetRestApi(BaseSupersetModelRestApi): + datamodel = SQLAInterface(FilterSet) + resource_name = "dashboard" + class_permission_name = FILTER_SET_API_PERMISSIONS_NAME + allow_browser_login = True + csrf_exempt = True + add_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_FIELD] + add_model_schema = FilterSetPostSchema() + edit_model_schema = FilterSetPutSchema() + edit_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_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, JSON_METADATA_FIELD] + show_exclude_columns = [OWNER_OBJECT_FIELD, DASHBOARD_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: [] + if is_feature_enabled("THUMBNAILS"): + self.include_route_methods = self.include_route_methods | {"thumbnail"} + super().__init__() + + def _init_properties(self) -> None: + super(BaseSupersetModelRestApi, self)._init_properties() + + @expose("//filtersets", methods=["GET"]) + @protect() + @safe + @permission_name("get") + @rison(get_list_schema) + @merge_response_func(ModelRestApi.merge_order_columns, API_ORDER_COLUMNS_RIS_KEY) + @merge_response_func(ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY) + @merge_response_func(ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY) + @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) + @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) + def get_list(self, dashboard_id: int, **kwargs) -> Response: + if not DashboardDAO.find_by_id(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("//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: + 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, FilterSetCreateFailedError) as error: + return self.response_400(message=error.message) + except DashboardNotFoundError: + return self.response_404() + + @expose("//filtersets/", 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: + 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.message) + except (ObjectNotFoundError, FilterSetForbiddenError, FilterSetUpdateFailedError) as e: + logger.error(e) + return self.response(e.status) + + + @expose("//filtersets/", 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: + try: + changed_model = UpdateFilterSetCommand(g.user, dashboard_id, pk).run() + return self.response(200, id=changed_model.id) + except ValidationError as error: + return self.response_400(message=error.message) + except (ObjectNotFoundError, FilterSetForbiddenError, FilterSetDeleteFailedError) as e: + logger.error(e) + return self.response(e.status) diff --git a/superset/dashboards/filter_sets/commands/__init__.py b/superset/dashboards/filter_sets/commands/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/superset/dashboards/filter_sets/commands/__init__.py @@ -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. diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py new file mode 100644 index 0000000000000..9765fa394b2b4 --- /dev/null +++ b/superset/dashboards/filter_sets/commands/base.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import logging +from typing import Optional +from flask_appbuilder.models.sqla import Model +from flask_appbuilder.security.sqla.models import User +from superset.commands.base import BaseCommand +from superset.dashboards.commands.exceptions import DashboardNotFoundError +from superset.dashboards.dao import DashboardDAO +from superset.dashboards.filter_sets.commands.exceptions import FilterSetNotFoundError, FilterSetForbiddenError +from superset.models.dashboard import Dashboard +from superset.models.filter_set import FilterSet +from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE +logger = logging.getLogger(__name__) + + +class BaseFilterSetCommand(BaseCommand): + _dashboard: Dashboard + _filter_set_id: Optional[int] + _filter_set: Optional[FilterSet] + + def __init__(self, user: User, dashboard_id: int): + self._actor = user + self._dashboard_id = dashboard_id + + def run(self) -> Model: + pass + + def validate(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._dashboard.am_i_owner() + + def validate_exist_filter_use_cases_set(self): + if self._filter_set_id: + self._filter_set = self._dashboard.filter_sets.get(self._filter_set_id, None) + if not self._filter_set: + raise FilterSetNotFoundError(str(self._filter_set_id)) + self.check_ownership() + + def check_ownership(self): + if self._filter_set.owner_type == USER_OWNER_TYPE: + if self._actor.id != self._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") diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py new file mode 100644 index 0000000000000..a99242c6b4541 --- /dev/null +++ b/superset/dashboards/filter_sets/commands/create.py @@ -0,0 +1,47 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from typing import Dict, Any +import logging +from flask_appbuilder.models.sqla import Model +from flask_appbuilder.security.sqla.models import User +from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand +from superset.dashboards.filter_sets.commands.exceptions import UserIsNotDashboardOwnerError +from superset.dashboards.filter_sets.dao import FilterSetDAO +from superset.dashboards.filter_sets.consts import DASHBOARD_ID_FIELD, DASHBOARD_OWNER_TYPE, OWNER_TYPE_FIELD, OWNER_ID_FIELD + +logger = logging.getLogger(__name__) + + +class CreateFilterSetCommand(BaseFilterSetCommand): + 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): + super().validate() + if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE: + if self._properties.get(OWNER_ID_FIELD, self._dashboard_id) != self._dashboard_id: + raise + if not self.is_user_dashboard_owner(): + raise UserIsNotDashboardOwnerError(str(self._dashboard_id)) + diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py new file mode 100644 index 0000000000000..095aaa39d68b6 --- /dev/null +++ b/superset/dashboards/filter_sets/commands/delete.py @@ -0,0 +1,51 @@ +# 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 FilterSetNotFoundError, FilterSetForbiddenError, FilterSetDeleteFailedError +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 e: + raise FilterSetDeleteFailedError(str(self._filter_set_id), '', e) + + def validate(self) -> None: + super().validate() + try: + self.validate_exist_filter_use_cases_set() + except FilterSetNotFoundError as e: + if FilterSetDAO.find_by_id(self._filter_set_id): + FilterSetForbiddenError('the filter-set does not related to dashboard "%s"' % self._dashboard_id) + else: + raise e + + + diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/dashboards/filter_sets/commands/exceptions.py new file mode 100644 index 0000000000000..47b0c64c336da --- /dev/null +++ b/superset/dashboards/filter_sets/commands/exceptions.py @@ -0,0 +1,66 @@ +# 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 ForbiddenError, CreateFailedError, ObjectNotFoundError, UpdateFailedError, DeleteFailedError + + +class FilterSetNotFoundError(ObjectNotFoundError): + def __init__(self, filterset_id: str = None, exception: Optional[Exception] = None) -> None: + super().__init__('FilterSet', filterset_id, exception) + + +class FilterSetCreateFailedError(CreateFailedError): + base_massage = 'CreateFilterSetCommand of dashboard "%s" failed: ' + def __init__(self, dashboard_id: str, reason: str = "", + exception: Optional[Exception] = None) -> None: + super().__init__((self.base_massage % dashboard_id) + reason, exception) + + +class FilterSetUpdateFailedError(UpdateFailedError): + base_massage = 'UpdateFilterSetCommand of filter_set "%s" failed: ' + def __init__(self, filterset_id: str, reason: str = "", + exception: Optional[Exception] = None) -> None: + super().__init__((self.base_massage % filterset_id) + reason, exception) + + +class FilterSetDeleteFailedError(DeleteFailedError): + base_massage = 'DeleteFilterSetCommand of filter_set "%s" failed: ' + def __init__(self, filterset_id: str, reason: str = "", + exception: Optional[Exception] = None) -> None: + super().__init__((self.base_massage % 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 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 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) diff --git a/superset/dashboards/filter_sets/commands/update.py b/superset/dashboards/filter_sets/commands/update.py new file mode 100644 index 0000000000000..80dd5481037c2 --- /dev/null +++ b/superset/dashboards/filter_sets/commands/update.py @@ -0,0 +1,48 @@ +# 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 Dict, Any +import logging +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.dao import FilterSetDAO +from superset.dashboards.filter_sets.consts import DASHBOARD_ID_FIELD +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() + self._properties[DASHBOARD_ID_FIELD] = self._dashboard_id + return FilterSetDAO.update(self._filter_set, self._properties, commit=True) + except DAOUpdateFailedError as e: + raise FilterSetUpdateFailedError(str(self._filter_set_id), '', e) + + def validate(self) -> None: + super().validate() + self.validate_exist_filter_use_cases_set() + + + diff --git a/superset/dashboards/filter_sets/consts.py b/superset/dashboards/filter_sets/consts.py new file mode 100644 index 0000000000000..876d379c899cb --- /dev/null +++ b/superset/dashboards/filter_sets/consts.py @@ -0,0 +1,29 @@ +# 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' + +FILTER_SET_API_PERMISSIONS_NAME = 'FilterSets' diff --git a/superset/dashboards/filter_sets/dao.py b/superset/dashboards/filter_sets/dao.py new file mode 100644 index 0000000000000..977571548beef --- /dev/null +++ b/superset/dashboards/filter_sets/dao.py @@ -0,0 +1,52 @@ +# 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, Dict +import logging +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.extensions import db +from superset.models.filter_set import FilterSet +from superset.dashboards.filter_sets.consts import NAME_FIELD, JSON_METADATA_FIELD, \ + DESCRIPTION_FIELD, OWNER_ID_FIELD, OWNER_TYPE_FIELD, DASHBOARD_ID_FIELD + +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() # pylint: disable=not-callable + 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[OWNER_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(exception=ex) + return model diff --git a/superset/dashboards/filter_sets/filters.py b/superset/dashboards/filter_sets/filters.py new file mode 100644 index 0000000000000..c59bd2352e341 --- /dev/null +++ b/superset/dashboards/filter_sets/filters.py @@ -0,0 +1,52 @@ +# 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.models.dashboard import dashboard_user +from superset.models.filter_set import FilterSet +from superset.views.base import BaseFilter, is_user_admin +from superset.dashboards.filter_sets.consts import DASHBOARD_OWNER_TYPE, USER_OWNER_TYPE +if TYPE_CHECKING: + from sqlalchemy.orm.query import Query + + +class FilterSetFilter(BaseFilter): + 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 = 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) + ) + ) diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py new file mode 100644 index 0000000000000..f5c3e60c361cf --- /dev/null +++ b/superset/dashboards/filter_sets/schemas.py @@ -0,0 +1,63 @@ +# 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 marshmallow import fields, Schema, ValidationError, pre_load +from marshmallow.validate import Length +from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE, \ + OWNER_ID_FIELD, OWNER_TYPE_FIELD + + +class JsonMetadataSchema(Schema): + nativeFilters = fields.Mapping(required=True, allow_none=False) + dataMask = fields.Mapping(required=False, allow_none=False) + + +class FilterSetPostSchema(Schema): + name = fields.String( + required=True, + allow_none=False, + validate=Length(0, 500), + ) + description = fields.String( + allow_none=True, + validate=[Length(1, 1000)] + ) + json_metadata = fields.Nested(JsonMetadataSchema, required=True) + + owner_id = fields.Int(required=True) + owner_type = fields.String(required=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE]) + + @pre_load + def validate(self, data, many, **kwargs): + 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 data + + +class FilterSetPutSchema(Schema): + name = fields.String(allow_none=False, validate=Length(0, 500)) + description = fields.String(allow_none=False, validate=[Length(1, 1000)]) + json_metadata = fields.Nested(JsonMetadataSchema, allow_none=False) + owner_type = fields.String(allow_none=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE]) + + +def validate_pair(first_field, second_field, data): + if first_field in data and second_field not in data: + raise ValidationError("{} must be included alongside {}".format(first_field, second_field)) + + +class FilterSetMetadataSchema(Schema): + pass diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py new file mode 100644 index 0000000000000..1edba7e8cbe59 --- /dev/null +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -0,0 +1,54 @@ +# 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. +"""empty message + +Revision ID: 3ebe0993c770 +Revises: 67da9ef1ef9c +Create Date: 2021-03-29 11:15:48.831225 + +""" + +# revision identifiers, used by Alembic. +revision = '3ebe0993c770' +down_revision = '67da9ef1ef9c' + +from alembic import op +import sqlalchemy as sa + + +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") diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index 0f21c524a56ac..e8f96e89ae19f 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -22,6 +22,7 @@ from typing import Any, Callable, Dict, List, Set, 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 @@ -59,6 +60,7 @@ from superset.utils import core as utils from superset.utils.decorators import debounce from superset.utils.urls import get_url_path +from superset.common.request_contexed_based import is_user_admin # pylint: disable=too-many-public-methods @@ -66,6 +68,7 @@ config = app.config logger = logging.getLogger(__name__) +from superset.models.filter_set import FilterSet def copy_dashboard( _mapper: Mapper, connection: Connection, target: "Dashboard" @@ -149,6 +152,7 @@ class Dashboard( # pylint: disable=too-many-instance-attributes 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") export_fields = [ "dashboard_title", "position_json", @@ -161,6 +165,17 @@ class Dashboard( # pylint: disable=too-many-instance-attributes def __repr__(self) -> str: return f"Dashboard<{self.id or self.slug}>" + @property + def filter_sets(self): + if is_user_admin(): + return self._filter_set + current_user = g.user.id + mapa = {"Dashboard": [], "User": []} + for fs in self._filter_sets: + mapa[fs.owner_type].append(fs) + rv = list(filter(lambda filter_set: filter_set.owner_id == current_user, mapa["User"])) + return {fs.id: fs for fs in rv + mapa["Dashboard"]} + @property def table_names(self) -> str: # pylint: disable=no-member @@ -373,6 +388,12 @@ def get(cls, id_or_slug: str) -> Dashboard: qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug)) return qry.one_or_none() + def am_i_owner(self): + if g.user is None or g.user.is_anonymous or not g.user.is_authenticated: + return False + else: + 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(): diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py new file mode 100644 index 0000000000000..2fffbc6df71f8 --- /dev/null +++ b/superset/models/filter_set.py @@ -0,0 +1,108 @@ +# 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 sqlalchemy_utils import generic_relationship +import json +import logging +from typing import Any, Dict +from flask_appbuilder import Model +from sqlalchemy import ( + Column, + Integer, + MetaData, + String, + Text, + ForeignKey, +) +from sqlalchemy.sql.elements import BinaryExpression +from sqlalchemy.orm import relationship +from superset import app, db +from superset.models.helpers import AuditMixinNullable + + +# pylint: disable=too-many-public-methods + +metadata = Model.metadata # pylint: disable=no-member +config = app.config +logger = logging.getLogger(__name__) + + +class FilterSet( # pylint: disable=too-many-instance-attributes + 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 __init__(self) -> None: + super().__init__() + + def __repr__(self) -> str: + return f"FilterSet<{self.name or self.id}>" + + @property + def url(self) -> str: + return f"/api/filtersets/{self.slug or 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}" + + @property + def data(self) -> Dict[str, Any]: + json_metadata = self.json_metadata + if json_metadata: + json_metadata = json.loads(json_metadata) + return { + "id": self.id, + "metadata": json_metadata, + "name": self.name, + "last_modified_time": self.changed_on.replace(microsecond=0).timestamp(), + } + + @classmethod + def get(cls, id_or_slug: str) -> FilterSet: + session = db.session() + qry = session.query(FilterSet).filter(id_or_slug_filter(id_or_slug)) + return qry.one_or_none() + + +def id_or_slug_filter(id_or_slug: str) -> BinaryExpression: + if id_or_slug.isdigit(): + return FilterSet.id == int(id_or_slug) + return FilterSet.slug == id_or_slug From 60ab14f1ba35334d717ce727859452b8b3b32fea Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Thu, 8 Apr 2021 12:11:35 +0300 Subject: [PATCH 02/60] fix: fix pre-commit --- superset/app.py | 1 + superset/commands/exceptions.py | 23 +++- superset/common/request_contexed_based.py | 9 +- superset/dashboards/commands/exceptions.py | 9 +- superset/dashboards/filter_sets/api.py | 100 ++++++++++++++---- .../dashboards/filter_sets/commands/base.py | 24 ++++- .../dashboards/filter_sets/commands/create.py | 21 +++- .../dashboards/filter_sets/commands/delete.py | 18 ++-- .../filter_sets/commands/exceptions.py | 46 +++++--- .../dashboards/filter_sets/commands/update.py | 20 ++-- superset/dashboards/filter_sets/consts.py | 22 ++-- superset/dashboards/filter_sets/dao.py | 14 ++- superset/dashboards/filter_sets/filters.py | 20 ++-- superset/dashboards/filter_sets/schemas.py | 36 ++++--- .../versions/3ebe0993c770_filterset_table.py | 14 +-- superset/models/dashboard.py | 8 +- superset/models/filter_set.py | 17 ++- 17 files changed, 273 insertions(+), 129 deletions(-) diff --git a/superset/app.py b/superset/app.py index cd6e69e7b2a2f..d0c9188484ad3 100644 --- a/superset/app.py +++ b/superset/app.py @@ -524,6 +524,7 @@ def init_views(self) -> None: ) appbuilder.add_separator("Data") from superset.dashboards.filter_sets.api import FilterSetRestApi + appbuilder.add_api(FilterSetRestApi) def init_app_in_ctx(self) -> None: diff --git a/superset/commands/exceptions.py b/superset/commands/exceptions.py index 394422d54dab8..3de57788c524c 100644 --- a/superset/commands/exceptions.py +++ b/superset/commands/exceptions.py @@ -15,8 +15,10 @@ # specific language governing permissions and limitations # under the License. from typing import Any, Dict, List, Optional + from flask_babel import lazy_gettext as _ from marshmallow import ValidationError + from superset.exceptions import SupersetException @@ -31,11 +33,22 @@ def __repr__(self) -> str: class ObjectNotFoundError(CommandException): status = 404 - message_format = '{} {}not found.' - - def __init__(self, object_type: str, object_id: str = None, - exception: Optional[Exception] = None) -> None: - super().__init__(_(self.message_format.format(object_type, '"%s" ' % object_id if object_id else '')), exception) + message_format = "{} {}not found." + + def __init__( + self, + object_type: str, + object_id: 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): diff --git a/superset/common/request_contexed_based.py b/superset/common/request_contexed_based.py index e099da17c97c7..b7344f28af1e6 100644 --- a/superset/common/request_contexed_based.py +++ b/superset/common/request_contexed_based.py @@ -15,12 +15,13 @@ # 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, -) + +from superset import conf, security_manager + if TYPE_CHECKING: from flask_appbuilder.security.sqla.models import Role diff --git a/superset/dashboards/commands/exceptions.py b/superset/dashboards/commands/exceptions.py index cb9bc3c419dd9..adef282abeb8b 100644 --- a/superset/dashboards/commands/exceptions.py +++ b/superset/dashboards/commands/exceptions.py @@ -26,8 +26,8 @@ DeleteFailedError, ForbiddenError, ImportFailedError, + ObjectNotFoundError, UpdateFailedError, - ObjectNotFoundError ) @@ -45,9 +45,10 @@ class DashboardInvalidError(CommandInvalidError): class DashboardNotFoundError(ObjectNotFoundError): - def __init__(self, dashboard_id: str = None, exception: Optional[Exception] = None) -> None: - super().__init__('Dashboard', dashboard_id, exception) - + def __init__( + self, dashboard_id: str = None, exception: Optional[Exception] = None + ) -> None: + super().__init__("Dashboard", dashboard_id, exception) class DashboardCreateFailedError(CreateFailedError): diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 0180307080835..df616e22d9b45 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -15,26 +15,57 @@ # specific language governing permissions and limitations # under the License. import logging -from flask import Response, request, g -from flask_appbuilder.api import expose, protect, safe, rison, permission_name, merge_response_func, \ - get_list_schema, API_ORDER_COLUMNS_RIS_KEY, API_LABEL_COLUMNS_RIS_KEY, \ - API_DESCRIPTION_COLUMNS_RIS_KEY, API_LIST_COLUMNS_RIS_KEY, API_LIST_TITLE_RIS_KEY, ModelRestApi + +from flask import g, request, Response +from flask_appbuilder.api import ( + API_DESCRIPTION_COLUMNS_RIS_KEY, + API_LABEL_COLUMNS_RIS_KEY, + API_LIST_COLUMNS_RIS_KEY, + API_LIST_TITLE_RIS_KEY, + API_ORDER_COLUMNS_RIS_KEY, + expose, + get_list_schema, + merge_response_func, + ModelRestApi, + permission_name, + protect, + rison, + safe, +) from flask_appbuilder.models.sqla.interface import SQLAInterface from marshmallow import ValidationError + from superset import is_feature_enabled from superset.commands.exceptions import ObjectNotFoundError from superset.dashboards.commands.exceptions import DashboardNotFoundError -from superset.dashboards.filter_sets.commands.exceptions import FilterSetForbiddenError, FilterSetUpdateFailedError, FilterSetDeleteFailedError, FilterSetCreateFailedError +from superset.dashboards.dao import DashboardDAO from superset.dashboards.filter_sets.commands.create import CreateFilterSetCommand +from superset.dashboards.filter_sets.commands.exceptions import ( + FilterSetCreateFailedError, + FilterSetDeleteFailedError, + FilterSetForbiddenError, + FilterSetUpdateFailedError, +) 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, +) from superset.dashboards.filter_sets.filters import FilterSetFilter -from superset.dashboards.filter_sets.schemas import FilterSetPostSchema, FilterSetPutSchema +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 -from superset.dashboards.dao import DashboardDAO -from superset.dashboards.filter_sets.consts import OWNER_OBJECT_FIELD, DASHBOARD_FIELD, \ - FILTER_SET_API_PERMISSIONS_NAME, NAME_FIELD, DESCRIPTION_FIELD, OWNER_TYPE_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD, JSON_METADATA_FIELD logger = logging.getLogger(__name__) @@ -45,15 +76,25 @@ class FilterSetRestApi(BaseSupersetModelRestApi): class_permission_name = FILTER_SET_API_PERMISSIONS_NAME allow_browser_login = True csrf_exempt = True - add_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_FIELD] + add_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_FIELD] add_model_schema = FilterSetPostSchema() edit_model_schema = FilterSetPutSchema() - edit_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_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, JSON_METADATA_FIELD] + edit_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_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, + JSON_METADATA_FIELD, + ] show_exclude_columns = [OWNER_OBJECT_FIELD, DASHBOARD_FIELD] - search_columns = ['id', NAME_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD] - base_filters = [[OWNER_ID_FIELD, FilterSetFilter, '']] + 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: [] @@ -70,16 +111,22 @@ def _init_properties(self) -> None: @permission_name("get") @rison(get_list_schema) @merge_response_func(ModelRestApi.merge_order_columns, API_ORDER_COLUMNS_RIS_KEY) - @merge_response_func(ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY) - @merge_response_func(ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY) + @merge_response_func( + ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY + ) + @merge_response_func( + ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY + ) @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, dashboard_id: int, **kwargs) -> Response: if not DashboardDAO.find_by_id(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)}) + 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("//filtersets", methods=["POST"]) @@ -119,11 +166,14 @@ def put(self, dashboard_id: int, pk: int) -> Response: return self.response(200, id=changed_model.id, result=item) except ValidationError as error: return self.response_400(message=error.message) - except (ObjectNotFoundError, FilterSetForbiddenError, FilterSetUpdateFailedError) as e: + except ( + ObjectNotFoundError, + FilterSetForbiddenError, + FilterSetUpdateFailedError, + ) as e: logger.error(e) return self.response(e.status) - @expose("//filtersets/", methods=["DELETE"]) @protect() @safe @@ -138,6 +188,10 @@ def delete(self, dashboard_id: int, pk: int) -> Response: return self.response(200, id=changed_model.id) except ValidationError as error: return self.response_400(message=error.message) - except (ObjectNotFoundError, FilterSetForbiddenError, FilterSetDeleteFailedError) as e: + except ( + ObjectNotFoundError, + FilterSetForbiddenError, + FilterSetDeleteFailedError, + ) as e: logger.error(e) return self.response(e.status) diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index 9765fa394b2b4..163a28ea71179 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -16,15 +16,21 @@ # under the License. import logging from typing import Optional + from flask_appbuilder.models.sqla import Model from flask_appbuilder.security.sqla.models import User + from superset.commands.base import BaseCommand from superset.dashboards.commands.exceptions import DashboardNotFoundError from superset.dashboards.dao import DashboardDAO -from superset.dashboards.filter_sets.commands.exceptions import FilterSetNotFoundError, FilterSetForbiddenError +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 -from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE + logger = logging.getLogger(__name__) @@ -50,7 +56,9 @@ def is_user_dashboard_owner(self) -> bool: def validate_exist_filter_use_cases_set(self): if self._filter_set_id: - self._filter_set = self._dashboard.filter_sets.get(self._filter_set_id, None) + self._filter_set = self._dashboard.filter_sets.get( + self._filter_set_id, None + ) if not self._filter_set: raise FilterSetNotFoundError(str(self._filter_set_id)) self.check_ownership() @@ -58,6 +66,12 @@ def validate_exist_filter_use_cases_set(self): def check_ownership(self): if self._filter_set.owner_type == USER_OWNER_TYPE: if self._actor.id != self._filter_set.owner_id: - raise FilterSetForbiddenError(str(self._filter_set_id), "The user is not the owner of the filter_set") + 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") + raise FilterSetForbiddenError( + str(self._filter_set_id), + "The user is not an owner of the filter_set's dashboard", + ) diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index a99242c6b4541..354b6d3751942 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -14,14 +14,23 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import Dict, Any import logging +from typing import Any, Dict + from flask_appbuilder.models.sqla import Model from flask_appbuilder.security.sqla.models import User + from superset.dashboards.filter_sets.commands.base import BaseFilterSetCommand -from superset.dashboards.filter_sets.commands.exceptions import UserIsNotDashboardOwnerError +from superset.dashboards.filter_sets.commands.exceptions import ( + 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 -from superset.dashboards.filter_sets.consts import DASHBOARD_ID_FIELD, DASHBOARD_OWNER_TYPE, OWNER_TYPE_FIELD, OWNER_ID_FIELD logger = logging.getLogger(__name__) @@ -40,8 +49,10 @@ def run(self) -> Model: def validate(self): super().validate() if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE: - if self._properties.get(OWNER_ID_FIELD, self._dashboard_id) != self._dashboard_id: + if ( + self._properties.get(OWNER_ID_FIELD, self._dashboard_id) + != self._dashboard_id + ): raise if not self.is_user_dashboard_owner(): raise UserIsNotDashboardOwnerError(str(self._dashboard_id)) - diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py index 095aaa39d68b6..02dd921420bc3 100644 --- a/superset/dashboards/filter_sets/commands/delete.py +++ b/superset/dashboards/filter_sets/commands/delete.py @@ -15,11 +15,17 @@ # 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 FilterSetNotFoundError, FilterSetForbiddenError, FilterSetDeleteFailedError +from superset.dashboards.filter_sets.commands.exceptions import ( + FilterSetDeleteFailedError, + FilterSetForbiddenError, + FilterSetNotFoundError, +) from superset.dashboards.filter_sets.dao import FilterSetDAO logger = logging.getLogger(__name__) @@ -35,7 +41,7 @@ def run(self) -> Model: self.validate() return FilterSetDAO.delete(self._filter_set, commit=True) except DAODeleteFailedError as e: - raise FilterSetDeleteFailedError(str(self._filter_set_id), '', e) + raise FilterSetDeleteFailedError(str(self._filter_set_id), "", e) def validate(self) -> None: super().validate() @@ -43,9 +49,9 @@ def validate(self) -> None: self.validate_exist_filter_use_cases_set() except FilterSetNotFoundError as e: if FilterSetDAO.find_by_id(self._filter_set_id): - FilterSetForbiddenError('the filter-set does not related to dashboard "%s"' % self._dashboard_id) + FilterSetForbiddenError( + 'the filter-set does not related to dashboard "%s"' + % self._dashboard_id + ) else: raise e - - - diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/dashboards/filter_sets/commands/exceptions.py index 47b0c64c336da..61289938066c7 100644 --- a/superset/dashboards/filter_sets/commands/exceptions.py +++ b/superset/dashboards/filter_sets/commands/exceptions.py @@ -15,52 +15,74 @@ # 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 ForbiddenError, CreateFailedError, ObjectNotFoundError, UpdateFailedError, DeleteFailedError + +from superset.commands.exceptions import ( + CreateFailedError, + DeleteFailedError, + ForbiddenError, + ObjectNotFoundError, + UpdateFailedError, +) class FilterSetNotFoundError(ObjectNotFoundError): - def __init__(self, filterset_id: str = None, exception: Optional[Exception] = None) -> None: - super().__init__('FilterSet', filterset_id, exception) + def __init__( + self, filterset_id: str = None, exception: Optional[Exception] = None + ) -> None: + super().__init__("FilterSet", filterset_id, exception) class FilterSetCreateFailedError(CreateFailedError): base_massage = 'CreateFilterSetCommand of dashboard "%s" failed: ' - def __init__(self, dashboard_id: str, reason: str = "", - exception: Optional[Exception] = None) -> None: + + def __init__( + self, dashboard_id: str, reason: str = "", exception: Optional[Exception] = None + ) -> None: super().__init__((self.base_massage % dashboard_id) + reason, exception) class FilterSetUpdateFailedError(UpdateFailedError): base_massage = 'UpdateFilterSetCommand of filter_set "%s" failed: ' - def __init__(self, filterset_id: str, reason: str = "", - exception: Optional[Exception] = None) -> None: + + def __init__( + self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None + ) -> None: super().__init__((self.base_massage % filterset_id) + reason, exception) class FilterSetDeleteFailedError(DeleteFailedError): base_massage = 'DeleteFilterSetCommand of filter_set "%s" failed: ' - def __init__(self, filterset_id: str, reason: str = "", - exception: Optional[Exception] = None) -> None: + + def __init__( + self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None + ) -> None: super().__init__((self.base_massage % 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: + 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 user is not the dashboard owner" - def __init__(self, dashboard_id: str, exception: Optional[Exception] = None) -> None: + 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: + def __init__( + self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None + ) -> None: super().__init__(_(self.message_format.format(filterset_id, reason)), exception) diff --git a/superset/dashboards/filter_sets/commands/update.py b/superset/dashboards/filter_sets/commands/update.py index 80dd5481037c2..fbeb09076ceb4 100644 --- a/superset/dashboards/filter_sets/commands/update.py +++ b/superset/dashboards/filter_sets/commands/update.py @@ -14,20 +14,27 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import Dict, Any 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.dao import FilterSetDAO +from superset.dashboards.filter_sets.commands.exceptions import ( + FilterSetUpdateFailedError, +) from superset.dashboards.filter_sets.consts import DASHBOARD_ID_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]): + 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() @@ -38,11 +45,8 @@ def run(self) -> Model: self._properties[DASHBOARD_ID_FIELD] = self._dashboard_id return FilterSetDAO.update(self._filter_set, self._properties, commit=True) except DAOUpdateFailedError as e: - raise FilterSetUpdateFailedError(str(self._filter_set_id), '', e) + raise FilterSetUpdateFailedError(str(self._filter_set_id), "", e) def validate(self) -> None: super().validate() self.validate_exist_filter_use_cases_set() - - - diff --git a/superset/dashboards/filter_sets/consts.py b/superset/dashboards/filter_sets/consts.py index 876d379c899cb..c2b694331bf53 100644 --- a/superset/dashboards/filter_sets/consts.py +++ b/superset/dashboards/filter_sets/consts.py @@ -14,16 +14,16 @@ # 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' +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' +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" -FILTER_SET_API_PERMISSIONS_NAME = 'FilterSets' +FILTER_SET_API_PERMISSIONS_NAME = "FilterSets" diff --git a/superset/dashboards/filter_sets/dao.py b/superset/dashboards/filter_sets/dao.py index 977571548beef..6addf705b20d3 100644 --- a/superset/dashboards/filter_sets/dao.py +++ b/superset/dashboards/filter_sets/dao.py @@ -14,16 +14,24 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import Any, Dict 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 -from superset.dashboards.filter_sets.consts import NAME_FIELD, JSON_METADATA_FIELD, \ - DESCRIPTION_FIELD, OWNER_ID_FIELD, OWNER_TYPE_FIELD, DASHBOARD_ID_FIELD logger = logging.getLogger(__name__) diff --git a/superset/dashboards/filter_sets/filters.py b/superset/dashboards/filter_sets/filters.py index c59bd2352e341..228575d03aad8 100644 --- a/superset/dashboards/filter_sets/filters.py +++ b/superset/dashboards/filter_sets/filters.py @@ -15,13 +15,17 @@ # 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 -from superset.dashboards.filter_sets.consts import DASHBOARD_OWNER_TYPE, USER_OWNER_TYPE + if TYPE_CHECKING: from sqlalchemy.orm.query import Query @@ -32,21 +36,23 @@ def apply(self, query: Query, value: Any) -> Query: return query current_user_id = g.user.id - filter_set_ids_by_dashboard_owners = query.from_self(FilterSet.id).\ - join(dashboard_user, FilterSet.owner_id == dashboard_user.c.dashboard_id).\ - filter( + filter_set_ids_by_dashboard_owners = ( + 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 + 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.owner_id == current_user_id, ), - FilterSet.id.in_(filter_set_ids_by_dashboard_owners) + FilterSet.id.in_(filter_set_ids_by_dashboard_owners), ) ) diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index f5c3e60c361cf..f9d315b6a4279 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -14,10 +14,15 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from marshmallow import fields, Schema, ValidationError, pre_load +from marshmallow import fields, pre_load, Schema, ValidationError from marshmallow.validate import Length -from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE, \ - OWNER_ID_FIELD, OWNER_TYPE_FIELD + +from superset.dashboards.filter_sets.consts import ( + DASHBOARD_OWNER_TYPE, + OWNER_ID_FIELD, + OWNER_TYPE_FIELD, + USER_OWNER_TYPE, +) class JsonMetadataSchema(Schema): @@ -26,24 +31,19 @@ class JsonMetadataSchema(Schema): class FilterSetPostSchema(Schema): - name = fields.String( - required=True, - allow_none=False, - validate=Length(0, 500), - ) - description = fields.String( - allow_none=True, - validate=[Length(1, 1000)] - ) + name = fields.String(required=True, allow_none=False, validate=Length(0, 500),) + description = fields.String(allow_none=True, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, required=True) owner_id = fields.Int(required=True) - owner_type = fields.String(required=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE]) + owner_type = fields.String( + required=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE] + ) @pre_load def validate(self, data, many, **kwargs): 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') + raise ValidationError("owner_id is mandatory when owner_type is User") return data @@ -51,12 +51,16 @@ class FilterSetPutSchema(Schema): name = fields.String(allow_none=False, validate=Length(0, 500)) description = fields.String(allow_none=False, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, allow_none=False) - owner_type = fields.String(allow_none=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE]) + owner_type = fields.String( + allow_none=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE] + ) def validate_pair(first_field, second_field, data): if first_field in data and second_field not in data: - raise ValidationError("{} must be included alongside {}".format(first_field, second_field)) + raise ValidationError( + "{} must be included alongside {}".format(first_field, second_field) + ) class FilterSetMetadataSchema(Schema): diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 1edba7e8cbe59..29fcfe2c0ea8c 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -23,11 +23,11 @@ """ # revision identifiers, used by Alembic. -revision = '3ebe0993c770' -down_revision = '67da9ef1ef9c' +revision = "3ebe0993c770" +down_revision = "67da9ef1ef9c" -from alembic import op import sqlalchemy as sa +from alembic import op def upgrade(): @@ -39,9 +39,11 @@ def upgrade(): 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("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"]), diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index e8f96e89ae19f..ffc406347c1b0 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -47,11 +47,13 @@ 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.exceptions import SupersetException 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 @@ -60,7 +62,6 @@ from superset.utils import core as utils from superset.utils.decorators import debounce from superset.utils.urls import get_url_path -from superset.common.request_contexed_based import is_user_admin # pylint: disable=too-many-public-methods @@ -68,7 +69,6 @@ config = app.config logger = logging.getLogger(__name__) -from superset.models.filter_set import FilterSet def copy_dashboard( _mapper: Mapper, connection: Connection, target: "Dashboard" @@ -173,7 +173,9 @@ def filter_sets(self): mapa = {"Dashboard": [], "User": []} for fs in self._filter_sets: mapa[fs.owner_type].append(fs) - rv = list(filter(lambda filter_set: filter_set.owner_id == current_user, mapa["User"])) + rv = list( + filter(lambda filter_set: filter_set.owner_id == current_user, mapa["User"]) + ) return {fs.id: fs for fs in rv + mapa["Dashboard"]} @property diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 2fffbc6df71f8..6cf2b1bf0edcf 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -15,25 +15,20 @@ # specific language governing permissions and limitations # under the License. from __future__ import annotations -from sqlalchemy_utils import generic_relationship + import json import logging from typing import Any, Dict + from flask_appbuilder import Model -from sqlalchemy import ( - Column, - Integer, - MetaData, - String, - Text, - ForeignKey, -) -from sqlalchemy.sql.elements import BinaryExpression +from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text from sqlalchemy.orm import relationship +from sqlalchemy.sql.elements import BinaryExpression +from sqlalchemy_utils import generic_relationship + from superset import app, db from superset.models.helpers import AuditMixinNullable - # pylint: disable=too-many-public-methods metadata = Model.metadata # pylint: disable=no-member From 8fddfd017a6d3389ca7381ebdb9c4e73aef29193 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 11 Apr 2021 15:30:22 +0300 Subject: [PATCH 03/60] add tests --- superset/dashboards/filter_sets/api.py | 108 ++------ tests/dashboards/filter_sets/__init__.py | 0 tests/dashboards/filter_sets/api_tests.py | 318 ++++++++++++++++++++++ tests/dashboards/filter_sets/utils.py | 0 4 files changed, 346 insertions(+), 80 deletions(-) create mode 100644 tests/dashboards/filter_sets/__init__.py create mode 100644 tests/dashboards/filter_sets/api_tests.py create mode 100644 tests/dashboards/filter_sets/utils.py diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index df616e22d9b45..8ef230cd185a1 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -15,57 +15,26 @@ # specific language governing permissions and limitations # under the License. import logging - -from flask import g, request, Response -from flask_appbuilder.api import ( - API_DESCRIPTION_COLUMNS_RIS_KEY, - API_LABEL_COLUMNS_RIS_KEY, - API_LIST_COLUMNS_RIS_KEY, - API_LIST_TITLE_RIS_KEY, - API_ORDER_COLUMNS_RIS_KEY, - expose, - get_list_schema, - merge_response_func, - ModelRestApi, - permission_name, - protect, - rison, - safe, -) +from flask import Response, request, g +from flask_appbuilder.api import expose, protect, safe, rison, permission_name, merge_response_func, \ + get_list_schema, API_ORDER_COLUMNS_RIS_KEY, API_LABEL_COLUMNS_RIS_KEY, \ + API_DESCRIPTION_COLUMNS_RIS_KEY, API_LIST_COLUMNS_RIS_KEY, API_LIST_TITLE_RIS_KEY, ModelRestApi from flask_appbuilder.models.sqla.interface import SQLAInterface from marshmallow import ValidationError - from superset import is_feature_enabled 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.exceptions import FilterSetForbiddenError, FilterSetUpdateFailedError, FilterSetDeleteFailedError, FilterSetCreateFailedError from superset.dashboards.filter_sets.commands.create import CreateFilterSetCommand -from superset.dashboards.filter_sets.commands.exceptions import ( - FilterSetCreateFailedError, - FilterSetDeleteFailedError, - FilterSetForbiddenError, - FilterSetUpdateFailedError, -) 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, -) from superset.dashboards.filter_sets.filters import FilterSetFilter -from superset.dashboards.filter_sets.schemas import ( - FilterSetPostSchema, - FilterSetPutSchema, -) +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 +from superset.dashboards.dao import DashboardDAO +from superset.dashboards.filter_sets.consts import OWNER_OBJECT_FIELD, DASHBOARD_FIELD, \ + FILTER_SET_API_PERMISSIONS_NAME, NAME_FIELD, DESCRIPTION_FIELD, OWNER_TYPE_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD, JSON_METADATA_FIELD logger = logging.getLogger(__name__) @@ -76,25 +45,15 @@ class FilterSetRestApi(BaseSupersetModelRestApi): class_permission_name = FILTER_SET_API_PERMISSIONS_NAME allow_browser_login = True csrf_exempt = True - add_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_FIELD] + add_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_FIELD] add_model_schema = FilterSetPostSchema() edit_model_schema = FilterSetPutSchema() - edit_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_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, - JSON_METADATA_FIELD, - ] + edit_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_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, JSON_METADATA_FIELD] show_exclude_columns = [OWNER_OBJECT_FIELD, DASHBOARD_FIELD] - search_columns = ["id", NAME_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD] - base_filters = [[OWNER_ID_FIELD, FilterSetFilter, ""]] + 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: [] @@ -111,22 +70,16 @@ def _init_properties(self) -> None: @permission_name("get") @rison(get_list_schema) @merge_response_func(ModelRestApi.merge_order_columns, API_ORDER_COLUMNS_RIS_KEY) - @merge_response_func( - ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY - ) - @merge_response_func( - ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY - ) + @merge_response_func(ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY) + @merge_response_func(ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, dashboard_id: int, **kwargs) -> Response: if not DashboardDAO.find_by_id(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)} - ) + 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("//filtersets", methods=["POST"]) @@ -144,7 +97,9 @@ def post(self, dashboard_id: int) -> Response: 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, FilterSetCreateFailedError) as error: + except ValidationError as error: + return self.response_400(message=error.messages) + except FilterSetCreateFailedError as error: return self.response_400(message=error.message) except DashboardNotFoundError: return self.response_404() @@ -165,15 +120,12 @@ def put(self, dashboard_id: int, pk: int) -> Response: 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.message) - except ( - ObjectNotFoundError, - FilterSetForbiddenError, - FilterSetUpdateFailedError, - ) as e: + return self.response_400(message=error.messages) + except (ObjectNotFoundError, FilterSetForbiddenError, FilterSetUpdateFailedError) as e: logger.error(e) return self.response(e.status) + @expose("//filtersets/", methods=["DELETE"]) @protect() @safe @@ -187,11 +139,7 @@ def delete(self, dashboard_id: int, pk: int) -> Response: changed_model = UpdateFilterSetCommand(g.user, dashboard_id, pk).run() return self.response(200, id=changed_model.id) except ValidationError as error: - return self.response_400(message=error.message) - except ( - ObjectNotFoundError, - FilterSetForbiddenError, - FilterSetDeleteFailedError, - ) as e: + return self.response_400(message=error.messages) + except (ObjectNotFoundError, FilterSetForbiddenError, FilterSetDeleteFailedError) as e: logger.error(e) return self.response(e.status) diff --git a/tests/dashboards/filter_sets/__init__.py b/tests/dashboards/filter_sets/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py new file mode 100644 index 0000000000000..36f3113e89b1b --- /dev/null +++ b/tests/dashboards/filter_sets/api_tests.py @@ -0,0 +1,318 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, Dict +import pytest +from flask import Response +from tests.test_app import app +from superset.dashboards.filter_sets.consts import NAME_FIELD, DESCRIPTION_FIELD, \ + JSON_METADATA_FIELD, DASHBOARD_ID_FIELD, OWNER_ID_FIELD, OWNER_TYPE_FIELD, USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE +from superset.models.dashboard import Dashboard +if TYPE_CHECKING: + from superset.models.dashboard import Dashboard +from tests.base_tests import logged_in_admin, login + +CREATE_FILTER_SET_URI = "api/v1/dashboard/{dashboard_id}/filtersets" + + +def call_create_filter_set(client, dashboard_id, data) -> Response: + uri = CREATE_FILTER_SET_URI.format(dashboard_id=dashboard_id) + return client.post(uri, json=data) + + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + + +@pytest.fixture +def dashboard() -> Dashboard: + return Dashboard(id=1) + + +@pytest.fixture +def valid_json_metadata() -> Dict: + return { + "nativeFilters": {} + } + + +@pytest.fixture +def exists_user_id(): + return 1 + + +@pytest.fixture +def valid_filter_set_data(dashboard: Dashboard, valid_json_metadata: Dict, exists_user_id: int) -> Dict: + dashboard_id = dashboard.id + name = 'test_filter_set_of_dashboard_' + str(dashboard_id) + return { + NAME_FIELD: name, + DESCRIPTION_FIELD: 'description of ' + name, + JSON_METADATA_FIELD: valid_json_metadata, + OWNER_TYPE_FIELD: USER_OWNER_TYPE, + OWNER_ID_FIELD: exists_user_id + } + + +class TestFilterSetsApi: + class TestCreate: + @pytest.mark.ofek + def test_with_extra_field__400(self, client, dashboard: Dashboard, valid_filter_set_data: Dict): + + # arrange + login(client, 'admin') + valid_filter_set_data['extra'] = 'val' + + # act + response = call_create_filter_set(client, dashboard.id, valid_filter_set_data) + + # assert + assert response.status_code == 400 + assert response.json['message']['extra'][0] == 'Unknown field.' + + def test_with_id_field__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data['id'] = 1 + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status_code == 400 + + def test_with_dashboard_not_exists__404(self, not_exists_dashboard: int, valid_filter_set_data: Dict): + # act + response = call_create_filter_set(not_exists_dashboard, valid_filter_set_data) + + # assert + assert response.status == 404 + + def test_without_name__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data.pop(NAME_FIELD, None) + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_with_none_name__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[NAME_FIELD] = None + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_with_int_as_name__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[NAME_FIELD] = 4 + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_without_description__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data.pop(DESCRIPTION_FIELD, None) + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 201 + + def test_with_none_description__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[DESCRIPTION_FIELD] = None + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 201 + + def test_with_int_as_description__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[DESCRIPTION_FIELD] = 1 + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_without_json_metadata__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data.pop(JSON_METADATA_FIELD, None) + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_with_invalid_json_metadata__400(self, dashboard: Dashboard, valid_filter_set_data: Dict, invalid_json_metadata: Dict): + # arrange + valid_filter_set_data[DESCRIPTION_FIELD] = invalid_json_metadata + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_without_dashboard_id_and_owner_type_is_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data.pop(DASHBOARD_ID_FIELD, None) + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 201 + + def test_without_dashboard_id_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data.pop(DASHBOARD_ID_FIELD, None) + valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 201 + + def test_with_dashboard_id_not_same_as_uri__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[DASHBOARD_ID_FIELD] = dashboard.id + 1 + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_with_dashboard_id_same_as_uri_and_owner_type_is_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 201 + + def test_with_dashboard_id_same_as_uri_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 201 + + def test_without_owner_type__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data.pop(OWNER_TYPE_FIELD, None) + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_with_invalid_owner_type__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[OWNER_TYPE_FIELD] = 'OTHER_TYPE' + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_without_owner_id_when_owner_type_is_user__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data.pop(OWNER_ID_FIELD, None) + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 400 + + def test_without_owner_id_when_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + # arrange + valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + valid_filter_set_data.pop(OWNER_ID_FIELD, None) + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 201 + + def test_with_not_exists_owner__403(self, dashboard: Dashboard, valid_filter_set_data: Dict, not_exists_user_id: int): + # arrange + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = not_exists_user_id + + # act + response = call_create_filter_set(dashboard.id, valid_filter_set_data) + + # assert + assert response.status == 403 + + def test_when_caller_is_admin_and_owner_is_admin__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_admin_and_owner_is_dashboard_owner__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_admin_and_owner_is_regular_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_admin_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_dashboard_owner_and_owner_is_admin__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_regular_user_and_owner_is_admin__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_regular_user_and_owner_is_regular_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(self, dashboard: Dashboard, valid_filter_set_data: Dict): + pass + + class TestGetFilterSets: + pass + + class TestUpdateFilterSet: + pass + + class TestDeleteFilterSet: + pass diff --git a/tests/dashboards/filter_sets/utils.py b/tests/dashboards/filter_sets/utils.py new file mode 100644 index 0000000000000..e69de29bb2d1d From 68aa395c4b8f0e1f2157642f60ebe5135c11de2e Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Tue, 13 Apr 2021 17:13:36 +0300 Subject: [PATCH 04/60] add tests and fixes based of failures --- superset/dashboards/filter_sets/api.py | 6 +- .../dashboards/filter_sets/commands/base.py | 26 +- .../dashboards/filter_sets/commands/create.py | 29 +- superset/dashboards/filter_sets/dao.py | 2 +- superset/dashboards/filter_sets/schemas.py | 42 +- superset/models/filter_set.py | 13 + tests/dashboards/filter_sets/api_tests.py | 397 +++++++++++++----- tests/dashboards/superset_factory_util.py | 11 +- 8 files changed, 358 insertions(+), 168 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 8ef230cd185a1..1c92f6ceeba02 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -24,7 +24,7 @@ from superset import is_feature_enabled from superset.commands.exceptions import ObjectNotFoundError from superset.dashboards.commands.exceptions import DashboardNotFoundError -from superset.dashboards.filter_sets.commands.exceptions import FilterSetForbiddenError, FilterSetUpdateFailedError, FilterSetDeleteFailedError, FilterSetCreateFailedError +from superset.dashboards.filter_sets.commands.exceptions import FilterSetForbiddenError, FilterSetUpdateFailedError, FilterSetDeleteFailedError, FilterSetCreateFailedError, UserIsNotDashboardOwnerError from superset.dashboards.filter_sets.commands.create import CreateFilterSetCommand from superset.dashboards.filter_sets.commands.update import UpdateFilterSetCommand from superset.dashboards.filter_sets.filters import FilterSetFilter @@ -82,7 +82,7 @@ def get_list(self, dashboard_id: int, **kwargs) -> Response: rison_data['filters'].append({'col': 'dashboard_id', 'opr': 'eq', 'value': str(dashboard_id)}) return self.get_list_headless(**kwargs) - @expose("//filtersets", methods=["POST"]) + @expose("//filtersets", methods=["POST"]) @protect() @safe @statsd_metrics @@ -99,6 +99,8 @@ def post(self, dashboard_id: int) -> Response: return self.response(201, id=new_model.id, result=item) except ValidationError as error: return self.response_400(message=error.messages) + except UserIsNotDashboardOwnerError as error: + return self.response_403() except FilterSetCreateFailedError as error: return self.response_400(message=error.message) except DashboardNotFoundError: diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index 163a28ea71179..4974f4fa0c91e 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -16,20 +16,16 @@ # under the License. import logging from typing import Optional - from flask_appbuilder.models.sqla import Model from flask_appbuilder.security.sqla.models import User - from superset.commands.base import BaseCommand +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.dashboards.filter_sets.commands.exceptions import FilterSetNotFoundError, FilterSetForbiddenError from superset.models.dashboard import Dashboard from superset.models.filter_set import FilterSet +from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE logger = logging.getLogger(__name__) @@ -52,13 +48,11 @@ def validate(self) -> None: raise DashboardNotFoundError() def is_user_dashboard_owner(self) -> bool: - return self._dashboard.am_i_owner() + return is_user_admin() or self._dashboard.am_i_owner() def validate_exist_filter_use_cases_set(self): if self._filter_set_id: - self._filter_set = self._dashboard.filter_sets.get( - self._filter_set_id, None - ) + self._filter_set = self._dashboard.filter_sets.get(self._filter_set_id, None) if not self._filter_set: raise FilterSetNotFoundError(str(self._filter_set_id)) self.check_ownership() @@ -66,12 +60,6 @@ def validate_exist_filter_use_cases_set(self): def check_ownership(self): if self._filter_set.owner_type == USER_OWNER_TYPE: if self._actor.id != self._filter_set.owner_id: - raise FilterSetForbiddenError( - str(self._filter_set_id), - "The user is not the owner of the filter_set", - ) + 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", - ) + raise FilterSetForbiddenError(str(self._filter_set_id), "The user is not an owner of the filter_set's dashboard") diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index 354b6d3751942..7103232021a53 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -14,24 +14,16 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from typing import Dict, Any 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.dashboards.filter_sets.commands.base import BaseFilterSetCommand -from superset.dashboards.filter_sets.commands.exceptions import ( - 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.commands.exceptions import UserIsNotDashboardOwnerError, FilterSetCreateFailedError from superset.dashboards.filter_sets.dao import FilterSetDAO - +from superset.dashboards.filter_sets.consts import DASHBOARD_ID_FIELD, DASHBOARD_OWNER_TYPE, OWNER_TYPE_FIELD, OWNER_ID_FIELD +from superset import security_manager logger = logging.getLogger(__name__) @@ -49,10 +41,13 @@ def run(self) -> Model: def validate(self): super().validate() if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE: - if ( - self._properties.get(OWNER_ID_FIELD, self._dashboard_id) - != self._dashboard_id - ): + if self._properties.get(OWNER_ID_FIELD, self._dashboard_id) != self._dashboard_id: raise if not self.is_user_dashboard_owner(): raise UserIsNotDashboardOwnerError(str(self._dashboard_id)) + else: + 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') + + diff --git a/superset/dashboards/filter_sets/dao.py b/superset/dashboards/filter_sets/dao.py index 6addf705b20d3..ba50e4d573959 100644 --- a/superset/dashboards/filter_sets/dao.py +++ b/superset/dashboards/filter_sets/dao.py @@ -47,7 +47,7 @@ def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model: 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[OWNER_ID_FIELD]) + 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: diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index f9d315b6a4279..28006f3b955ef 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -14,15 +14,10 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from marshmallow import fields, pre_load, Schema, ValidationError -from marshmallow.validate import Length - -from superset.dashboards.filter_sets.consts import ( - DASHBOARD_OWNER_TYPE, - OWNER_ID_FIELD, - OWNER_TYPE_FIELD, - USER_OWNER_TYPE, -) +from marshmallow import fields, Schema, ValidationError, post_load +from marshmallow.validate import Length, OneOf +from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE, \ + OWNER_ID_FIELD, OWNER_TYPE_FIELD class JsonMetadataSchema(Schema): @@ -31,19 +26,24 @@ class JsonMetadataSchema(Schema): class FilterSetPostSchema(Schema): - name = fields.String(required=True, allow_none=False, validate=Length(0, 500),) - description = fields.String(allow_none=True, validate=[Length(1, 1000)]) + name = fields.String( + required=True, + allow_none=False, + validate=Length(0, 500), + ) + description = fields.String( + allow_none=True, + validate=[Length(1, 1000)] + ) json_metadata = fields.Nested(JsonMetadataSchema, required=True) - owner_id = fields.Int(required=True) - owner_type = fields.String( - required=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE] - ) + owner_type = fields.String(required=True, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])) + owner_id = fields.Int(required=False) - @pre_load + @post_load def validate(self, data, many, **kwargs): 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") + raise ValidationError('owner_id is mandatory when owner_type is User') return data @@ -51,16 +51,12 @@ class FilterSetPutSchema(Schema): name = fields.String(allow_none=False, validate=Length(0, 500)) description = fields.String(allow_none=False, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, allow_none=False) - owner_type = fields.String( - allow_none=False, OneOf=[USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE] - ) + owner_type = fields.String(allow_none=False, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])) def validate_pair(first_field, second_field, data): if first_field in data and second_field not in data: - raise ValidationError( - "{} must be included alongside {}".format(first_field, second_field) - ) + raise ValidationError("{} must be included alongside {}".format(first_field, second_field)) class FilterSetMetadataSchema(Schema): diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 6cf2b1bf0edcf..3f5807047ab1e 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -96,6 +96,19 @@ def get(cls, id_or_slug: str) -> FilterSet: qry = session.query(FilterSet).filter(id_or_slug_filter(id_or_slug)) 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() + + def id_or_slug_filter(id_or_slug: str) -> BinaryExpression: if id_or_slug.isdigit(): diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py index 36f3113e89b1b..581d4c54c7916 100644 --- a/tests/dashboards/filter_sets/api_tests.py +++ b/tests/dashboards/filter_sets/api_tests.py @@ -4,14 +4,52 @@ from flask import Response from tests.test_app import app from superset.dashboards.filter_sets.consts import NAME_FIELD, DESCRIPTION_FIELD, \ - JSON_METADATA_FIELD, DASHBOARD_ID_FIELD, OWNER_ID_FIELD, OWNER_TYPE_FIELD, USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE + JSON_METADATA_FIELD, OWNER_ID_FIELD, OWNER_TYPE_FIELD, USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE from superset.models.dashboard import Dashboard +from superset.models.filter_set import FilterSet if TYPE_CHECKING: from superset.models.dashboard import Dashboard -from tests.base_tests import logged_in_admin, login + from flask_appbuilder.security.sqla.models import Role + from flask_appbuilder.security.manager import BaseSecurityManager +from tests.base_tests import login +from tests.dashboards.superset_factory_util import create_database, create_datasource_table, create_slice, create_dashboard +from superset import security_manager as sm + +security_manager: BaseSecurityManager = sm CREATE_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' + + +@pytest.fixture(autouse=True, scope='module') +def test_users(): + usernames = [ADMIN_USERNAME_FOR_TEST, DASHBOARD_OWNER_USERNAME, FILTER_SET_OWNER_USERNAME] + with app.app_context() as ctx: + filter_set_role: Role = security_manager.add_role('filter_set_role') + filterset_view_name = security_manager.find_view_menu('FilterSets') + all_datasource_view_name = security_manager.find_view_menu('all_datasource_access') + pvms = 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) + users = [] + admin_role = security_manager.find_role('Admin') + + for username in usernames: + roles_to_add = [admin_role] if username == ADMIN_USERNAME_FOR_TEST else [filter_set_role] + user = security_manager.add_user(username, 'test', 'test', username, roles_to_add, password='general') + users.append(user) + users = {user.username: user.id for user in users} + yield users + with app.app_context() as ctx: + session = ctx.app.appbuilder.get_session + for username in users.keys(): + session.delete(security_manager.find_user(username)) + session.commit() + + def call_create_filter_set(client, dashboard_id, data) -> Response: uri = CREATE_FILTER_SET_URI.format(dashboard_id=dashboard_id) @@ -25,8 +63,38 @@ def client(): @pytest.fixture -def dashboard() -> Dashboard: - return Dashboard(id=1) +def dashboard_id() -> Dashboard: + dashboard_id = None + dashboard = None + slice = None, + datasource = None + database = None + try: + with app.app_context() as ctx: + dashboard_owner_user = security_manager.find_user(DASHBOARD_OWNER_USERNAME) + database = create_database('test_database') + 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() + dashboard_id = dashboard.id + yield dashboard_id + except Exception as ex: + print(str(ex)) + finally: + with app.app_context() as ctx: + session = ctx.app.appbuilder.get_session + if dashboard_id is not None: + dashboard = Dashboard.get(str(dashboard_id)) + for fs in dashboard._filter_sets: + session.delete(fs) + session.delete(dashboard) + session.delete(slice) + session.delete(datasource) + session.delete(database) + session.commit() @pytest.fixture @@ -42,8 +110,7 @@ def exists_user_id(): @pytest.fixture -def valid_filter_set_data(dashboard: Dashboard, valid_json_metadata: Dict, exists_user_id: int) -> Dict: - dashboard_id = dashboard.id +def valid_filter_set_data(dashboard_id: int, valid_json_metadata: Dict, exists_user_id: int) -> Dict: name = 'test_filter_set_of_dashboard_' + str(dashboard_id) return { NAME_FIELD: name, @@ -54,259 +121,381 @@ def valid_filter_set_data(dashboard: Dashboard, valid_json_metadata: Dict, exist } +@pytest.fixture +def not_exists_dashboard(dashboard_id): + return dashboard_id + 1 + + +def get_filter_set_by_name(name): + with app.app_context() as ctx: + return FilterSet.get_by_name(name) + + +def get_filter_set_by_dashboard_id(dashboard_id): + with app.app_context() as ctx: + return FilterSet.get_by_dashboard_id(dashboard_id) + + +@pytest.fixture +def not_exists_user_id(): + return 99999 + + +@pytest.mark.ofek class TestFilterSetsApi: - class TestCreate: - @pytest.mark.ofek - def test_with_extra_field__400(self, client, dashboard: Dashboard, valid_filter_set_data: Dict): + class TestCreate: + def test_with_extra_field__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange login(client, 'admin') valid_filter_set_data['extra'] = 'val' # act - response = call_create_filter_set(client, dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert assert response.status_code == 400 assert response.json['message']['extra'][0] == 'Unknown field.' + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_with_id_field__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_with_id_field__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data['id'] = 1 # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert assert response.status_code == 400 + assert response.json['message']['id'][0] == 'Unknown field.' + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_with_dashboard_not_exists__404(self, not_exists_dashboard: int, valid_filter_set_data: Dict): + def test_with_dashboard_not_exists__404(self, not_exists_dashboard: int, valid_filter_set_data: Dict, client): # act - response = call_create_filter_set(not_exists_dashboard, valid_filter_set_data) + login(client, 'admin') + response = call_create_filter_set(client, not_exists_dashboard, valid_filter_set_data) # assert - assert response.status == 404 + assert response.status_code == 404 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_without_name__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_without_name__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data.pop(NAME_FIELD, None) # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 400 + assert get_filter_set_by_dashboard_id(dashboard_id) == [] - def test_with_none_name__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_with_none_name__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data[NAME_FIELD] = None # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_with_int_as_name__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_with_int_as_name__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data[NAME_FIELD] = 4 # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_without_description__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_without_description__201(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data.pop(DESCRIPTION_FIELD, None) # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 201 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_with_none_description__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_with_none_description__201(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data[DESCRIPTION_FIELD] = None # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 201 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_with_int_as_description__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_with_int_as_description__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data[DESCRIPTION_FIELD] = 1 # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_without_json_metadata__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_without_json_metadata__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data.pop(JSON_METADATA_FIELD, None) # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + + # assert + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None + + def test_with_invalid_json_metadata__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + # arrange + login(client, 'admin') + valid_filter_set_data[DESCRIPTION_FIELD] = {} + + # act + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + + # assert + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None + + def test_without_owner_type__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + # arrange + login(client, 'admin') + valid_filter_set_data.pop(OWNER_TYPE_FIELD, None) + + # act + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_with_invalid_json_metadata__400(self, dashboard: Dashboard, valid_filter_set_data: Dict, invalid_json_metadata: Dict): + def test_with_invalid_owner_type__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange - valid_filter_set_data[DESCRIPTION_FIELD] = invalid_json_metadata + login(client, 'admin') + valid_filter_set_data[OWNER_TYPE_FIELD] = 'OTHER_TYPE' # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_without_dashboard_id_and_owner_type_is_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_without_owner_id_when_owner_type_is_user__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange - valid_filter_set_data.pop(DASHBOARD_ID_FIELD, None) + login(client, 'admin') valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data.pop(OWNER_ID_FIELD, None) # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 201 + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_without_dashboard_id_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_without_owner_id_when_owner_type_is_dashboard__201(self, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange - valid_filter_set_data.pop(DASHBOARD_ID_FIELD, None) + login(client, 'admin') valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + valid_filter_set_data.pop(OWNER_ID_FIELD, None) # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 201 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_with_dashboard_id_not_same_as_uri__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_with_not_exists_owner__400(self, dashboard_id: int, valid_filter_set_data: Dict, not_exists_user_id: int, client): # arrange - valid_filter_set_data[DASHBOARD_ID_FIELD] = dashboard.id + 1 + login(client, 'admin') + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = not_exists_user_id # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 400 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None - def test_with_dashboard_id_same_as_uri_and_owner_type_is_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_when_caller_is_admin_and_owner_is_admin__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, 'admin') valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 201 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_with_dashboard_id_same_as_uri_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_when_caller_is_admin_and_owner_is_dashboard_owner__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange - valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + login(client, 'admin') + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, + valid_filter_set_data) # assert - assert response.status == 201 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_without_owner_type__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_when_caller_is_admin_and_owner_is_regular_user__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange - valid_filter_set_data.pop(OWNER_TYPE_FIELD, None) + login(client, 'admin') + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = test_users[FILTER_SET_OWNER_USERNAME] # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id,valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_with_invalid_owner_type__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_when_caller_is_admin_and_owner_type_is_dashboard__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange - valid_filter_set_data[OWNER_TYPE_FIELD] = 'OTHER_TYPE' + login(client, 'admin') + valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_without_owner_id_when_owner_type_is_user__400(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_when_caller_is_dashboard_owner_and_owner_is_admin__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, DASHBOARD_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data.pop(OWNER_ID_FIELD, None) + valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 400 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_without_owner_id_when_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): + def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange - valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE - valid_filter_set_data.pop(OWNER_ID_FIELD, None) + login(client, DASHBOARD_OWNER_USERNAME) + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 201 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_with_not_exists_owner__403(self, dashboard: Dashboard, valid_filter_set_data: Dict, not_exists_user_id: int): + def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): # arrange + login(client, DASHBOARD_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = not_exists_user_id + valid_filter_set_data[OWNER_ID_FIELD] = test_users[FILTER_SET_OWNER_USERNAME] # act - response = call_create_filter_set(dashboard.id, valid_filter_set_data) + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) # assert - assert response.status == 403 + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_when_caller_is_admin_and_owner_is_admin__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + # arrange + login(client, DASHBOARD_OWNER_USERNAME) + valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id + + # act + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + + # assert + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_when_caller_is_admin_and_owner_is_dashboard_owner__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass - def test_when_caller_is_admin_and_owner_is_regular_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + def test_when_caller_is_regular_user_and_owner_is_admin__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + # arrange + login(client, FILTER_SET_OWNER_USERNAME) + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] + + # act + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + + # assert + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_when_caller_is_admin_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + # arrange + login(client, FILTER_SET_OWNER_USERNAME) + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] - def test_when_caller_is_dashboard_owner_and_owner_is_admin__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + # act + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) - def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + # assert + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + def test_when_caller_is_regular_user_and_owner_is_regular_user__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + # arrange + login(client, FILTER_SET_OWNER_USERNAME) + valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = test_users[FILTER_SET_OWNER_USERNAME] - def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + # act + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) - def test_when_caller_is_regular_user_and_owner_is_admin__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + # assert + assert response.status_code == 201 + assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + # arrange + login(client, FILTER_SET_OWNER_USERNAME) + valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE + valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id - def test_when_caller_is_regular_user_and_owner_is_regular_user__201(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + # act + response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) - def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(self, dashboard: Dashboard, valid_filter_set_data: Dict): - pass + # assert + assert response.status_code == 403 + assert get_filter_set_by_name(valid_filter_set_data['name']) is None class TestGetFilterSets: pass diff --git a/tests/dashboards/superset_factory_util.py b/tests/dashboards/superset_factory_util.py index f62f4ac2cddf7..b6fb1b851115f 100644 --- a/tests/dashboards/superset_factory_util.py +++ b/tests/dashboards/superset_factory_util.py @@ -112,10 +112,14 @@ def create_slice_to_db( 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 [] + 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 ) @@ -123,7 +127,7 @@ def create_slice( slice_name=name, datasource_id=datasource_id, owners=owners, - datasource_type="table", + datasource_type=datasource_type, ) @@ -141,10 +145,13 @@ 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 [] + if database: + return SqlaTable(table_name=name, database=database, owners=owners) db_id = db_id or create_database_to_db(name=name + "_db").id return SqlaTable(table_name=name, database_id=db_id, owners=owners) From 9ca86fbe990b1fcff3a260b7e504027f16a6402d Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Wed, 14 Apr 2021 15:14:44 +0300 Subject: [PATCH 05/60] Fix pre-commit errors --- superset/commands/exceptions.py | 2 +- superset/commands/export.py | 4 +- superset/dashboards/commands/exceptions.py | 2 +- superset/dashboards/filter_sets/api.py | 112 ++- .../dashboards/filter_sets/commands/base.py | 32 +- .../dashboards/filter_sets/commands/create.py | 32 +- .../dashboards/filter_sets/commands/delete.py | 4 +- .../filter_sets/commands/exceptions.py | 2 +- superset/dashboards/filter_sets/dao.py | 6 +- superset/dashboards/filter_sets/schemas.py | 44 +- superset/models/dashboard.py | 8 +- superset/models/filter_set.py | 1 - tests/dashboards/filter_sets/api_tests.py | 639 ++++++++++++------ tests/dashboards/superset_factory_util.py | 18 +- 14 files changed, 632 insertions(+), 274 deletions(-) diff --git a/superset/commands/exceptions.py b/superset/commands/exceptions.py index 3de57788c524c..40c059765b6e7 100644 --- a/superset/commands/exceptions.py +++ b/superset/commands/exceptions.py @@ -38,7 +38,7 @@ class ObjectNotFoundError(CommandException): def __init__( self, object_type: str, - object_id: str = None, + object_id: Optional[str] = None, exception: Optional[Exception] = None, ) -> None: super().__init__( diff --git a/superset/commands/export.py b/superset/commands/export.py index 5bf117cca31ef..76a9694b0380b 100644 --- a/superset/commands/export.py +++ b/superset/commands/export.py @@ -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 @@ -34,7 +34,7 @@ class ExportModelsCommand(BaseCommand): dao = BaseDAO - not_found = CommandException + not_found: Type[CommandException] = CommandException def __init__(self, model_ids: List[int]): self.model_ids = model_ids diff --git a/superset/dashboards/commands/exceptions.py b/superset/dashboards/commands/exceptions.py index adef282abeb8b..37d3e5d7afffc 100644 --- a/superset/dashboards/commands/exceptions.py +++ b/superset/dashboards/commands/exceptions.py @@ -46,7 +46,7 @@ class DashboardInvalidError(CommandInvalidError): class DashboardNotFoundError(ObjectNotFoundError): def __init__( - self, dashboard_id: str = None, exception: Optional[Exception] = None + self, dashboard_id: Optional[str] = None, exception: Optional[Exception] = None ) -> None: super().__init__("Dashboard", dashboard_id, exception) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 1c92f6ceeba02..77f5df80d1b58 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -15,26 +15,59 @@ # specific language governing permissions and limitations # under the License. import logging -from flask import Response, request, g -from flask_appbuilder.api import expose, protect, safe, rison, permission_name, merge_response_func, \ - get_list_schema, API_ORDER_COLUMNS_RIS_KEY, API_LABEL_COLUMNS_RIS_KEY, \ - API_DESCRIPTION_COLUMNS_RIS_KEY, API_LIST_COLUMNS_RIS_KEY, API_LIST_TITLE_RIS_KEY, ModelRestApi +from typing import Any + +from flask import g, request, Response +from flask_appbuilder.api import ( + API_DESCRIPTION_COLUMNS_RIS_KEY, + API_LABEL_COLUMNS_RIS_KEY, + API_LIST_COLUMNS_RIS_KEY, + API_LIST_TITLE_RIS_KEY, + API_ORDER_COLUMNS_RIS_KEY, + expose, + get_list_schema, + merge_response_func, + ModelRestApi, + permission_name, + protect, + rison, + safe, +) from flask_appbuilder.models.sqla.interface import SQLAInterface from marshmallow import ValidationError -from superset import is_feature_enabled + from superset.commands.exceptions import ObjectNotFoundError from superset.dashboards.commands.exceptions import DashboardNotFoundError -from superset.dashboards.filter_sets.commands.exceptions import FilterSetForbiddenError, FilterSetUpdateFailedError, FilterSetDeleteFailedError, FilterSetCreateFailedError, UserIsNotDashboardOwnerError +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, + 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, +) from superset.dashboards.filter_sets.filters import FilterSetFilter -from superset.dashboards.filter_sets.schemas import FilterSetPostSchema, FilterSetPutSchema +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 -from superset.dashboards.dao import DashboardDAO -from superset.dashboards.filter_sets.consts import OWNER_OBJECT_FIELD, DASHBOARD_FIELD, \ - FILTER_SET_API_PERMISSIONS_NAME, NAME_FIELD, DESCRIPTION_FIELD, OWNER_TYPE_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD, JSON_METADATA_FIELD logger = logging.getLogger(__name__) @@ -45,20 +78,30 @@ class FilterSetRestApi(BaseSupersetModelRestApi): class_permission_name = FILTER_SET_API_PERMISSIONS_NAME allow_browser_login = True csrf_exempt = True - add_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_FIELD] + add_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_FIELD] add_model_schema = FilterSetPostSchema() edit_model_schema = FilterSetPutSchema() - edit_exclude_columns = ['id', OWNER_OBJECT_FIELD, DASHBOARD_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, JSON_METADATA_FIELD] + edit_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_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, + JSON_METADATA_FIELD, + ] show_exclude_columns = [OWNER_OBJECT_FIELD, DASHBOARD_FIELD] - search_columns = ['id', NAME_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD] - base_filters = [[OWNER_ID_FIELD, FilterSetFilter, '']] + 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: [] - if is_feature_enabled("THUMBNAILS"): - self.include_route_methods = self.include_route_methods | {"thumbnail"} + # if is_feature_enabled("THUMBNAILS"): + # self.include_route_methods = self.include_route_methods | {"thumbnail"} super().__init__() def _init_properties(self) -> None: @@ -70,16 +113,22 @@ def _init_properties(self) -> None: @permission_name("get") @rison(get_list_schema) @merge_response_func(ModelRestApi.merge_order_columns, API_ORDER_COLUMNS_RIS_KEY) - @merge_response_func(ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY) - @merge_response_func(ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY) + @merge_response_func( + ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY + ) + @merge_response_func( + ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY + ) @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) - def get_list(self, dashboard_id: int, **kwargs) -> Response: + def get_list(self, dashboard_id: int, **kwargs: Any) -> Response: if not DashboardDAO.find_by_id(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)}) + 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("//filtersets", methods=["POST"]) @@ -123,11 +172,14 @@ def put(self, dashboard_id: int, pk: int) -> Response: 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 e: + except ( + ObjectNotFoundError, + FilterSetForbiddenError, + FilterSetUpdateFailedError, + ) as e: logger.error(e) return self.response(e.status) - @expose("//filtersets/", methods=["DELETE"]) @protect() @safe @@ -138,10 +190,14 @@ def put(self, dashboard_id: int, pk: int) -> Response: ) def delete(self, dashboard_id: int, pk: int) -> Response: try: - changed_model = UpdateFilterSetCommand(g.user, dashboard_id, pk).run() + 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 (ObjectNotFoundError, FilterSetForbiddenError, FilterSetDeleteFailedError) as e: + except ( + ObjectNotFoundError, + FilterSetForbiddenError, + FilterSetDeleteFailedError, + ) as e: logger.error(e) return self.response(e.status) diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index 4974f4fa0c91e..aab7cb2898029 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -16,16 +16,21 @@ # under the License. import logging from typing import Optional + from flask_appbuilder.models.sqla import Model from flask_appbuilder.security.sqla.models import User + from superset.commands.base import BaseCommand 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 FilterSetNotFoundError, FilterSetForbiddenError +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 -from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE logger = logging.getLogger(__name__) @@ -50,16 +55,27 @@ def validate(self) -> None: def is_user_dashboard_owner(self) -> bool: return is_user_admin() or self._dashboard.am_i_owner() - def validate_exist_filter_use_cases_set(self): + def validate_exist_filter_use_cases_set(self) -> None: if self._filter_set_id: - self._filter_set = self._dashboard.filter_sets.get(self._filter_set_id, None) + self._filter_set = self._dashboard.filter_sets.get( + self._filter_set_id, None + ) if not self._filter_set: raise FilterSetNotFoundError(str(self._filter_set_id)) self.check_ownership() - def check_ownership(self): - if self._filter_set.owner_type == USER_OWNER_TYPE: + def check_ownership(self) -> None: + if ( + self._filter_set is not None + and self._filter_set.owner_type == USER_OWNER_TYPE + ): if self._actor.id != self._filter_set.owner_id: - raise FilterSetForbiddenError(str(self._filter_set_id), "The user is not the owner of the filter_set") + 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") + raise FilterSetForbiddenError( + str(self._filter_set_id), + "The user is not an owner of the filter_set's dashboard", + ) diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index 7103232021a53..3d5898bf4ec2a 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -14,16 +14,27 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import Dict, Any 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 UserIsNotDashboardOwnerError, FilterSetCreateFailedError +from superset.dashboards.filter_sets.commands.exceptions import ( + 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 -from superset.dashboards.filter_sets.consts import DASHBOARD_ID_FIELD, DASHBOARD_OWNER_TYPE, OWNER_TYPE_FIELD, OWNER_ID_FIELD -from superset import security_manager + logger = logging.getLogger(__name__) @@ -38,16 +49,19 @@ def run(self) -> Model: filter_set = FilterSetDAO.create(self._properties, commit=True) return filter_set - def validate(self): + def validate(self) -> None: super().validate() if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE: - if self._properties.get(OWNER_ID_FIELD, self._dashboard_id) != self._dashboard_id: + if ( + self._properties.get(OWNER_ID_FIELD, self._dashboard_id) + != self._dashboard_id + ): raise if not self.is_user_dashboard_owner(): raise UserIsNotDashboardOwnerError(str(self._dashboard_id)) else: 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') - - + raise FilterSetCreateFailedError( + str(self._dashboard_id), "owner_id does not exists" + ) diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py index 02dd921420bc3..ab4e36f320574 100644 --- a/superset/dashboards/filter_sets/commands/delete.py +++ b/superset/dashboards/filter_sets/commands/delete.py @@ -48,10 +48,10 @@ def validate(self) -> None: try: self.validate_exist_filter_use_cases_set() except FilterSetNotFoundError as e: - if FilterSetDAO.find_by_id(self._filter_set_id): + if FilterSetDAO.find_by_id(self._filter_set_id): # type: ignore FilterSetForbiddenError( 'the filter-set does not related to dashboard "%s"' - % self._dashboard_id + % str(self._dashboard_id) ) else: raise e diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/dashboards/filter_sets/commands/exceptions.py index 61289938066c7..36e13fc15940a 100644 --- a/superset/dashboards/filter_sets/commands/exceptions.py +++ b/superset/dashboards/filter_sets/commands/exceptions.py @@ -29,7 +29,7 @@ class FilterSetNotFoundError(ObjectNotFoundError): def __init__( - self, filterset_id: str = None, exception: Optional[Exception] = None + self, filterset_id: Optional[str] = None, exception: Optional[Exception] = None ) -> None: super().__init__("FilterSet", filterset_id, exception) diff --git a/superset/dashboards/filter_sets/dao.py b/superset/dashboards/filter_sets/dao.py index ba50e4d573959..da30afd71646e 100644 --- a/superset/dashboards/filter_sets/dao.py +++ b/superset/dashboards/filter_sets/dao.py @@ -47,7 +47,11 @@ def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model: 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_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: diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index 28006f3b955ef..d7ef7db52468a 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -14,10 +14,17 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from marshmallow import fields, Schema, ValidationError, post_load +from typing import Any, Dict + +from marshmallow import fields, post_load, Schema, ValidationError from marshmallow.validate import Length, OneOf -from superset.dashboards.filter_sets.consts import USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE, \ - OWNER_ID_FIELD, OWNER_TYPE_FIELD + +from superset.dashboards.filter_sets.consts import ( + DASHBOARD_OWNER_TYPE, + OWNER_ID_FIELD, + OWNER_TYPE_FIELD, + USER_OWNER_TYPE, +) class JsonMetadataSchema(Schema): @@ -26,24 +33,21 @@ class JsonMetadataSchema(Schema): class FilterSetPostSchema(Schema): - name = fields.String( - required=True, - allow_none=False, - validate=Length(0, 500), - ) - description = fields.String( - allow_none=True, - validate=[Length(1, 1000)] - ) + name = fields.String(required=True, allow_none=False, validate=Length(0, 500),) + description = fields.String(allow_none=True, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, required=True) - owner_type = fields.String(required=True, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])) + 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, many, **kwargs): + def validate( + self, data: Dict[str, Any], many: Any, **kwargs: Any + ) -> Dict[str, Any]: 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') + raise ValidationError("owner_id is mandatory when owner_type is User") return data @@ -51,12 +55,16 @@ class FilterSetPutSchema(Schema): name = fields.String(allow_none=False, validate=Length(0, 500)) description = fields.String(allow_none=False, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, allow_none=False) - owner_type = fields.String(allow_none=False, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE])) + owner_type = fields.String( + allow_none=False, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE]) + ) -def validate_pair(first_field, second_field, 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)) + raise ValidationError( + "{} must be included alongside {}".format(first_field, second_field) + ) class FilterSetMetadataSchema(Schema): diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index ffc406347c1b0..a9facfd1edd0b 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -19,7 +19,7 @@ import json import logging from functools import partial -from typing import Any, Callable, Dict, List, Set, Union +from typing import Any, Callable, Collection, Dict, List, Set, Union import sqlalchemy as sqla from flask import g @@ -166,11 +166,11 @@ def __repr__(self) -> str: return f"Dashboard<{self.id or self.slug}>" @property - def filter_sets(self): + def filter_sets(self) -> Dict[int, FilterSet]: if is_user_admin(): return self._filter_set current_user = g.user.id - mapa = {"Dashboard": [], "User": []} + mapa: Dict[str, List[Any]] = {"Dashboard": [], "User": []} for fs in self._filter_sets: mapa[fs.owner_type].append(fs) rv = list( @@ -390,7 +390,7 @@ def get(cls, id_or_slug: str) -> Dashboard: qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug)) return qry.one_or_none() - def am_i_owner(self): + def am_i_owner(self) -> bool: if g.user is None or g.user.is_anonymous or not g.user.is_authenticated: return False else: diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 3f5807047ab1e..a4cc367d85a7f 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -109,7 +109,6 @@ def get_by_dashboard_id(cls, dashboard_id: int) -> FilterSet: return qry.all() - def id_or_slug_filter(id_or_slug: str) -> BinaryExpression: if id_or_slug.isdigit(): return FilterSet.id == int(id_or_slug) diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py index 581d4c54c7916..b06c33acb050d 100644 --- a/tests/dashboards/filter_sets/api_tests.py +++ b/tests/dashboards/filter_sets/api_tests.py @@ -1,81 +1,122 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict + +from typing import Any, Dict, Generator, List, TYPE_CHECKING, Union + import pytest from flask import Response -from tests.test_app import app -from superset.dashboards.filter_sets.consts import NAME_FIELD, DESCRIPTION_FIELD, \ - JSON_METADATA_FIELD, OWNER_ID_FIELD, OWNER_TYPE_FIELD, USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE + +from superset import security_manager as sm +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 superset.models.dashboard import Dashboard from superset.models.filter_set import FilterSet +from tests.base_tests import login +from tests.dashboards.superset_factory_util import ( + create_dashboard, + create_database, + create_datasource_table, + create_slice, +) +from tests.test_app import app + if TYPE_CHECKING: - from superset.models.dashboard import Dashboard - from flask_appbuilder.security.sqla.models import Role + from flask.testing import FlaskClient + from flask_appbuilder.security.sqla.models import Role, User from flask_appbuilder.security.manager import BaseSecurityManager -from tests.base_tests import login -from tests.dashboards.superset_factory_util import create_database, create_datasource_table, create_slice, create_dashboard -from superset import security_manager as sm + from superset.models.dashboard import Dashboard security_manager: BaseSecurityManager = sm CREATE_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' - - -@pytest.fixture(autouse=True, scope='module') -def test_users(): - usernames = [ADMIN_USERNAME_FOR_TEST, DASHBOARD_OWNER_USERNAME, FILTER_SET_OWNER_USERNAME] - with app.app_context() as ctx: - filter_set_role: Role = security_manager.add_role('filter_set_role') - filterset_view_name = security_manager.find_view_menu('FilterSets') - all_datasource_view_name = security_manager.find_view_menu('all_datasource_access') - pvms = security_manager.find_permissions_view_menu(filterset_view_name) + security_manager.find_permissions_view_menu(all_datasource_view_name) +ADMIN_USERNAME_FOR_TEST = "admin@filterset.com" +DASHBOARD_OWNER_USERNAME = "dash_owner_user@filterset.com" +FILTER_SET_OWNER_USERNAME = "fs_owner_user@filterset.com" + + +@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, + ] + with app.app_context(): + filter_set_role: Role = security_manager.add_role("filter_set_role") + filterset_view_name = security_manager.find_view_menu("FilterSets") + all_datasource_view_name = security_manager.find_view_menu( + "all_datasource_access" + ) + pvms = 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) - users = [] - admin_role = security_manager.find_role('Admin') + users: List[User] = [] + admin_role = security_manager.find_role("Admin") for username in usernames: - roles_to_add = [admin_role] if username == ADMIN_USERNAME_FOR_TEST else [filter_set_role] - user = security_manager.add_user(username, 'test', 'test', username, roles_to_add, password='general') + roles_to_add = ( + [admin_role] + if username == ADMIN_USERNAME_FOR_TEST + else [filter_set_role] + ) + user = security_manager.add_user( + username, "test", "test", username, roles_to_add, password="general" + ) users.append(user) - users = {user.username: user.id for user in users} - yield users + usernames_to_ids: Dict[str, int] = {user.username: user.id for user in users} + yield usernames_to_ids with app.app_context() as ctx: session = ctx.app.appbuilder.get_session - for username in users.keys(): + for username in usernames_to_ids.keys(): session.delete(security_manager.find_user(username)) session.commit() - -def call_create_filter_set(client, dashboard_id, data) -> Response: +def call_create_filter_set( + client: FlaskClient[Any], dashboard_id: int, data: Dict[str, Any] +) -> Response: uri = CREATE_FILTER_SET_URI.format(dashboard_id=dashboard_id) return client.post(uri, json=data) @pytest.fixture -def client(): +def client() -> Generator[FlaskClient[Any], None, None]: with app.test_client() as client: yield client @pytest.fixture -def dashboard_id() -> Dashboard: +def dashboard_id() -> Generator[int, None, None]: dashboard_id = None dashboard = None - slice = None, + slice = (None,) datasource = None database = None try: with app.app_context() as ctx: dashboard_owner_user = security_manager.find_user(DASHBOARD_OWNER_USERNAME) - database = create_database('test_database') - 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]) + database = create_database("test_database") + 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() @@ -98,404 +139,616 @@ def dashboard_id() -> Dashboard: @pytest.fixture -def valid_json_metadata() -> Dict: - return { - "nativeFilters": {} - } +def valid_json_metadata() -> Dict[Any, Any]: + return {"nativeFilters": {}} @pytest.fixture -def exists_user_id(): +def exists_user_id() -> int: return 1 @pytest.fixture -def valid_filter_set_data(dashboard_id: int, valid_json_metadata: Dict, exists_user_id: int) -> Dict: - name = 'test_filter_set_of_dashboard_' + str(dashboard_id) +def valid_filter_set_data( + dashboard_id: int, valid_json_metadata: Dict[Any, Any], 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, + DESCRIPTION_FIELD: "description of " + name, JSON_METADATA_FIELD: valid_json_metadata, OWNER_TYPE_FIELD: USER_OWNER_TYPE, - OWNER_ID_FIELD: exists_user_id + OWNER_ID_FIELD: exists_user_id, } @pytest.fixture -def not_exists_dashboard(dashboard_id): +def not_exists_dashboard(dashboard_id: int) -> int: return dashboard_id + 1 -def get_filter_set_by_name(name): - with app.app_context() as ctx: +def get_filter_set_by_name(name: str) -> FilterSet: + with app.app_context(): return FilterSet.get_by_name(name) -def get_filter_set_by_dashboard_id(dashboard_id): - with app.app_context() as ctx: +def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet: + with app.app_context(): return FilterSet.get_by_dashboard_id(dashboard_id) @pytest.fixture -def not_exists_user_id(): +def not_exists_user_id() -> int: return 99999 @pytest.mark.ofek class TestFilterSetsApi: - class TestCreate: - def test_with_extra_field__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + def test_with_extra_field__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') - valid_filter_set_data['extra'] = 'val' + login(client, "admin") + valid_filter_set_data["extra"] = "val" # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert response.json['message']['extra'][0] == 'Unknown field.' - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_with_id_field__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert response.json["message"]["extra"][0] == "Unknown field." + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_with_id_field__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') - valid_filter_set_data['id'] = 1 + login(client, "admin") + valid_filter_set_data["id"] = 1 # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert response.json['message']['id'][0] == 'Unknown field.' - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_with_dashboard_not_exists__404(self, not_exists_dashboard: int, valid_filter_set_data: Dict, client): + assert response.json["message"]["id"][0] == "Unknown field." + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_with_dashboard_not_exists__404( + self, + not_exists_dashboard: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # act - login(client, 'admin') - response = call_create_filter_set(client, not_exists_dashboard, valid_filter_set_data) + login(client, "admin") + response = call_create_filter_set( + client, not_exists_dashboard, valid_filter_set_data + ) # assert assert response.status_code == 404 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_without_name__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_without_name__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data.pop(NAME_FIELD, None) # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # 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: Dict, client): + def test_with_none_name__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[NAME_FIELD] = None # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_with_int_as_name__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_with_int_as_name__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[NAME_FIELD] = 4 # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_without_description__201(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_without_description__201( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data.pop(DESCRIPTION_FIELD, None) # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_with_none_description__201(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_with_none_description__201( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[DESCRIPTION_FIELD] = None # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_with_int_as_description__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_with_int_as_description__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[DESCRIPTION_FIELD] = 1 # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_without_json_metadata__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_without_json_metadata__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data.pop(JSON_METADATA_FIELD, None) # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_with_invalid_json_metadata__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_with_invalid_json_metadata__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[DESCRIPTION_FIELD] = {} # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_without_owner_type__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_without_owner_type__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data.pop(OWNER_TYPE_FIELD, None) # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_with_invalid_owner_type__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_with_invalid_owner_type__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') - valid_filter_set_data[OWNER_TYPE_FIELD] = 'OTHER_TYPE' + login(client, "admin") + valid_filter_set_data[OWNER_TYPE_FIELD] = "OTHER_TYPE" # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_without_owner_id_when_owner_type_is_user__400(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_without_owner_id_when_owner_type_is_user__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data.pop(OWNER_ID_FIELD, None) # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_without_owner_id_when_owner_type_is_dashboard__201(self, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_without_owner_id_when_owner_type_is_dashboard__201( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE valid_filter_set_data.pop(OWNER_ID_FIELD, None) # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_with_not_exists_owner__400(self, dashboard_id: int, valid_filter_set_data: Dict, not_exists_user_id: int, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_with_not_exists_owner__400( + self, + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + not_exists_user_id: int, + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = not_exists_user_id # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None - - def test_when_caller_is_admin_and_owner_is_admin__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None + + def test_when_caller_is_admin_and_owner_is_admin__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_admin_and_owner_is_dashboard_owner__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_admin_and_owner_is_dashboard_owner__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] # act - response = call_create_filter_set(client, dashboard_id, - valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_admin_and_owner_is_regular_user__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_admin_and_owner_is_regular_user__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[FILTER_SET_OWNER_USERNAME] + valid_filter_set_data[OWNER_ID_FIELD] = test_users[ + FILTER_SET_OWNER_USERNAME + ] # act - response = call_create_filter_set(client, dashboard_id,valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_admin_and_owner_type_is_dashboard__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_admin_and_owner_type_is_dashboard__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange - login(client, 'admin') + login(client, "admin") valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_dashboard_owner_and_owner_is_admin__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_dashboard_owner_and_owner_is_admin__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, DASHBOARD_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, DASHBOARD_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, DASHBOARD_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[FILTER_SET_OWNER_USERNAME] + valid_filter_set_data[OWNER_ID_FIELD] = test_users[ + FILTER_SET_OWNER_USERNAME + ] # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, DASHBOARD_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - - def test_when_caller_is_regular_user_and_owner_is_admin__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_regular_user_and_owner_is_admin__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, FILTER_SET_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, FILTER_SET_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_regular_user_and_owner_is_regular_user__201(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_regular_user_and_owner_is_regular_user__201( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, FILTER_SET_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[FILTER_SET_OWNER_USERNAME] + valid_filter_set_data[OWNER_ID_FIELD] = test_users[ + FILTER_SET_OWNER_USERNAME + ] # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data['name']) is not None - - def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403(self, test_users, dashboard_id: int, valid_filter_set_data: Dict, client): + assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None + + def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403( + self, + test_users: Dict[str, int], + dashboard_id: int, + valid_filter_set_data: Dict[str, Any], + client: FlaskClient[Any], + ): # arrange login(client, FILTER_SET_OWNER_USERNAME) valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id # act - response = call_create_filter_set(client, dashboard_id, valid_filter_set_data) + response = call_create_filter_set( + client, dashboard_id, valid_filter_set_data + ) # assert assert response.status_code == 403 - assert get_filter_set_by_name(valid_filter_set_data['name']) is None + assert get_filter_set_by_name(valid_filter_set_data["name"]) is None class TestGetFilterSets: pass diff --git a/tests/dashboards/superset_factory_util.py b/tests/dashboards/superset_factory_util.py index b6fb1b851115f..bc482963c043b 100644 --- a/tests/dashboards/superset_factory_util.py +++ b/tests/dashboards/superset_factory_util.py @@ -105,20 +105,28 @@ 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] = None, datasource: Optional[SqlaTable] = None, name: Optional[str] = None, owners: Optional[List[User]] = None + 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 [] - datasource_type = 'table' + datasource_type = "table" if datasource: - return Slice(slice_name=name, table=datasource, owners=owners, datasource_type=datasource_type) + 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 @@ -136,7 +144,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 From f292dea5f23143f854aa122316c376712f3e05f9 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Wed, 14 Apr 2021 15:19:18 +0300 Subject: [PATCH 06/60] chore init filterset resource under ff constraint --- superset/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/superset/app.py b/superset/app.py index d0c9188484ad3..0613429255541 100644 --- a/superset/app.py +++ b/superset/app.py @@ -523,9 +523,9 @@ def init_views(self) -> None: icon="fa-cog", ) appbuilder.add_separator("Data") - from superset.dashboards.filter_sets.api import FilterSetRestApi - - appbuilder.add_api(FilterSetRestApi) + if feature_flag_manager.is_feature_enabled("DASHBOARD_NATIVE_FILTERS_SET"): + from superset.dashboards.filter_sets.api import FilterSetRestApi + appbuilder.add_api(FilterSetRestApi) def init_app_in_ctx(self) -> None: """ From 2a782f959783eaa00fe5b7f110206dd45ef87c27 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 11:38:47 +0300 Subject: [PATCH 07/60] Fix migration conflicts --- superset/migrations/versions/3ebe0993c770_filterset_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 29fcfe2c0ea8c..ea400c8ffaffc 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -24,7 +24,7 @@ # revision identifiers, used by Alembic. revision = "3ebe0993c770" -down_revision = "67da9ef1ef9c" +down_revision = "19e978e1b9c3" import sqlalchemy as sa from alembic import op From 8cd164e1d101218e5d52c938450bce89a58fc3d2 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 12:31:56 +0300 Subject: [PATCH 08/60] Fix pylint and migrations issues --- superset/dashboards/commands/exceptions.py | 1 - superset/dashboards/filter_sets/api.py | 27 +++++++++++----------- superset/dashboards/filter_sets/filters.py | 2 +- superset/dashboards/filter_sets/schemas.py | 6 ++--- superset/models/dashboard.py | 7 +++--- superset/models/filter_set.py | 15 +++--------- tests/dashboards/filter_sets/__init__.py | 16 +++++++++++++ tests/dashboards/filter_sets/api_tests.py | 17 +++++++++++++- tests/dashboards/filter_sets/utils.py | 0 9 files changed, 55 insertions(+), 36 deletions(-) delete mode 100644 tests/dashboards/filter_sets/utils.py diff --git a/superset/dashboards/commands/exceptions.py b/superset/dashboards/commands/exceptions.py index 37d3e5d7afffc..1a5bdaf789f67 100644 --- a/superset/dashboards/commands/exceptions.py +++ b/superset/dashboards/commands/exceptions.py @@ -20,7 +20,6 @@ from marshmallow.validate import ValidationError from superset.commands.exceptions import ( - CommandException, CommandInvalidError, CreateFailedError, DeleteFailedError, diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 77f5df80d1b58..6173bead86d61 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -77,7 +77,7 @@ class FilterSetRestApi(BaseSupersetModelRestApi): resource_name = "dashboard" class_permission_name = FILTER_SET_API_PERMISSIONS_NAME allow_browser_login = True - csrf_exempt = True + csrf_exempt = False add_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_FIELD] add_model_schema = FilterSetPostSchema() edit_model_schema = FilterSetPutSchema() @@ -100,12 +100,10 @@ class FilterSetRestApi(BaseSupersetModelRestApi): def __init__(self) -> None: self.datamodel.get_search_columns_list = lambda: [] - # if is_feature_enabled("THUMBNAILS"): - # self.include_route_methods = self.include_route_methods | {"thumbnail"} super().__init__() def _init_properties(self) -> None: - super(BaseSupersetModelRestApi, self)._init_properties() + super(BaseSupersetModelRestApi, self)._init_properties() # pylint: disable=E1003 @expose("//filtersets", methods=["GET"]) @protect() @@ -121,7 +119,8 @@ def _init_properties(self) -> None: ) @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) - def get_list(self, dashboard_id: int, **kwargs: Any) -> Response: + def get_list(self, **kwargs: Any) -> Response: + dashboard_id = kwargs.get('dashboard_id') if not DashboardDAO.find_by_id(dashboard_id): return self.response(404, message="dashboard '%s' not found" % dashboard_id) rison_data = kwargs.setdefault("rison", {}) @@ -139,7 +138,7 @@ def get_list(self, dashboard_id: int, **kwargs: Any) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post", log_to_statsd=False, ) - def post(self, dashboard_id: int) -> Response: + def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 if not request.is_json: return self.response_400(message="Request is not JSON") try: @@ -163,7 +162,7 @@ def post(self, dashboard_id: int) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put", log_to_statsd=False, ) - def put(self, dashboard_id: int, pk: int) -> Response: + def put(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 if not request.is_json: return self.response_400(message="Request is not JSON") try: @@ -176,9 +175,9 @@ def put(self, dashboard_id: int, pk: int) -> Response: ObjectNotFoundError, FilterSetForbiddenError, FilterSetUpdateFailedError, - ) as e: - logger.error(e) - return self.response(e.status) + ) as err: + logger.error(err) + return self.response(err.status) @expose("//filtersets/", methods=["DELETE"]) @protect() @@ -188,7 +187,7 @@ def put(self, dashboard_id: int, pk: int) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete", log_to_statsd=False, ) - def delete(self, dashboard_id: int, pk: int) -> Response: + def delete(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 try: changed_model = DeleteFilterSetCommand(g.user, dashboard_id, pk).run() return self.response(200, id=changed_model.id) @@ -198,6 +197,6 @@ def delete(self, dashboard_id: int, pk: int) -> Response: ObjectNotFoundError, FilterSetForbiddenError, FilterSetDeleteFailedError, - ) as e: - logger.error(e) - return self.response(e.status) + ) as err: + logger.error(err) + return self.response(err.status) diff --git a/superset/dashboards/filter_sets/filters.py b/superset/dashboards/filter_sets/filters.py index 228575d03aad8..6a0ae9e316660 100644 --- a/superset/dashboards/filter_sets/filters.py +++ b/superset/dashboards/filter_sets/filters.py @@ -36,7 +36,7 @@ def apply(self, query: Query, value: Any) -> Query: return query current_user_id = g.user.id - filter_set_ids_by_dashboard_owners = ( + 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( diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index d7ef7db52468a..54941711011ff 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -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 +from typing import Any, Dict, Mapping from marshmallow import fields, post_load, Schema, ValidationError from marshmallow.validate import Length, OneOf @@ -33,7 +33,7 @@ class JsonMetadataSchema(Schema): class FilterSetPostSchema(Schema): - name = fields.String(required=True, allow_none=False, validate=Length(0, 500),) + name = fields.String(required=True, allow_none=False, validate=Length(0, 500), ) description = fields.String(allow_none=True, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, required=True) @@ -44,7 +44,7 @@ class FilterSetPostSchema(Schema): @post_load def validate( - self, data: Dict[str, Any], many: Any, **kwargs: Any + self, data: Mapping, *, many: Any, partial: Any # pylint: disable=W0613 ) -> Dict[str, Any]: 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") diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index a9facfd1edd0b..2a51a1bfc4108 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -19,7 +19,7 @@ import json import logging from functools import partial -from typing import Any, Callable, Collection, Dict, List, Set, Union +from typing import Any, Callable, Dict, List, Set, Union import sqlalchemy as sqla from flask import g @@ -168,7 +168,7 @@ def __repr__(self) -> str: @property def filter_sets(self) -> Dict[int, FilterSet]: if is_user_admin(): - return self._filter_set + return self._filter_sets current_user = g.user.id mapa: Dict[str, List[Any]] = {"Dashboard": [], "User": []} for fs in self._filter_sets: @@ -393,8 +393,7 @@ def get(cls, id_or_slug: str) -> Dashboard: def am_i_owner(self) -> bool: if g.user is None or g.user.is_anonymous or not g.user.is_authenticated: return False - else: - return g.user.id in set(map(lambda user: user.id, self.owners)) + return g.user.id in set(map(lambda user: user.id, self.owners)) def id_or_slug_filter(id_or_slug: str) -> BinaryExpression: diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index a4cc367d85a7f..57f1c475ae3be 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -50,15 +50,12 @@ class FilterSet( # pylint: disable=too-many-instance-attributes owner_type = Column(String(255), nullable=False) owner_object = generic_relationship(owner_type, owner_id) - def __init__(self) -> None: - super().__init__() - def __repr__(self) -> str: return f"FilterSet<{self.name or self.id}>" @property def url(self) -> str: - return f"/api/filtersets/{self.slug or self.id}/" + return f"/api/filtersets/{self.id}/" @property def sqla_metadata(self) -> None: @@ -91,9 +88,9 @@ def data(self) -> Dict[str, Any]: } @classmethod - def get(cls, id_or_slug: str) -> FilterSet: + def get(cls, _id: int) -> FilterSet: session = db.session() - qry = session.query(FilterSet).filter(id_or_slug_filter(id_or_slug)) + qry = session.query(FilterSet).filter(_id) return qry.one_or_none() @classmethod @@ -107,9 +104,3 @@ 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() - - -def id_or_slug_filter(id_or_slug: str) -> BinaryExpression: - if id_or_slug.isdigit(): - return FilterSet.id == int(id_or_slug) - return FilterSet.slug == id_or_slug diff --git a/tests/dashboards/filter_sets/__init__.py b/tests/dashboards/filter_sets/__init__.py index e69de29bb2d1d..13a83393a9124 100644 --- a/tests/dashboards/filter_sets/__init__.py +++ b/tests/dashboards/filter_sets/__init__.py @@ -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. diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py index b06c33acb050d..0442d1f35fead 100644 --- a/tests/dashboards/filter_sets/api_tests.py +++ b/tests/dashboards/filter_sets/api_tests.py @@ -1,3 +1,19 @@ +# 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, Generator, List, TYPE_CHECKING, Union @@ -181,7 +197,6 @@ def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet: def not_exists_user_id() -> int: return 99999 - @pytest.mark.ofek class TestFilterSetsApi: class TestCreate: diff --git a/tests/dashboards/filter_sets/utils.py b/tests/dashboards/filter_sets/utils.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 From 3ab9609812396a6e60bf64ff305457f29032994a Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 12:58:22 +0300 Subject: [PATCH 09/60] Fix pylint and migrations issues --- superset/dashboards/filter_sets/api.py | 4 +++- superset/models/filter_set.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 6173bead86d61..5abdd901e1753 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -73,6 +73,8 @@ class FilterSetRestApi(BaseSupersetModelRestApi): + # pylint: disable=W0221 + datamodel = SQLAInterface(FilterSet) resource_name = "dashboard" class_permission_name = FILTER_SET_API_PERMISSIONS_NAME @@ -162,7 +164,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put", log_to_statsd=False, ) - def put(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 + def put(self, dashboard_id: int, pk: int) -> Response: if not request.is_json: return self.response_400(message="Request is not JSON") try: diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 57f1c475ae3be..a98f69973c74d 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -23,7 +23,6 @@ from flask_appbuilder import Model from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text from sqlalchemy.orm import relationship -from sqlalchemy.sql.elements import BinaryExpression from sqlalchemy_utils import generic_relationship from superset import app, db From 6c2ed5c668e00fe48160e1ed2a4aabc1de47752a Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 13:15:22 +0300 Subject: [PATCH 10/60] Fix pylint and migrations issues --- superset/dashboards/filter_sets/api.py | 10 +++++----- superset/dashboards/filter_sets/commands/base.py | 2 +- superset/dashboards/filter_sets/commands/create.py | 3 ++- superset/dashboards/filter_sets/commands/delete.py | 8 ++++---- superset/dashboards/filter_sets/commands/exceptions.py | 2 +- superset/dashboards/filter_sets/commands/update.py | 4 ++-- superset/dashboards/filter_sets/schemas.py | 6 +++--- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 5abdd901e1753..8231f4fa9b489 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. import logging -from typing import Any +from typing import Any, cast, Optional from flask import g, request, Response from flask_appbuilder.api import ( @@ -107,7 +107,7 @@ def __init__(self) -> None: def _init_properties(self) -> None: super(BaseSupersetModelRestApi, self)._init_properties() # pylint: disable=E1003 - @expose("//filtersets", methods=["GET"]) + @expose("//filtersets", methods=["GET"]) @protect() @safe @permission_name("get") @@ -122,8 +122,8 @@ def _init_properties(self) -> None: @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, **kwargs: Any) -> Response: - dashboard_id = kwargs.get('dashboard_id') - if not DashboardDAO.find_by_id(dashboard_id): + dashboard_id: Optional[int] = kwargs.get('dashboard_id', None) + 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", []) @@ -149,7 +149,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 return self.response(201, id=new_model.id, result=item) except ValidationError as error: return self.response_400(message=error.messages) - except UserIsNotDashboardOwnerError as error: + except UserIsNotDashboardOwnerError: return self.response_403() except FilterSetCreateFailedError as error: return self.response_400(message=error.message) diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index aab7cb2898029..e3f686273b466 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -55,7 +55,7 @@ def validate(self) -> None: def is_user_dashboard_owner(self) -> bool: return is_user_admin() or self._dashboard.am_i_owner() - def validate_exist_filter_use_cases_set(self) -> None: + def validate_exist_filter_use_cases_set(self) -> None: # pylint: disable=C0103 if self._filter_set_id: self._filter_set = self._dashboard.filter_sets.get( self._filter_set_id, None diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index 3d5898bf4ec2a..e749703f8881e 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -26,6 +26,7 @@ from superset.dashboards.filter_sets.commands.exceptions import ( FilterSetCreateFailedError, UserIsNotDashboardOwnerError, + DashboardIdInconsistencyError ) from superset.dashboards.filter_sets.consts import ( DASHBOARD_ID_FIELD, @@ -56,7 +57,7 @@ def validate(self) -> None: self._properties.get(OWNER_ID_FIELD, self._dashboard_id) != self._dashboard_id ): - raise + raise DashboardIdInconsistencyError(str(self._dashboard_id)) if not self.is_user_dashboard_owner(): raise UserIsNotDashboardOwnerError(str(self._dashboard_id)) else: diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py index ab4e36f320574..f98705e29b0f4 100644 --- a/superset/dashboards/filter_sets/commands/delete.py +++ b/superset/dashboards/filter_sets/commands/delete.py @@ -40,18 +40,18 @@ def run(self) -> Model: try: self.validate() return FilterSetDAO.delete(self._filter_set, commit=True) - except DAODeleteFailedError as e: - raise FilterSetDeleteFailedError(str(self._filter_set_id), "", e) + except DAODeleteFailedError as err: + raise FilterSetDeleteFailedError(str(self._filter_set_id), "", err) def validate(self) -> None: super().validate() try: self.validate_exist_filter_use_cases_set() - except FilterSetNotFoundError as e: + except FilterSetNotFoundError as err: if FilterSetDAO.find_by_id(self._filter_set_id): # type: ignore FilterSetForbiddenError( 'the filter-set does not related to dashboard "%s"' % str(self._dashboard_id) ) else: - raise e + raise err diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/dashboards/filter_sets/commands/exceptions.py index 36e13fc15940a..3274821c6a8d0 100644 --- a/superset/dashboards/filter_sets/commands/exceptions.py +++ b/superset/dashboards/filter_sets/commands/exceptions.py @@ -71,7 +71,7 @@ def __init__( class DashboardIdInconsistencyError(FilterSetCreateFailedError): - reason = "cannot create dashboard owner filterset based when the user is not the dashboard owner" + 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 diff --git a/superset/dashboards/filter_sets/commands/update.py b/superset/dashboards/filter_sets/commands/update.py index fbeb09076ceb4..45df2511f229e 100644 --- a/superset/dashboards/filter_sets/commands/update.py +++ b/superset/dashboards/filter_sets/commands/update.py @@ -44,8 +44,8 @@ def run(self) -> Model: self.validate() self._properties[DASHBOARD_ID_FIELD] = self._dashboard_id return FilterSetDAO.update(self._filter_set, self._properties, commit=True) - except DAOUpdateFailedError as e: - raise FilterSetUpdateFailedError(str(self._filter_set_id), "", e) + except DAOUpdateFailedError as err: + raise FilterSetUpdateFailedError(str(self._filter_set_id), "", err) def validate(self) -> None: super().validate() diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index 54941711011ff..b2210d05d06df 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -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, Mapping +from typing import Any, Dict, Mapping, cast from marshmallow import fields, post_load, Schema, ValidationError from marshmallow.validate import Length, OneOf @@ -44,11 +44,11 @@ class FilterSetPostSchema(Schema): @post_load def validate( - self, data: Mapping, *, many: Any, partial: Any # pylint: disable=W0613 + self, data: Mapping[Any, Any], *, many: Any, partial: Any # pylint: disable=W0613 ) -> Dict[str, Any]: 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 data + return cast(Dict[str, Any], data) class FilterSetPutSchema(Schema): From 4c87d47865a94aa8d9e219c2509a3a692b5043b7 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 13:24:56 +0300 Subject: [PATCH 11/60] Fix pylint and migrations issues --- superset/app.py | 1 + superset/dashboards/filter_sets/api.py | 6 ++++-- superset/dashboards/filter_sets/commands/create.py | 2 +- superset/dashboards/filter_sets/commands/exceptions.py | 10 ++++++++-- superset/dashboards/filter_sets/filters.py | 2 +- superset/dashboards/filter_sets/schemas.py | 10 +++++++--- tests/dashboards/filter_sets/api_tests.py | 1 + 7 files changed, 23 insertions(+), 9 deletions(-) diff --git a/superset/app.py b/superset/app.py index d39189d04c212..220ddea47a3a3 100644 --- a/superset/app.py +++ b/superset/app.py @@ -527,6 +527,7 @@ def init_views(self) -> None: appbuilder.add_separator("Data") if feature_flag_manager.is_feature_enabled("DASHBOARD_NATIVE_FILTERS_SET"): from superset.dashboards.filter_sets.api import FilterSetRestApi + appbuilder.add_api(FilterSetRestApi) def init_app_in_ctx(self) -> None: diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 8231f4fa9b489..6db93cb62f880 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -105,7 +105,9 @@ def __init__(self) -> None: super().__init__() def _init_properties(self) -> None: - super(BaseSupersetModelRestApi, self)._init_properties() # pylint: disable=E1003 + super( + BaseSupersetModelRestApi, self + )._init_properties() # pylint: disable=E1003 @expose("//filtersets", methods=["GET"]) @protect() @@ -122,7 +124,7 @@ def _init_properties(self) -> None: @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, **kwargs: Any) -> Response: - dashboard_id: Optional[int] = kwargs.get('dashboard_id', None) + dashboard_id: Optional[int] = kwargs.get("dashboard_id", None) 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", {}) diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index e749703f8881e..deaaa7b1fa635 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -24,9 +24,9 @@ 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, - DashboardIdInconsistencyError ) from superset.dashboards.filter_sets.consts import ( DASHBOARD_ID_FIELD, diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/dashboards/filter_sets/commands/exceptions.py index 3274821c6a8d0..323028338f3ac 100644 --- a/superset/dashboards/filter_sets/commands/exceptions.py +++ b/superset/dashboards/filter_sets/commands/exceptions.py @@ -62,7 +62,10 @@ def __init__( class UserIsNotDashboardOwnerError(FilterSetCreateFailedError): - reason = "cannot create dashboard owner filterset based when the user is not the dashboard owner" + 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 @@ -71,7 +74,10 @@ def __init__( class DashboardIdInconsistencyError(FilterSetCreateFailedError): - reason = "cannot create dashboard owner filterset based when the ownerid is not the dashboard id" + 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 diff --git a/superset/dashboards/filter_sets/filters.py b/superset/dashboards/filter_sets/filters.py index 6a0ae9e316660..fefa265d45588 100644 --- a/superset/dashboards/filter_sets/filters.py +++ b/superset/dashboards/filter_sets/filters.py @@ -36,7 +36,7 @@ def apply(self, query: Query, value: Any) -> Query: return query current_user_id = g.user.id - filter_set_ids_by_dashboard_owners = ( # pylint: disable=C0103 + 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( diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index b2210d05d06df..0203fb5873e96 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -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, Mapping, cast +from typing import Any, cast, Dict, Mapping from marshmallow import fields, post_load, Schema, ValidationError from marshmallow.validate import Length, OneOf @@ -33,7 +33,7 @@ class JsonMetadataSchema(Schema): class FilterSetPostSchema(Schema): - name = fields.String(required=True, allow_none=False, validate=Length(0, 500), ) + name = fields.String(required=True, allow_none=False, validate=Length(0, 500),) description = fields.String(allow_none=True, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, required=True) @@ -44,7 +44,11 @@ class FilterSetPostSchema(Schema): @post_load def validate( - self, data: Mapping[Any, Any], *, many: Any, partial: Any # pylint: disable=W0613 + self, + data: Mapping[Any, Any], + *, + many: Any, + partial: Any # pylint: disable=W0613 ) -> Dict[str, Any]: 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") diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py index 0442d1f35fead..edfb06945fe11 100644 --- a/tests/dashboards/filter_sets/api_tests.py +++ b/tests/dashboards/filter_sets/api_tests.py @@ -197,6 +197,7 @@ def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet: def not_exists_user_id() -> int: return 99999 + @pytest.mark.ofek class TestFilterSetsApi: class TestCreate: From 94abd2382d8bfc6b49cce54ebb5372799412fd00 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 13:44:06 +0300 Subject: [PATCH 12/60] Fix pylint and migrations issues --- superset/dashboards/filter_sets/api.py | 3 ++- superset/dashboards/filter_sets/schemas.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 6db93cb62f880..af755b4d93696 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -105,9 +105,10 @@ def __init__(self) -> None: super().__init__() def _init_properties(self) -> None: + # pylint: disable=E1003 super( BaseSupersetModelRestApi, self - )._init_properties() # pylint: disable=E1003 + )._init_properties() @expose("//filtersets", methods=["GET"]) @protect() diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index 0203fb5873e96..a9334e2ef4b5c 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -33,6 +33,7 @@ class JsonMetadataSchema(Schema): class FilterSetPostSchema(Schema): + # pylint: disable=W0613 name = fields.String(required=True, allow_none=False, validate=Length(0, 500),) description = fields.String(allow_none=True, validate=[Length(1, 1000)]) json_metadata = fields.Nested(JsonMetadataSchema, required=True) @@ -48,7 +49,7 @@ def validate( data: Mapping[Any, Any], *, many: Any, - partial: Any # pylint: disable=W0613 + partial: Any ) -> Dict[str, Any]: 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") From 4037097c39d21bd821a3b2d3aae03225fad32dc6 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 13:50:43 +0300 Subject: [PATCH 13/60] Fix pylint and migrations issues --- superset/dashboards/filter_sets/api.py | 4 +--- superset/dashboards/filter_sets/schemas.py | 6 +----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index af755b4d93696..14574f13989b0 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -106,9 +106,7 @@ def __init__(self) -> None: def _init_properties(self) -> None: # pylint: disable=E1003 - super( - BaseSupersetModelRestApi, self - )._init_properties() + super(BaseSupersetModelRestApi, self)._init_properties() @expose("//filtersets", methods=["GET"]) @protect() diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index a9334e2ef4b5c..7c69edd99749e 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -45,11 +45,7 @@ class FilterSetPostSchema(Schema): @post_load def validate( - self, - data: Mapping[Any, Any], - *, - many: Any, - partial: Any + self, data: Mapping[Any, Any], *, many: Any, partial: Any ) -> Dict[str, Any]: 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") From 4b3fd593ef7eff9e90e05269ff48f01a225b8253 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 19:32:15 +0300 Subject: [PATCH 14/60] Fix pylint and migrations issues --- tests/dashboards/filter_sets/api_tests.py | 147 +++++++++++++++++++++- tests/superset_test_config.py | 4 +- 2 files changed, 143 insertions(+), 8 deletions(-) diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py index edfb06945fe11..32b41767578d8 100644 --- a/tests/dashboards/filter_sets/api_tests.py +++ b/tests/dashboards/filter_sets/api_tests.py @@ -20,7 +20,7 @@ import pytest from flask import Response - +from functools import reduce from superset import security_manager as sm from superset.dashboards.filter_sets.consts import ( DASHBOARD_OWNER_TYPE, @@ -41,6 +41,7 @@ create_slice, ) from tests.test_app import app +import json if TYPE_CHECKING: from flask.testing import FlaskClient @@ -50,11 +51,12 @@ security_manager: BaseSecurityManager = sm -CREATE_FILTER_SET_URI = "api/v1/dashboard/{dashboard_id}/filtersets" +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" @pytest.fixture(autouse=True, scope="module") @@ -63,6 +65,7 @@ def test_users() -> Generator[Dict[str, int], None, None]: ADMIN_USERNAME_FOR_TEST, DASHBOARD_OWNER_USERNAME, FILTER_SET_OWNER_USERNAME, + REGULAR_USER ] with app.app_context(): filter_set_role: Role = security_manager.add_role("filter_set_role") @@ -100,10 +103,17 @@ def test_users() -> Generator[Dict[str, int], None, None]: def call_create_filter_set( client: FlaskClient[Any], dashboard_id: int, data: Dict[str, Any] ) -> Response: - uri = CREATE_FILTER_SET_URI.format(dashboard_id=dashboard_id) + 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) + + @pytest.fixture def client() -> Generator[FlaskClient[Any], None, None]: with app.test_client() as client: @@ -154,6 +164,47 @@ def dashboard_id() -> Generator[int, None, None]: session.commit() +@pytest.fixture +def filtersets(dashboard_id: int, test_users: Dict[str, int], + valid_json_metadata: Dict[str, Any]) -> Generator[ + Dict[str, List[int]], None, None]: + try: + with app.app_context() as ctx: + session = ctx.app.appbuilder.get_session + first_filter_set = FilterSet(name="filter_set_1_of_" + str(dashboard_id), + dashboard_id=dashboard_id, + json_metadata=json.dumps(valid_json_metadata), + owner_id=dashboard_id, + owner_type='Dashboard') + second_filter_set = FilterSet(name="filter_set_2_of_" + str(dashboard_id), + json_metadata=json.dumps(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=json.dumps(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=json.dumps(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.id, second_filter_set.id], + FILTER_SET_OWNER_USERNAME: [third_filter_set.id, forth_filter_set.id] + } + yield yv + except Exception as ex: + print(str(ex)) + + @pytest.fixture def valid_json_metadata() -> Dict[Any, Any]: return {"nativeFilters": {}} @@ -198,7 +249,6 @@ def not_exists_user_id() -> int: return 99999 -@pytest.mark.ofek class TestFilterSetsApi: class TestCreate: def test_with_extra_field__400( @@ -766,8 +816,93 @@ def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403( assert response.status_code == 403 assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - class TestGetFilterSets: - pass + + class TestGet: + 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[int]], + client: FlaskClient[Any]): + # arrange + login(client, "admin") + expected_ids = reduce(lambda a, b: a.union(b), filtersets.values(), 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 + + @pytest.mark.ofek + def test_when_caller_dashboard_owner__200(self, dashboard_id: int, + filtersets: Dict[str, List[int]], + client: FlaskClient[Any]): + # arrange + login(client, DASHBOARD_OWNER_USERNAME) + expected_ids = set(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 + + @pytest.mark.ofek + def test_when_caller_filterset_owner__200(self, dashboard_id: int, + filtersets: Dict[str, List[int]], + client: FlaskClient[Any]): + # arrange + login(client, FILTER_SET_OWNER_USERNAME) + expected_ids = set(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 + + @pytest.mark.ofek + 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() + + # 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 class TestUpdateFilterSet: pass diff --git a/tests/superset_test_config.py b/tests/superset_test_config.py index 0ce8909a817ec..24d0218fc5ead 100644 --- a/tests/superset_test_config.py +++ b/tests/superset_test_config.py @@ -22,9 +22,9 @@ AUTH_USER_REGISTRATION_ROLE = "alpha" SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(DATA_DIR, "unittests.db") -DEBUG = False +DEBUG = True 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: From 733a537bda8697030b7f34618c5d10b0e2b8b8fb Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 19:43:44 +0300 Subject: [PATCH 15/60] Fix pylint and migrations issues --- superset/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/superset/app.py b/superset/app.py index 220ddea47a3a3..416e391efba7a 100644 --- a/superset/app.py +++ b/superset/app.py @@ -525,9 +525,8 @@ def init_views(self) -> None: icon="fa-cog", ) appbuilder.add_separator("Data") - if feature_flag_manager.is_feature_enabled("DASHBOARD_NATIVE_FILTERS_SET"): + if True or feature_flag_manager.is_feature_enabled("DASHBOARD_NATIVE_FILTERS_SET"): from superset.dashboards.filter_sets.api import FilterSetRestApi - appbuilder.add_api(FilterSetRestApi) def init_app_in_ctx(self) -> None: From eac85266312cec2a97364d053b73e5d7b7e8f9b9 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 20:03:46 +0300 Subject: [PATCH 16/60] Fix pylint and migrations issues --- superset/app.py | 7 +- tests/dashboards/filter_sets/api_tests.py | 138 ++++++++++++---------- 2 files changed, 82 insertions(+), 63 deletions(-) diff --git a/superset/app.py b/superset/app.py index 416e391efba7a..75e23911802e0 100644 --- a/superset/app.py +++ b/superset/app.py @@ -525,9 +525,10 @@ def init_views(self) -> None: icon="fa-cog", ) appbuilder.add_separator("Data") - if True or feature_flag_manager.is_feature_enabled("DASHBOARD_NATIVE_FILTERS_SET"): - from superset.dashboards.filter_sets.api import FilterSetRestApi - appbuilder.add_api(FilterSetRestApi) + + from superset.dashboards.filter_sets.api import FilterSetRestApi + + appbuilder.add_api(FilterSetRestApi) def init_app_in_ctx(self) -> None: """ diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py index 32b41767578d8..a22c15a8901f1 100644 --- a/tests/dashboards/filter_sets/api_tests.py +++ b/tests/dashboards/filter_sets/api_tests.py @@ -16,11 +16,13 @@ # under the License. from __future__ import annotations -from typing import Any, Dict, Generator, List, TYPE_CHECKING, Union +import json +from functools import reduce +from typing import Any, Dict, Generator, List, Set, TYPE_CHECKING, Union import pytest from flask import Response -from functools import reduce + from superset import security_manager as sm from superset.dashboards.filter_sets.consts import ( DASHBOARD_OWNER_TYPE, @@ -41,7 +43,6 @@ create_slice, ) from tests.test_app import app -import json if TYPE_CHECKING: from flask.testing import FlaskClient @@ -65,7 +66,7 @@ def test_users() -> Generator[Dict[str, int], None, None]: ADMIN_USERNAME_FOR_TEST, DASHBOARD_OWNER_USERNAME, FILTER_SET_OWNER_USERNAME, - REGULAR_USER + REGULAR_USER, ] with app.app_context(): filter_set_role: Role = security_manager.add_role("filter_set_role") @@ -107,9 +108,7 @@ def call_create_filter_set( return client.post(uri, json=data) -def call_get_filter_sets( - client: FlaskClient[Any], dashboard_id: int -) -> Response: +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) @@ -165,41 +164,49 @@ def dashboard_id() -> Generator[int, None, None]: @pytest.fixture -def filtersets(dashboard_id: int, test_users: Dict[str, int], - valid_json_metadata: Dict[str, Any]) -> Generator[ - Dict[str, List[int]], None, None]: +def filtersets( + dashboard_id: int, test_users: Dict[str, int], valid_json_metadata: Dict[str, Any] +) -> Generator[Dict[str, List[int]], None, None]: try: with app.app_context() as ctx: session = ctx.app.appbuilder.get_session - first_filter_set = FilterSet(name="filter_set_1_of_" + str(dashboard_id), - dashboard_id=dashboard_id, - json_metadata=json.dumps(valid_json_metadata), - owner_id=dashboard_id, - owner_type='Dashboard') - second_filter_set = FilterSet(name="filter_set_2_of_" + str(dashboard_id), - json_metadata=json.dumps(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=json.dumps(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=json.dumps(valid_json_metadata), - dashboard_id=dashboard_id, - owner_id=test_users[FILTER_SET_OWNER_USERNAME], - owner_type='User') + first_filter_set = FilterSet( + name="filter_set_1_of_" + str(dashboard_id), + dashboard_id=dashboard_id, + json_metadata=json.dumps(valid_json_metadata), + owner_id=dashboard_id, + owner_type="Dashboard", + ) + second_filter_set = FilterSet( + name="filter_set_2_of_" + str(dashboard_id), + json_metadata=json.dumps(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=json.dumps(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=json.dumps(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.id, second_filter_set.id], - FILTER_SET_OWNER_USERNAME: [third_filter_set.id, forth_filter_set.id] - } + "Dashboard": [first_filter_set.id, second_filter_set.id], + FILTER_SET_OWNER_USERNAME: [third_filter_set.id, forth_filter_set.id], + } yield yv except Exception as ex: print(str(ex)) @@ -816,12 +823,9 @@ def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403( assert response.status_code == 403 assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - class TestGet: def test_with_dashboard_not_exists__404( - self, - not_exists_dashboard: int, - client: FlaskClient[Any], + self, not_exists_dashboard: int, client: FlaskClient[Any], ): # arrange login(client, "admin") @@ -832,8 +836,9 @@ def test_with_dashboard_not_exists__404( # assert assert response.status_code == 404 - def test_dashboards_without_filtersets__200(self, dashboard_id: int, - client: FlaskClient[Any]): + def test_dashboards_without_filtersets__200( + self, dashboard_id: int, client: FlaskClient[Any] + ): # arrange login(client, "admin") @@ -842,42 +847,52 @@ def test_dashboards_without_filtersets__200(self, dashboard_id: int, # assert assert response.status_code == 200 - assert response.is_json and response.json['count'] == 0 + assert response.is_json and response.json["count"] == 0 - - def test_when_caller_admin__200(self, dashboard_id: int, - filtersets: Dict[str, List[int]], - client: FlaskClient[Any]): + def test_when_caller_admin__200( + self, + dashboard_id: int, + filtersets: Dict[str, List[int]], + client: FlaskClient[Any], + ): # arrange login(client, "admin") - expected_ids = reduce(lambda a, b: a.union(b), filtersets.values(), set()) + expected_ids: Set[int] = reduce( + lambda a, b: a.union(b), filtersets.values(), 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 + assert response.is_json and set(response.json["ids"]) == expected_ids @pytest.mark.ofek - def test_when_caller_dashboard_owner__200(self, dashboard_id: int, - filtersets: Dict[str, List[int]], - client: FlaskClient[Any]): + def test_when_caller_dashboard_owner__200( + self, + dashboard_id: int, + filtersets: Dict[str, List[int]], + client: FlaskClient[Any], + ): # arrange login(client, DASHBOARD_OWNER_USERNAME) - expected_ids = set(filtersets['Dashboard']) + expected_ids = set(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 + assert response.is_json and set(response.json["ids"]) == expected_ids @pytest.mark.ofek - def test_when_caller_filterset_owner__200(self, dashboard_id: int, - filtersets: Dict[str, List[int]], - client: FlaskClient[Any]): + def test_when_caller_filterset_owner__200( + self, + dashboard_id: int, + filtersets: Dict[str, List[int]], + client: FlaskClient[Any], + ): # arrange login(client, FILTER_SET_OWNER_USERNAME) expected_ids = set(filtersets[FILTER_SET_OWNER_USERNAME]) @@ -887,22 +902,25 @@ def test_when_caller_filterset_owner__200(self, dashboard_id: int, # assert assert response.status_code == 200 - assert response.is_json and set(response.json['ids']) == expected_ids + assert response.is_json and set(response.json["ids"]) == expected_ids @pytest.mark.ofek - def test_when_caller_regular_user__200(self, dashboard_id: int, - filtersets: Dict[str, List[int]], - client: FlaskClient[Any]): + 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() + 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 + assert response.is_json and set(response.json["ids"]) == expected_ids class TestUpdateFilterSet: pass From 89e8bee9d6a26f8eae795fa9aeca90b18047af4a Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 20:18:06 +0300 Subject: [PATCH 17/60] Fix pylint and migrations issues --- superset/dashboards/filter_sets/api.py | 145 +++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 14574f13989b0..53d460d78a373 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -123,6 +123,34 @@ def _init_properties(self) -> None: @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, **kwargs: Any) -> Response: + """Gets a dashboard's Filter-sets + --- + get: + description: >- + Get a dashboard's Filter-sets + parameters: + - in: path + dashboard_id: id + description: Either the id of the dashboard + responses: + 200: + description: Filtersets[] + content: + application/json: + schema: + type: object + properties: + result: + $ref: '#/components/schemas/DashboardGetResponseSchema' + 302: + description: Redirects to the current digest + 400: + $ref: '#/components/responses/400' + 401: + $ref: '#/components/responses/401' + 404: + $ref: '#/components/responses/404' + """ dashboard_id: Optional[int] = kwargs.get("dashboard_id", None) if not DashboardDAO.find_by_id(cast(int, dashboard_id)): return self.response(404, message="dashboard '%s' not found" % dashboard_id) @@ -142,6 +170,45 @@ def get_list(self, **kwargs: Any) -> Response: log_to_statsd=False, ) def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 + """Creates a new Dashboard's FilterSet + --- + post: + description: >- + Create a new Dashboard's Filterset. + requestBody: + description: Filterset schema + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/{{self.__class__.__name__}}.post' + parameters: + - in: path + dashboard_id: id + description: Either the id of the dashboard + responses: + 201: + description: Dashboard's FilterSet 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: @@ -166,6 +233,51 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 log_to_statsd=False, ) def put(self, dashboard_id: int, pk: int) -> Response: + """Changes a Dashboard's Filterset + --- + put: + description: >- + Changes a Dashboard's Filterset. + parameters: + - in: path + dashboard_id: id + description: Either the id of the dashboard + - in: path + schema: + type: integer + name: pk + requestBody: + description: Dashboard schema + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/{{self.__class__.__name__}}.put' + responses: + 200: + description: Dashboard 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: @@ -191,6 +303,39 @@ def put(self, dashboard_id: int, pk: int) -> Response: log_to_statsd=False, ) def delete(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 + """Deletes a Dashboard's FilterSet + --- + delete: + description: >- + Deletes a Dashboard's FilterSet. + parameters: + - in: path + name: dashboard_id + - in: path + schema: + type: integer + name: pk + responses: + 200: + description: Dashboard 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) From 3a7f080ee55263cdd729ca3f0f5ff2f82b8d2b03 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Sun, 18 Apr 2021 20:33:35 +0300 Subject: [PATCH 18/60] Fix pylint and migrations issues --- superset/dashboards/filter_sets/api.py | 143 +------------------------ 1 file changed, 3 insertions(+), 140 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 53d460d78a373..385ec722262c1 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -124,32 +124,6 @@ def _init_properties(self) -> None: @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, **kwargs: Any) -> Response: """Gets a dashboard's Filter-sets - --- - get: - description: >- - Get a dashboard's Filter-sets - parameters: - - in: path - dashboard_id: id - description: Either the id of the dashboard - responses: - 200: - description: Filtersets[] - content: - application/json: - schema: - type: object - properties: - result: - $ref: '#/components/schemas/DashboardGetResponseSchema' - 302: - description: Redirects to the current digest - 400: - $ref: '#/components/responses/400' - 401: - $ref: '#/components/responses/401' - 404: - $ref: '#/components/responses/404' """ dashboard_id: Optional[int] = kwargs.get("dashboard_id", None) if not DashboardDAO.find_by_id(cast(int, dashboard_id)): @@ -171,44 +145,7 @@ def get_list(self, **kwargs: Any) -> Response: ) def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 """Creates a new Dashboard's FilterSet - --- - post: - description: >- - Create a new Dashboard's Filterset. - requestBody: - description: Filterset schema - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/{{self.__class__.__name__}}.post' - parameters: - - in: path - dashboard_id: id - description: Either the id of the dashboard - responses: - 201: - description: Dashboard's FilterSet 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: @@ -234,50 +171,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 ) def put(self, dashboard_id: int, pk: int) -> Response: """Changes a Dashboard's Filterset - --- - put: - description: >- - Changes a Dashboard's Filterset. - parameters: - - in: path - dashboard_id: id - description: Either the id of the dashboard - - in: path - schema: - type: integer - name: pk - requestBody: - description: Dashboard schema - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/{{self.__class__.__name__}}.put' - responses: - 200: - description: Dashboard 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: @@ -304,38 +198,7 @@ def put(self, dashboard_id: int, pk: int) -> Response: ) def delete(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 """Deletes a Dashboard's FilterSet - --- - delete: - description: >- - Deletes a Dashboard's FilterSet. - parameters: - - in: path - name: dashboard_id - - in: path - schema: - type: integer - name: pk - responses: - 200: - description: Dashboard 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) From 8c367cce5585ddebf2ea58c8d18e0ecf09ce3b99 Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Tue, 27 Apr 2021 16:32:23 +0300 Subject: [PATCH 19/60] add tests and fixes based of failures --- superset/common/not_authrized_object.py | 23 + superset/dashboards/filter_sets/api.py | 19 +- .../dashboards/filter_sets/commands/base.py | 56 +- .../dashboards/filter_sets/commands/create.py | 34 +- .../dashboards/filter_sets/commands/delete.py | 5 +- .../dashboards/filter_sets/commands/update.py | 8 +- superset/dashboards/filter_sets/schemas.py | 14 +- .../versions/3ebe0993c770_filterset_table.py | 2 +- superset/models/dashboard.py | 8 +- superset/models/filter_set.py | 18 +- tests/dashboards/filter_sets/api_tests.py | 929 ------------------ tests/dashboards/filter_sets/conftest.py | 310 ++++++ tests/dashboards/filter_sets/consts.py | 22 + .../filter_sets/create_api_tests.py | 630 ++++++++++++ .../filter_sets/delete_api_tests.py | 209 ++++ tests/dashboards/filter_sets/get_api_tests.py | 126 +++ .../filter_sets/update_api_tests.py | 508 ++++++++++ tests/dashboards/filter_sets/utils.py | 102 ++ tests/dashboards/superset_factory_util.py | 25 +- 19 files changed, 2048 insertions(+), 1000 deletions(-) create mode 100644 superset/common/not_authrized_object.py delete mode 100644 tests/dashboards/filter_sets/api_tests.py create mode 100644 tests/dashboards/filter_sets/conftest.py create mode 100644 tests/dashboards/filter_sets/consts.py create mode 100644 tests/dashboards/filter_sets/create_api_tests.py create mode 100644 tests/dashboards/filter_sets/delete_api_tests.py create mode 100644 tests/dashboards/filter_sets/get_api_tests.py create mode 100644 tests/dashboards/filter_sets/update_api_tests.py create mode 100644 tests/dashboards/filter_sets/utils.py diff --git a/superset/common/not_authrized_object.py b/superset/common/not_authrized_object.py new file mode 100644 index 0000000000000..aad78b1377a3e --- /dev/null +++ b/superset/common/not_authrized_object.py @@ -0,0 +1,23 @@ +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 + ) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 385ec722262c1..22e14f5441299 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -45,6 +45,7 @@ FilterSetCreateFailedError, FilterSetDeleteFailedError, FilterSetForbiddenError, + FilterSetNotFoundError, FilterSetUpdateFailedError, UserIsNotDashboardOwnerError, ) @@ -123,7 +124,8 @@ def _init_properties(self) -> None: @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, **kwargs: Any) -> Response: - """Gets a dashboard's Filter-sets + """ + Gets a dashboard's Filter-sets """ dashboard_id: Optional[int] = kwargs.get("dashboard_id", None) if not DashboardDAO.find_by_id(cast(int, dashboard_id)): @@ -144,7 +146,8 @@ def get_list(self, **kwargs: Any) -> Response: log_to_statsd=False, ) def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 - """Creates a new Dashboard's FilterSet + """ + Creates a new Dashboard's FilterSet """ if not request.is_json: return self.response_400(message="Request is not JSON") @@ -161,7 +164,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 except DashboardNotFoundError: return self.response_404() - @expose("//filtersets/", methods=["PUT"]) + @expose("//filtersets/", methods=["PUT"]) @protect() @safe @statsd_metrics @@ -170,7 +173,8 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 log_to_statsd=False, ) def put(self, dashboard_id: int, pk: int) -> Response: - """Changes a Dashboard's Filterset + """ + Changes a Dashboard's Filterset """ if not request.is_json: return self.response_400(message="Request is not JSON") @@ -188,7 +192,7 @@ def put(self, dashboard_id: int, pk: int) -> Response: logger.error(err) return self.response(err.status) - @expose("//filtersets/", methods=["DELETE"]) + @expose("//filtersets/", methods=["DELETE"]) @protect() @safe @statsd_metrics @@ -197,13 +201,16 @@ def put(self, dashboard_id: int, pk: int) -> Response: log_to_statsd=False, ) def delete(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 - """Deletes a Dashboard's FilterSet + """ + Deletes a Dashboard's FilterSet """ 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, diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index e3f686273b466..fe2e0c10f508e 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -15,12 +15,13 @@ # specific language governing permissions and limitations # under the License. import logging -from typing import Optional +from typing import cast, Optional from flask_appbuilder.models.sqla import Model from flask_appbuilder.security.sqla.models import User from superset.commands.base import BaseCommand +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 @@ -36,46 +37,61 @@ class BaseFilterSetCommand(BaseCommand): + # 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(self) -> None: + self._validate_filterset_dashboard_exists() + + 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 is_user_admin() or self._dashboard.am_i_owner() + return self._is_actor_admin or self._dashboard.am_i_owner() def validate_exist_filter_use_cases_set(self) -> None: # pylint: disable=C0103 - if self._filter_set_id: - self._filter_set = self._dashboard.filter_sets.get( - self._filter_set_id, None - ) - if not self._filter_set: - raise FilterSetNotFoundError(str(self._filter_set_id)) - self.check_ownership() + 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: - if ( - self._filter_set is not None - and self._filter_set.owner_type == USER_OWNER_TYPE - ): - if self._actor.id != self._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(): + 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), - "The user is not an owner of the filter_set's dashboard", + "user not authorized to access the filterset", + err, ) + except FilterSetForbiddenError as err: + raise err diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index deaaa7b1fa635..4513403390ed2 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -40,6 +40,7 @@ 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() @@ -53,16 +54,25 @@ def run(self) -> Model: def validate(self) -> None: super().validate() if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE: - if ( - self._properties.get(OWNER_ID_FIELD, self._dashboard_id) - != self._dashboard_id - ): - raise DashboardIdInconsistencyError(str(self._dashboard_id)) - if not self.is_user_dashboard_owner(): - raise UserIsNotDashboardOwnerError(str(self._dashboard_id)) + self._validate_owner_id_is_dashboard_id() + self._validate_user_is_the_dashboard_owner() else: - 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" - ) + 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)) diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py index f98705e29b0f4..ae10916bd875a 100644 --- a/superset/dashboards/filter_sets/commands/delete.py +++ b/superset/dashboards/filter_sets/commands/delete.py @@ -49,9 +49,8 @@ def validate(self) -> None: self.validate_exist_filter_use_cases_set() except FilterSetNotFoundError as err: if FilterSetDAO.find_by_id(self._filter_set_id): # type: ignore - FilterSetForbiddenError( + raise FilterSetForbiddenError( 'the filter-set does not related to dashboard "%s"' % str(self._dashboard_id) ) - else: - raise err + raise err diff --git a/superset/dashboards/filter_sets/commands/update.py b/superset/dashboards/filter_sets/commands/update.py index 45df2511f229e..6b20adbb9d45e 100644 --- a/superset/dashboards/filter_sets/commands/update.py +++ b/superset/dashboards/filter_sets/commands/update.py @@ -25,7 +25,7 @@ from superset.dashboards.filter_sets.commands.exceptions import ( FilterSetUpdateFailedError, ) -from superset.dashboards.filter_sets.consts import DASHBOARD_ID_FIELD +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__) @@ -42,7 +42,11 @@ def __init__( def run(self) -> Model: try: self.validate() - self._properties[DASHBOARD_ID_FIELD] = self._dashboard_id + 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), "", err) diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index 7c69edd99749e..ee58963bbb765 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -35,7 +35,9 @@ class JsonMetadataSchema(Schema): class FilterSetPostSchema(Schema): # pylint: disable=W0613 name = fields.String(required=True, allow_none=False, validate=Length(0, 500),) - description = fields.String(allow_none=True, validate=[Length(1, 1000)]) + description = fields.String( + required=False, allow_none=True, validate=[Length(1, 1000)] + ) json_metadata = fields.Nested(JsonMetadataSchema, required=True) owner_type = fields.String( @@ -53,11 +55,13 @@ def validate( class FilterSetPutSchema(Schema): - name = fields.String(allow_none=False, validate=Length(0, 500)) - description = fields.String(allow_none=False, validate=[Length(1, 1000)]) - json_metadata = fields.Nested(JsonMetadataSchema, allow_none=False) + 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.Nested(JsonMetadataSchema, required=False, allow_none=False) owner_type = fields.String( - allow_none=False, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE]) + allow_none=False, required=False, validate=OneOf([DASHBOARD_OWNER_TYPE]) ) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index ea400c8ffaffc..bcc49e194fa83 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -38,7 +38,7 @@ def upgrade(): 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("json_metadata", sa.JSON(), nullable=False), sa.Column("owner_id", sa.Integer(), nullable=False), sa.Column("owner_type", sa.VARCHAR(255), nullable=False), sa.Column( diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index 2a51a1bfc4108..e4c5a0271a6c6 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -152,7 +152,9 @@ class Dashboard( # pylint: disable=too-many-instance-attributes 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") + _filter_sets = relationship( + "FilterSet", back_populates="dashboard", cascade="all, delete" + ) export_fields = [ "dashboard_title", "position_json", @@ -167,6 +169,10 @@ def __repr__(self) -> str: @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 diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index a98f69973c74d..867e5ef3d380b 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -16,12 +16,11 @@ # 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 import Column, ForeignKey, Integer, JSON, MetaData, String, Text from sqlalchemy.orm import relationship from sqlalchemy_utils import generic_relationship @@ -42,7 +41,7 @@ class FilterSet( # pylint: disable=too-many-instance-attributes 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) + json_metadata = Column(JSON, nullable=False) dashboard_id = Column(Integer, ForeignKey("dashboards.id")) dashboard = relationship("Dashboard", back_populates="_filter_sets") owner_id = Column(Integer, nullable=False) @@ -74,16 +73,15 @@ def changed_by_url(self) -> str: return "" return f"/superset/profile/{self.changed_by.username}" - @property - def data(self) -> Dict[str, Any]: - json_metadata = self.json_metadata - if json_metadata: - json_metadata = json.loads(json_metadata) + def to_dict(self) -> Dict[str, Any]: return { "id": self.id, - "metadata": json_metadata, "name": self.name, - "last_modified_time": self.changed_on.replace(microsecond=0).timestamp(), + "description": self.description, + "json_metadata": self.json_metadata, + "dashboard_id": self.dashboard_id, + "owner_type": self.owner_type, + "owner_id": self.owner_id, } @classmethod diff --git a/tests/dashboards/filter_sets/api_tests.py b/tests/dashboards/filter_sets/api_tests.py deleted file mode 100644 index a22c15a8901f1..0000000000000 --- a/tests/dashboards/filter_sets/api_tests.py +++ /dev/null @@ -1,929 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -from __future__ import annotations - -import json -from functools import reduce -from typing import Any, Dict, Generator, List, Set, TYPE_CHECKING, Union - -import pytest -from flask import Response - -from superset import security_manager as sm -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 superset.models.dashboard import Dashboard -from superset.models.filter_set import FilterSet -from tests.base_tests import login -from tests.dashboards.superset_factory_util import ( - create_dashboard, - create_database, - create_datasource_table, - create_slice, -) -from tests.test_app import app - -if TYPE_CHECKING: - from flask.testing import FlaskClient - from flask_appbuilder.security.sqla.models import Role, User - from flask_appbuilder.security.manager import BaseSecurityManager - from superset.models.dashboard import Dashboard - -security_manager: BaseSecurityManager = sm - -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" - - -@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: Role = security_manager.add_role("filter_set_role") - filterset_view_name = security_manager.find_view_menu("FilterSets") - all_datasource_view_name = security_manager.find_view_menu( - "all_datasource_access" - ) - pvms = 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) - users: List[User] = [] - admin_role = security_manager.find_role("Admin") - - for username in usernames: - roles_to_add = ( - [admin_role] - if username == ADMIN_USERNAME_FOR_TEST - else [filter_set_role] - ) - user = security_manager.add_user( - username, "test", "test", username, roles_to_add, password="general" - ) - users.append(user) - usernames_to_ids: Dict[str, int] = {user.username: user.id for user in users} - yield usernames_to_ids - with app.app_context() as ctx: - session = ctx.app.appbuilder.get_session - for username in usernames_to_ids.keys(): - session.delete(security_manager.find_user(username)) - session.commit() - - -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) - - -@pytest.fixture -def client() -> Generator[FlaskClient[Any], None, None]: - with app.test_client() as client: - yield client - - -@pytest.fixture -def dashboard_id() -> Generator[int, None, None]: - dashboard_id = None - dashboard = None - slice = (None,) - datasource = None - database = None - try: - with app.app_context() as ctx: - dashboard_owner_user = security_manager.find_user(DASHBOARD_OWNER_USERNAME) - database = create_database("test_database") - 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() - dashboard_id = dashboard.id - yield dashboard_id - except Exception as ex: - print(str(ex)) - finally: - with app.app_context() as ctx: - session = ctx.app.appbuilder.get_session - if dashboard_id is not None: - dashboard = Dashboard.get(str(dashboard_id)) - for fs in dashboard._filter_sets: - session.delete(fs) - session.delete(dashboard) - session.delete(slice) - session.delete(datasource) - session.delete(database) - session.commit() - - -@pytest.fixture -def filtersets( - dashboard_id: int, test_users: Dict[str, int], valid_json_metadata: Dict[str, Any] -) -> Generator[Dict[str, List[int]], None, None]: - try: - with app.app_context() as ctx: - session = ctx.app.appbuilder.get_session - first_filter_set = FilterSet( - name="filter_set_1_of_" + str(dashboard_id), - dashboard_id=dashboard_id, - json_metadata=json.dumps(valid_json_metadata), - owner_id=dashboard_id, - owner_type="Dashboard", - ) - second_filter_set = FilterSet( - name="filter_set_2_of_" + str(dashboard_id), - json_metadata=json.dumps(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=json.dumps(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=json.dumps(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.id, second_filter_set.id], - FILTER_SET_OWNER_USERNAME: [third_filter_set.id, forth_filter_set.id], - } - yield yv - except Exception as ex: - print(str(ex)) - - -@pytest.fixture -def valid_json_metadata() -> Dict[Any, Any]: - return {"nativeFilters": {}} - - -@pytest.fixture -def exists_user_id() -> int: - return 1 - - -@pytest.fixture -def valid_filter_set_data( - dashboard_id: int, valid_json_metadata: Dict[Any, Any], 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: valid_json_metadata, - OWNER_TYPE_FIELD: USER_OWNER_TYPE, - OWNER_ID_FIELD: exists_user_id, - } - - -@pytest.fixture -def not_exists_dashboard(dashboard_id: int) -> int: - return dashboard_id + 1 - - -def get_filter_set_by_name(name: str) -> FilterSet: - with app.app_context(): - return FilterSet.get_by_name(name) - - -def get_filter_set_by_dashboard_id(dashboard_id: int) -> FilterSet: - with app.app_context(): - return FilterSet.get_by_dashboard_id(dashboard_id) - - -@pytest.fixture -def not_exists_user_id() -> int: - return 99999 - - -class TestFilterSetsApi: - class TestCreate: - def test_with_extra_field__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data["extra"] = "val" - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert response.json["message"]["extra"][0] == "Unknown field." - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_with_id_field__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data["id"] = 1 - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert response.json["message"]["id"][0] == "Unknown field." - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_with_dashboard_not_exists__404( - self, - not_exists_dashboard: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # act - login(client, "admin") - response = call_create_filter_set( - client, not_exists_dashboard, valid_filter_set_data - ) - - # assert - assert response.status_code == 404 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_without_name__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data.pop(NAME_FIELD, None) - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # 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: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[NAME_FIELD] = None - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_with_int_as_name__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[NAME_FIELD] = 4 - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_without_description__201( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data.pop(DESCRIPTION_FIELD, None) - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_with_none_description__201( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[DESCRIPTION_FIELD] = None - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_with_int_as_description__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[DESCRIPTION_FIELD] = 1 - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_without_json_metadata__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data.pop(JSON_METADATA_FIELD, None) - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_with_invalid_json_metadata__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[DESCRIPTION_FIELD] = {} - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_without_owner_type__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data.pop(OWNER_TYPE_FIELD, None) - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_with_invalid_owner_type__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = "OTHER_TYPE" - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_without_owner_id_when_owner_type_is_user__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data.pop(OWNER_ID_FIELD, None) - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_without_owner_id_when_owner_type_is_dashboard__201( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE - valid_filter_set_data.pop(OWNER_ID_FIELD, None) - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_with_not_exists_owner__400( - self, - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - not_exists_user_id: int, - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = not_exists_user_id - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 400 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - def test_when_caller_is_admin_and_owner_is_admin__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_admin_and_owner_is_dashboard_owner__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_admin_and_owner_is_regular_user__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[ - FILTER_SET_OWNER_USERNAME - ] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_admin_and_owner_type_is_dashboard__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_dashboard_owner_and_owner_is_admin__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, DASHBOARD_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_dashboard_owner_and_owner_is_dashboard_owner__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, DASHBOARD_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_dashboard_owner_and_owner_is_regular_user__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, DASHBOARD_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[ - FILTER_SET_OWNER_USERNAME - ] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_dashboard_owner_and_owner_type_is_dashboard__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, DASHBOARD_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_regular_user_and_owner_is_admin__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, FILTER_SET_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[ADMIN_USERNAME_FOR_TEST] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_regular_user_and_owner_is_dashboard_owner__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, FILTER_SET_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[DASHBOARD_OWNER_USERNAME] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_regular_user_and_owner_is_regular_user__201( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, FILTER_SET_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = USER_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = test_users[ - FILTER_SET_OWNER_USERNAME - ] - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 201 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is not None - - def test_when_caller_is_regular_user_and_owner_type_is_dashboard__403( - self, - test_users: Dict[str, int], - dashboard_id: int, - valid_filter_set_data: Dict[str, Any], - client: FlaskClient[Any], - ): - # arrange - login(client, FILTER_SET_OWNER_USERNAME) - valid_filter_set_data[OWNER_TYPE_FIELD] = DASHBOARD_OWNER_TYPE - valid_filter_set_data[OWNER_ID_FIELD] = dashboard_id - - # act - response = call_create_filter_set( - client, dashboard_id, valid_filter_set_data - ) - - # assert - assert response.status_code == 403 - assert get_filter_set_by_name(valid_filter_set_data["name"]) is None - - class TestGet: - 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[int]], - client: FlaskClient[Any], - ): - # arrange - login(client, "admin") - expected_ids: Set[int] = reduce( - lambda a, b: a.union(b), filtersets.values(), 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 - - @pytest.mark.ofek - def test_when_caller_dashboard_owner__200( - self, - dashboard_id: int, - filtersets: Dict[str, List[int]], - client: FlaskClient[Any], - ): - # arrange - login(client, DASHBOARD_OWNER_USERNAME) - expected_ids = set(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 - - @pytest.mark.ofek - def test_when_caller_filterset_owner__200( - self, - dashboard_id: int, - filtersets: Dict[str, List[int]], - client: FlaskClient[Any], - ): - # arrange - login(client, FILTER_SET_OWNER_USERNAME) - expected_ids = set(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 - - @pytest.mark.ofek - 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 - - class TestUpdateFilterSet: - pass - - class TestDeleteFilterSet: - pass diff --git a/tests/dashboards/filter_sets/conftest.py b/tests/dashboards/filter_sets/conftest.py new file mode 100644 index 0000000000000..0b3883ede866c --- /dev/null +++ b/tests/dashboards/filter_sets/conftest.py @@ -0,0 +1,310 @@ +# 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.dashboards.filter_sets.consts import ( + ADMIN_USERNAME_FOR_TEST, + DASHBOARD_OWNER_USERNAME, + FILTER_SET_OWNER_USERNAME, + REGULAR_USER, +) +from tests.dashboards.superset_factory_util import ( + create_dashboard, + create_database, + create_datasource_table, + create_slice, +) +from 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() -> None: + ctx: AppContext + with app.app_context() as ctx: + ctx.app.appbuilder.get_session.configure(expire_on_commit=False) + + +@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" + ) + 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") + 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], valid_json_metadata: Dict[str, Any] +) -> 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=json.dumps(valid_json_metadata), + owner_id=dashboard_id, + owner_type="Dashboard", + ) + second_filter_set = FilterSet( + name="filter_set_2_of_" + str(dashboard_id), + json_metadata=json.dumps(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=json.dumps(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=json.dumps(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[Any, Any]: + return {"nativeFilters": {}} + + +@pytest.fixture +def exists_user_id() -> int: + return 1 + + +@pytest.fixture +def valid_filter_set_data_for_create( + dashboard_id: int, valid_json_metadata: Dict[Any, Any], 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: 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, valid_json_metadata: Dict[Any, Any], 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: 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() diff --git a/tests/dashboards/filter_sets/consts.py b/tests/dashboards/filter_sets/consts.py new file mode 100644 index 0000000000000..f54f00fea8b75 --- /dev/null +++ b/tests/dashboards/filter_sets/consts.py @@ -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" diff --git a/tests/dashboards/filter_sets/create_api_tests.py b/tests/dashboards/filter_sets/create_api_tests.py new file mode 100644 index 0000000000000..d0817c5f71acf --- /dev/null +++ b/tests/dashboards/filter_sets/create_api_tests.py @@ -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.base_tests import login +from tests.dashboards.filter_sets.consts import ( + ADMIN_USERNAME_FOR_TEST, + DASHBOARD_OWNER_USERNAME, + FILTER_SET_OWNER_USERNAME, +) +from 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(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) diff --git a/tests/dashboards/filter_sets/delete_api_tests.py b/tests/dashboards/filter_sets/delete_api_tests.py new file mode 100644 index 0000000000000..1ba9402f94150 --- /dev/null +++ b/tests/dashboards/filter_sets/delete_api_tests.py @@ -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.base_tests import login +from tests.dashboards.filter_sets.consts import ( + DASHBOARD_OWNER_USERNAME, + FILTER_SET_OWNER_USERNAME, + REGULAR_USER, +) +from 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) diff --git a/tests/dashboards/filter_sets/get_api_tests.py b/tests/dashboards/filter_sets/get_api_tests.py new file mode 100644 index 0000000000000..97164d4ee3d2c --- /dev/null +++ b/tests/dashboards/filter_sets/get_api_tests.py @@ -0,0 +1,126 @@ +# 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.base_tests import login +from tests.dashboards.filter_sets.consts import ( + DASHBOARD_OWNER_USERNAME, + FILTER_SET_OWNER_USERNAME, + REGULAR_USER, +) +from 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 diff --git a/tests/dashboards/filter_sets/update_api_tests.py b/tests/dashboards/filter_sets/update_api_tests.py new file mode 100644 index 0000000000000..5f098f216c79c --- /dev/null +++ b/tests/dashboards/filter_sets/update_api_tests.py @@ -0,0 +1,508 @@ +# 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 superset.dashboards.filter_sets.consts import ( + DESCRIPTION_FIELD, + JSON_METADATA_FIELD, + NAME_FIELD, + OWNER_TYPE_FIELD, +) +from tests.base_tests import login +from tests.dashboards.filter_sets.consts import ( + DASHBOARD_OWNER_USERNAME, + FILTER_SET_OWNER_USERNAME, + REGULAR_USER, +) +from 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, second) -> Dict[Any, Any]: + 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] = 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) diff --git a/tests/dashboards/filter_sets/utils.py b/tests/dashboards/filter_sets/utils.py new file mode 100644 index 0000000000000..df2914009b5ab --- /dev/null +++ b/tests/dashboards/filter_sets/utils.py @@ -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.dashboards.filter_sets.consts import FILTER_SET_URI +from 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)) diff --git a/tests/dashboards/superset_factory_util.py b/tests/dashboards/superset_factory_util.py index bc482963c043b..59044c2c95b48 100644 --- a/tests/dashboards/superset_factory_util.py +++ b/tests/dashboards/superset_factory_util.py @@ -78,10 +78,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, @@ -117,8 +117,8 @@ def create_slice( 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( @@ -129,8 +129,11 @@ def create_slice( ) 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, @@ -156,11 +159,11 @@ def create_datasource_table( database: Optional[Database] = None, owners: Optional[List[User]] = None, ) -> SqlaTable: - 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 [] if database: return SqlaTable(table_name=name, database=database, owners=owners) - db_id = db_id or create_database_to_db(name=name + "_db").id + 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) @@ -172,7 +175,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:") From 4aa43140ba3a6aa84c67f23474223464804cf3fb Mon Sep 17 00:00:00 2001 From: Ofeknielsen Date: Wed, 28 Apr 2021 11:06:24 +0300 Subject: [PATCH 20/60] Fix missing license --- superset/common/not_authrized_object.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/superset/common/not_authrized_object.py b/superset/common/not_authrized_object.py index aad78b1377a3e..7295da9aa7ef1 100644 --- a/superset/common/not_authrized_object.py +++ b/superset/common/not_authrized_object.py @@ -1,3 +1,19 @@ +# 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 From 6d2d23e624df1fd2e1d8962ecdc2778fd4f0319b Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Mon, 3 May 2021 14:51:46 +0300 Subject: [PATCH 21/60] fix down revision --- superset/migrations/versions/3ebe0993c770_filterset_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index bcc49e194fa83..20c142d0074f6 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -17,14 +17,14 @@ """empty message Revision ID: 3ebe0993c770 -Revises: 67da9ef1ef9c +Revises: d416d0d715cc Create Date: 2021-03-29 11:15:48.831225 """ # revision identifiers, used by Alembic. revision = "3ebe0993c770" -down_revision = "19e978e1b9c3" +down_revision = "d416d0d715cc" import sqlalchemy as sa from alembic import op From ff6ca29929d8f125007f418c0a80075059f45682 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 25 May 2021 13:53:53 +0300 Subject: [PATCH 22/60] update down_revision --- .../fc3a3a8ff221_migrate_filter_sets_to_new_format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py b/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py index 79b032894f058..8b1edccdb1ec9 100644 --- a/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py +++ b/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py @@ -17,14 +17,14 @@ """migrate filter sets to new format Revision ID: fc3a3a8ff221 -Revises: 085f06488938 +Revises: 453530256cea Create Date: 2021-04-12 12:38:03.913514 """ # revision identifiers, used by Alembic. revision = "fc3a3a8ff221" -down_revision = "085f06488938" +down_revision = "453530256cea" import json from typing import Any, Dict, Iterable From 2452389e4a0b499be9e610467b0e5f604dedc6f4 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 25 May 2021 14:02:52 +0300 Subject: [PATCH 23/60] fix: update down_revision --- superset/migrations/versions/3ebe0993c770_filterset_table.py | 4 ++-- .../fc3a3a8ff221_migrate_filter_sets_to_new_format.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 20c142d0074f6..306a5f0c61a5d 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -17,14 +17,14 @@ """empty message Revision ID: 3ebe0993c770 -Revises: d416d0d715cc +Revises: 453530256cea Create Date: 2021-03-29 11:15:48.831225 """ # revision identifiers, used by Alembic. revision = "3ebe0993c770" -down_revision = "d416d0d715cc" +down_revision = "453530256cea" import sqlalchemy as sa from alembic import op diff --git a/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py b/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py index 8b1edccdb1ec9..79b032894f058 100644 --- a/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py +++ b/superset/migrations/versions/fc3a3a8ff221_migrate_filter_sets_to_new_format.py @@ -17,14 +17,14 @@ """migrate filter sets to new format Revision ID: fc3a3a8ff221 -Revises: 453530256cea +Revises: 085f06488938 Create Date: 2021-04-12 12:38:03.913514 """ # revision identifiers, used by Alembic. revision = "fc3a3a8ff221" -down_revision = "453530256cea" +down_revision = "085f06488938" import json from typing import Any, Dict, Iterable From 2c7d2c4b4640ab36fd9fa4f403634595e86bd61e Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 25 May 2021 14:04:12 +0300 Subject: [PATCH 24/60] chore: add description to migration --- superset/migrations/versions/3ebe0993c770_filterset_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 306a5f0c61a5d..7b21984202271 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -"""empty message +"""add filter set model Revision ID: 3ebe0993c770 Revises: 453530256cea From 4aff6b1a10da1aacc2f6009e516f7c670b4c14c6 Mon Sep 17 00:00:00 2001 From: Amit Miran <47772523+amitmiran137@users.noreply.github.com> Date: Sun, 6 Jun 2021 11:20:20 +0300 Subject: [PATCH 25/60] fix: type --- superset/dashboards/filter_sets/commands/exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/dashboards/filter_sets/commands/exceptions.py index 323028338f3ac..2ed807592e57e 100644 --- a/superset/dashboards/filter_sets/commands/exceptions.py +++ b/superset/dashboards/filter_sets/commands/exceptions.py @@ -35,7 +35,7 @@ def __init__( class FilterSetCreateFailedError(CreateFailedError): - base_massage = 'CreateFilterSetCommand of dashboard "%s" failed: ' + base_message = 'CreateFilterSetCommand of dashboard "%s" failed: ' def __init__( self, dashboard_id: str, reason: str = "", exception: Optional[Exception] = None @@ -44,7 +44,7 @@ def __init__( class FilterSetUpdateFailedError(UpdateFailedError): - base_massage = 'UpdateFilterSetCommand of filter_set "%s" failed: ' + base_message = 'UpdateFilterSetCommand of filter_set "%s" failed: ' def __init__( self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None @@ -53,7 +53,7 @@ def __init__( class FilterSetDeleteFailedError(DeleteFailedError): - base_massage = 'DeleteFilterSetCommand of filter_set "%s" failed: ' + base_message = 'DeleteFilterSetCommand of filter_set "%s" failed: ' def __init__( self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None From 145b07d7ad5a5d67de5eeb24a117f345bd059392 Mon Sep 17 00:00:00 2001 From: Amit Miran <47772523+amitmiran137@users.noreply.github.com> Date: Sun, 6 Jun 2021 11:22:51 +0300 Subject: [PATCH 26/60] refactor: is_user_admin --- superset/common/request_contexed_based.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/superset/common/request_contexed_based.py b/superset/common/request_contexed_based.py index b7344f28af1e6..658b7897d8e25 100644 --- a/superset/common/request_contexed_based.py +++ b/superset/common/request_contexed_based.py @@ -35,4 +35,5 @@ def get_user_roles() -> List[Role]: def is_user_admin() -> bool: user_roles = [role.name.lower() for role in list(get_user_roles())] - return "admin" in user_roles + admin_role = conf.get("AUTH_ROLE_ADMIN").lower() + return admin_role in user_roles From 73ab92f9c3d72926dbaa690f98460f56b0777243 Mon Sep 17 00:00:00 2001 From: Amit Miran <47772523+amitmiran137@users.noreply.github.com> Date: Sun, 6 Jun 2021 11:25:17 +0300 Subject: [PATCH 27/60] fix: use get_public_role --- superset/common/request_contexed_based.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/common/request_contexed_based.py b/superset/common/request_contexed_based.py index 658b7897d8e25..515672e61dc79 100644 --- a/superset/common/request_contexed_based.py +++ b/superset/common/request_contexed_based.py @@ -29,7 +29,7 @@ def get_user_roles() -> List[Role]: if g.user.is_anonymous: public_role = conf.get("AUTH_ROLE_PUBLIC") - return [security_manager.find_role(public_role)] if public_role else [] + return [security_manager.get_public_role()] if public_role else [] return g.user.roles From af8e35f709386a96dd2a9c3d4be6c8f4b84749dd Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Sun, 6 Jun 2021 15:10:57 +0300 Subject: [PATCH 28/60] fix: move import to the relevant location --- superset/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/app.py b/superset/app.py index 9f6929703afaa..6bb4813147c54 100644 --- a/superset/app.py +++ b/superset/app.py @@ -25,6 +25,7 @@ from flask_babel import gettext as __, lazy_gettext as _ from flask_compress import Compress + from superset.connectors.connector_registry import ConnectorRegistry from superset.extensions import ( _event_logger, @@ -158,6 +159,7 @@ def init_views(self) -> None: from superset.reports.api import ReportScheduleRestApi from superset.reports.logs.api import ReportExecutionLogRestApi from superset.views.access_requests import AccessRequestsModelView + from superset.dashboards.filter_sets.api import FilterSetRestApi from superset.views.alerts import ( AlertLogModelView, AlertModelView, @@ -549,8 +551,6 @@ def init_views(self) -> None: "Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]) ) - from superset.dashboards.filter_sets.api import FilterSetRestApi - appbuilder.add_api(FilterSetRestApi) def init_app_in_ctx(self) -> None: From 48b84e8b69343a15f2ac90e1c36399c25704b720 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Sun, 6 Jun 2021 16:12:55 +0300 Subject: [PATCH 29/60] chore: add openSpec api schema --- superset/dashboards/filter_sets/api.py | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 22e14f5441299..0b3701c325752 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -126,7 +126,53 @@ def _init_properties(self) -> None: def get_list(self, **kwargs: Any) -> Response: """ Gets a dashboard's Filter-sets + --- + get: + description: >- + Get a dashboard's list of filter sets + parameters: + - in: path + schema: + type: string + name: id_or_slug + description: Either 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' """ + dashboard_id: Optional[int] = kwargs.get("dashboard_id", None) if not DashboardDAO.find_by_id(cast(int, dashboard_id)): return self.response(404, message="dashboard '%s' not found" % dashboard_id) From f7526b800def82de5cccc223c3011330b7f60016 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Sun, 6 Jun 2021 17:05:00 +0300 Subject: [PATCH 30/60] chore: cover all openspec API --- superset/dashboards/filter_sets/api.py | 128 +++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 6 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 0b3701c325752..08e39af7406a9 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -125,7 +125,7 @@ def _init_properties(self) -> None: @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) def get_list(self, **kwargs: Any) -> Response: """ - Gets a dashboard's Filter-sets + Gets a dashboard's Filter sets --- get: description: >- @@ -133,9 +133,9 @@ def get_list(self, **kwargs: Any) -> Response: parameters: - in: path schema: - type: string - name: id_or_slug - description: Either the id of the dashboard + type: int + name: dashboard_id + description: The id of the dashboard responses: 200: description: FilterSets @@ -193,7 +193,46 @@ def get_list(self, **kwargs: Any) -> Response: ) def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 """ - Creates a new Dashboard's FilterSet + Creates a new Dashboard's Filter Set + --- + post: + description: >- + Create a new Dashboard's Filter Set. + parameters: + - in: path + schema: + type: int + name: dashboard_id + description: The id of the dashboard + requestBody: + description: Filter set schema + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FilterSetPostSchema' + responses: + 201: + description: Filter set added + content: + application/json: + schema: + type: object + properties: + id: + type: number + result: + $ref: '#/components/schemas/FilterSetPostSchema' + 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") @@ -220,7 +259,51 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 ) def put(self, dashboard_id: int, pk: int) -> Response: """ - Changes a Dashboard's Filterset + 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/FilterSetPutSchema' + responses: + 200: + description: Filter set changed + content: + application/json: + schema: + type: object + properties: + id: + type: number + result: + $ref: '#/components/schemas/FilterSetPutSchema' + 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") @@ -249,6 +332,39 @@ def put(self, dashboard_id: int, pk: int) -> Response: def delete(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 """ 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() From 7ed5321f27daec6d1e99e9ab626576971bddd449 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Sun, 6 Jun 2021 17:22:45 +0300 Subject: [PATCH 31/60] fix: pre-commit and lint --- superset/app.py | 1 - superset/dashboards/filter_sets/commands/exceptions.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/superset/app.py b/superset/app.py index 6bb4813147c54..eb642d7ca7d41 100644 --- a/superset/app.py +++ b/superset/app.py @@ -25,7 +25,6 @@ from flask_babel import gettext as __, lazy_gettext as _ from flask_compress import Compress - from superset.connectors.connector_registry import ConnectorRegistry from superset.extensions import ( _event_logger, diff --git a/superset/dashboards/filter_sets/commands/exceptions.py b/superset/dashboards/filter_sets/commands/exceptions.py index 2ed807592e57e..ade0bbbe9090a 100644 --- a/superset/dashboards/filter_sets/commands/exceptions.py +++ b/superset/dashboards/filter_sets/commands/exceptions.py @@ -40,7 +40,7 @@ class FilterSetCreateFailedError(CreateFailedError): def __init__( self, dashboard_id: str, reason: str = "", exception: Optional[Exception] = None ) -> None: - super().__init__((self.base_massage % dashboard_id) + reason, exception) + super().__init__((self.base_message % dashboard_id) + reason, exception) class FilterSetUpdateFailedError(UpdateFailedError): @@ -49,7 +49,7 @@ class FilterSetUpdateFailedError(UpdateFailedError): def __init__( self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None ) -> None: - super().__init__((self.base_massage % filterset_id) + reason, exception) + super().__init__((self.base_message % filterset_id) + reason, exception) class FilterSetDeleteFailedError(DeleteFailedError): @@ -58,7 +58,7 @@ class FilterSetDeleteFailedError(DeleteFailedError): def __init__( self, filterset_id: str, reason: str = "", exception: Optional[Exception] = None ) -> None: - super().__init__((self.base_massage % filterset_id) + reason, exception) + super().__init__((self.base_message % filterset_id) + reason, exception) class UserIsNotDashboardOwnerError(FilterSetCreateFailedError): From dbb0ae04feb8e13559db1848ab500f9af01f8d7e Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Sun, 6 Jun 2021 17:33:18 +0300 Subject: [PATCH 32/60] fix: put and post schemas --- superset/dashboards/filter_sets/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 08e39af7406a9..9032c9a9bff27 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -210,7 +210,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 content: application/json: schema: - $ref: '#/components/schemas/FilterSetPostSchema' + $ref: '#/components/schemas/{{self.__class__.__name__}}.post' responses: 201: description: Filter set added @@ -222,7 +222,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 id: type: number result: - $ref: '#/components/schemas/FilterSetPostSchema' + $ref: '#/components/schemas/{{self.__class__.__name__}}.post' 302: description: Redirects to the current digest 400: @@ -279,7 +279,7 @@ def put(self, dashboard_id: int, pk: int) -> Response: content: application/json: schema: - $ref: '#/components/schemas/FilterSetPutSchema' + $ref: '#/components/schemas/{{self.__class__.__name__}}.put' responses: 200: description: Filter set changed @@ -291,7 +291,7 @@ def put(self, dashboard_id: int, pk: int) -> Response: id: type: number result: - $ref: '#/components/schemas/FilterSetPutSchema' + $ref: '#/components/schemas/{{self.__class__.__name__}}.put' 400: $ref: '#/components/responses/400' 401: From 033734df72022fdcd18b058105ef9cbed0517db4 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Sun, 6 Jun 2021 18:01:56 +0300 Subject: [PATCH 33/60] fix: undo superset_test_config.py --- tests/superset_test_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/superset_test_config.py b/tests/superset_test_config.py index 04dcf11bfd568..48e46745fab84 100644 --- a/tests/superset_test_config.py +++ b/tests/superset_test_config.py @@ -22,9 +22,8 @@ AUTH_USER_REGISTRATION_ROLE = "alpha" SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(DATA_DIR, "unittests.db") -DEBUG = True +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: From ebb14053e04787bcc0619c4db6d73b4a786c5165 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Mon, 7 Jun 2021 19:25:42 +0300 Subject: [PATCH 34/60] fix: limit filterSetsApi to include_route_methods = {"get_list", "put", "post", "delete"} --- superset/app.py | 3 +-- superset/dashboards/filter_sets/api.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/superset/app.py b/superset/app.py index eb642d7ca7d41..a9862001f3f94 100644 --- a/superset/app.py +++ b/superset/app.py @@ -215,6 +215,7 @@ def init_views(self) -> None: appbuilder.add_api(CacheRestApi) appbuilder.add_api(ChartRestApi) appbuilder.add_api(CssTemplateRestApi) + appbuilder.add_api(FilterSetRestApi) appbuilder.add_api(DashboardRestApi) appbuilder.add_api(DatabaseRestApi) appbuilder.add_api(DatasetRestApi) @@ -550,8 +551,6 @@ def init_views(self) -> None: "Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]) ) - appbuilder.add_api(FilterSetRestApi) - def init_app_in_ctx(self) -> None: """ Runs init logic in the context of the app diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 9032c9a9bff27..271f29823ca38 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -75,7 +75,7 @@ class FilterSetRestApi(BaseSupersetModelRestApi): # pylint: disable=W0221 - + include_route_methods = {"get_list", "put", "post", "delete"} datamodel = SQLAInterface(FilterSet) resource_name = "dashboard" class_permission_name = FILTER_SET_API_PERMISSIONS_NAME From 16c8a188be2b76ee945c416f100005d63178723a Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 8 Jun 2021 10:25:36 +0300 Subject: [PATCH 35/60] renaming some params --- superset/models/dashboard.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index b9854c6f8e18e..a67df9e5bd534 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -175,13 +175,19 @@ def filter_sets_lst(self) -> Dict[int, FilterSet]: if is_user_admin(): return self._filter_sets current_user = g.user.id - mapa: Dict[str, List[Any]] = {"Dashboard": [], "User": []} + filter_sets_by_owner_type: Dict[str, List[Any]] = {"Dashboard": [], "User": []} for fs in self._filter_sets: - mapa[fs.owner_type].append(fs) - rv = list( - filter(lambda filter_set: filter_set.owner_id == current_user, mapa["User"]) + 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 rv + mapa["Dashboard"]} + return { + fs.id: fs + for fs in user_filter_sets + filter_sets_by_owner_type["Dashboard"] + } @property def table_names(self) -> str: From 94d8478ef805f424e10b749bfd39320ebe87d70f Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 8 Jun 2021 10:28:33 +0300 Subject: [PATCH 36/60] chore: add debug in test config --- tests/superset_test_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/superset_test_config.py b/tests/superset_test_config.py index 48e46745fab84..04dcf11bfd568 100644 --- a/tests/superset_test_config.py +++ b/tests/superset_test_config.py @@ -22,8 +22,9 @@ AUTH_USER_REGISTRATION_ROLE = "alpha" SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(DATA_DIR, "unittests.db") -DEBUG = False +DEBUG = True 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: From 6b4b3d770ded5211a939f7ee4d3a008d105c5279 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 8 Jun 2021 17:23:45 +0300 Subject: [PATCH 37/60] fix: rename database to different name --- tests/dashboards/filter_sets/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dashboards/filter_sets/conftest.py b/tests/dashboards/filter_sets/conftest.py index 0b3883ede866c..fc26c170c77e1 100644 --- a/tests/dashboards/filter_sets/conftest.py +++ b/tests/dashboards/filter_sets/conftest.py @@ -152,7 +152,7 @@ def dashboard() -> Generator[Dashboard, None, None]: try: with app.app_context() as ctx: dashboard_owner_user = security_manager.find_user(DASHBOARD_OWNER_USERNAME) - database = create_database("test_database") + database = create_database("test_database_filter_sets") datasource = create_datasource_table( name="test_datasource", database=database, owners=[dashboard_owner_user] ) From 90786776c2461516ea775072d7eb32f80c332dfb Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 8 Jun 2021 18:41:10 +0300 Subject: [PATCH 38/60] fix: try to make conftest.py harmless --- tests/dashboards/filter_sets/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/dashboards/filter_sets/conftest.py b/tests/dashboards/filter_sets/conftest.py index fc26c170c77e1..f5e2058826f59 100644 --- a/tests/dashboards/filter_sets/conftest.py +++ b/tests/dashboards/filter_sets/conftest.py @@ -75,6 +75,8 @@ def expire_on_commit_true() -> 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") From e84dd266ec496863464f4929e6de58f0440edd7c Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 8 Jun 2021 19:09:25 +0300 Subject: [PATCH 39/60] fix: pre-commit --- tests/dashboards/filter_sets/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dashboards/filter_sets/conftest.py b/tests/dashboards/filter_sets/conftest.py index f5e2058826f59..0ff14de5eeff6 100644 --- a/tests/dashboards/filter_sets/conftest.py +++ b/tests/dashboards/filter_sets/conftest.py @@ -71,7 +71,7 @@ @pytest.fixture(autouse=True) -def expire_on_commit_true() -> None: +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) From 1c28bf868baac823ac5bb8872512c3d058726186 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 24 Aug 2021 09:38:54 +0300 Subject: [PATCH 40/60] fix: new down_revision ref --- superset/migrations/versions/3ebe0993c770_filterset_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 7b21984202271..32cee5b28622b 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -17,14 +17,14 @@ """add filter set model Revision ID: 3ebe0993c770 -Revises: 453530256cea +Revises: 07071313dd52 Create Date: 2021-03-29 11:15:48.831225 """ # revision identifiers, used by Alembic. revision = "3ebe0993c770" -down_revision = "453530256cea" +down_revision = "07071313dd52" import sqlalchemy as sa from alembic import op From a6b6c11e696ffde9d0a04cce54153f3eba586602 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 24 Aug 2021 09:45:21 +0300 Subject: [PATCH 41/60] fix: bad ref --- tests/integration_tests/charts/filter_sets/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests/charts/filter_sets/conftest.py b/tests/integration_tests/charts/filter_sets/conftest.py index 0ff14de5eeff6..c01e7db64986d 100644 --- a/tests/integration_tests/charts/filter_sets/conftest.py +++ b/tests/integration_tests/charts/filter_sets/conftest.py @@ -32,19 +32,19 @@ ) from superset.models.dashboard import Dashboard from superset.models.filter_set import FilterSet -from tests.dashboards.filter_sets.consts import ( +from tests.integration_tests.charts.filter_sets.consts import ( ADMIN_USERNAME_FOR_TEST, DASHBOARD_OWNER_USERNAME, FILTER_SET_OWNER_USERNAME, REGULAR_USER, ) -from tests.dashboards.superset_factory_util import ( +from tests.integration_tests.dashboards.superset_factory_util import ( create_dashboard, create_database, create_datasource_table, create_slice, ) -from tests.test_app import app +from tests.integration_tests.test_app import app if TYPE_CHECKING: from flask.ctx import AppContext From 645e371ed37457aed3cccac3d9974ec6cf532663 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 24 Aug 2021 10:13:26 +0300 Subject: [PATCH 42/60] fix: bad ref 2 --- .../{charts => dashboards}/filter_sets/__init__.py | 0 .../{charts => dashboards}/filter_sets/conftest.py | 2 +- .../{charts => dashboards}/filter_sets/consts.py | 0 .../{charts => dashboards}/filter_sets/create_api_tests.py | 6 +++--- .../{charts => dashboards}/filter_sets/delete_api_tests.py | 6 +++--- .../{charts => dashboards}/filter_sets/get_api_tests.py | 6 +++--- .../{charts => dashboards}/filter_sets/update_api_tests.py | 6 +++--- .../{charts => dashboards}/filter_sets/utils.py | 0 8 files changed, 13 insertions(+), 13 deletions(-) rename tests/integration_tests/{charts => dashboards}/filter_sets/__init__.py (100%) rename tests/integration_tests/{charts => dashboards}/filter_sets/conftest.py (99%) rename tests/integration_tests/{charts => dashboards}/filter_sets/consts.py (100%) rename tests/integration_tests/{charts => dashboards}/filter_sets/create_api_tests.py (99%) rename tests/integration_tests/{charts => dashboards}/filter_sets/delete_api_tests.py (97%) rename tests/integration_tests/{charts => dashboards}/filter_sets/get_api_tests.py (94%) rename tests/integration_tests/{charts => dashboards}/filter_sets/update_api_tests.py (98%) rename tests/integration_tests/{charts => dashboards}/filter_sets/utils.py (100%) diff --git a/tests/integration_tests/charts/filter_sets/__init__.py b/tests/integration_tests/dashboards/filter_sets/__init__.py similarity index 100% rename from tests/integration_tests/charts/filter_sets/__init__.py rename to tests/integration_tests/dashboards/filter_sets/__init__.py diff --git a/tests/integration_tests/charts/filter_sets/conftest.py b/tests/integration_tests/dashboards/filter_sets/conftest.py similarity index 99% rename from tests/integration_tests/charts/filter_sets/conftest.py rename to tests/integration_tests/dashboards/filter_sets/conftest.py index c01e7db64986d..807d6ccb023c8 100644 --- a/tests/integration_tests/charts/filter_sets/conftest.py +++ b/tests/integration_tests/dashboards/filter_sets/conftest.py @@ -32,7 +32,7 @@ ) from superset.models.dashboard import Dashboard from superset.models.filter_set import FilterSet -from tests.integration_tests.charts.filter_sets.consts import ( +from tests.integration_tests.dashboards.filter_sets.consts import ( ADMIN_USERNAME_FOR_TEST, DASHBOARD_OWNER_USERNAME, FILTER_SET_OWNER_USERNAME, diff --git a/tests/integration_tests/charts/filter_sets/consts.py b/tests/integration_tests/dashboards/filter_sets/consts.py similarity index 100% rename from tests/integration_tests/charts/filter_sets/consts.py rename to tests/integration_tests/dashboards/filter_sets/consts.py diff --git a/tests/integration_tests/charts/filter_sets/create_api_tests.py b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py similarity index 99% rename from tests/integration_tests/charts/filter_sets/create_api_tests.py rename to tests/integration_tests/dashboards/filter_sets/create_api_tests.py index d0817c5f71acf..4dade9e4d6a2a 100644 --- a/tests/integration_tests/charts/filter_sets/create_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py @@ -27,13 +27,13 @@ OWNER_TYPE_FIELD, USER_OWNER_TYPE, ) -from tests.base_tests import login -from tests.dashboards.filter_sets.consts import ( +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.dashboards.filter_sets.utils import ( +from tests.integration_tests.dashboards.filter_sets.utils import ( call_create_filter_set, get_filter_set_by_dashboard_id, get_filter_set_by_name, diff --git a/tests/integration_tests/charts/filter_sets/delete_api_tests.py b/tests/integration_tests/dashboards/filter_sets/delete_api_tests.py similarity index 97% rename from tests/integration_tests/charts/filter_sets/delete_api_tests.py rename to tests/integration_tests/dashboards/filter_sets/delete_api_tests.py index 1ba9402f94150..12a9bff9e13b8 100644 --- a/tests/integration_tests/charts/filter_sets/delete_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/delete_api_tests.py @@ -18,13 +18,13 @@ from typing import Any, Dict, List, TYPE_CHECKING -from tests.base_tests import login -from tests.dashboards.filter_sets.consts import ( +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.dashboards.filter_sets.utils import ( +from tests.integration_tests.dashboards.filter_sets.utils import ( call_delete_filter_set, collect_all_ids, get_filter_set_by_name, diff --git a/tests/integration_tests/charts/filter_sets/get_api_tests.py b/tests/integration_tests/dashboards/filter_sets/get_api_tests.py similarity index 94% rename from tests/integration_tests/charts/filter_sets/get_api_tests.py rename to tests/integration_tests/dashboards/filter_sets/get_api_tests.py index 97164d4ee3d2c..18725c7e7a885 100644 --- a/tests/integration_tests/charts/filter_sets/get_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/get_api_tests.py @@ -18,13 +18,13 @@ from typing import Any, Dict, List, Set, TYPE_CHECKING -from tests.base_tests import login -from tests.dashboards.filter_sets.consts import ( +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.dashboards.filter_sets.utils import call_get_filter_sets, collect_all_ids +from tests.integration_tests.dashboards.filter_sets.utils import call_get_filter_sets, collect_all_ids if TYPE_CHECKING: from flask.testing import FlaskClient diff --git a/tests/integration_tests/charts/filter_sets/update_api_tests.py b/tests/integration_tests/dashboards/filter_sets/update_api_tests.py similarity index 98% rename from tests/integration_tests/charts/filter_sets/update_api_tests.py rename to tests/integration_tests/dashboards/filter_sets/update_api_tests.py index 5f098f216c79c..f6557b80a0515 100644 --- a/tests/integration_tests/charts/filter_sets/update_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/update_api_tests.py @@ -24,13 +24,13 @@ NAME_FIELD, OWNER_TYPE_FIELD, ) -from tests.base_tests import login -from tests.dashboards.filter_sets.consts import ( +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.dashboards.filter_sets.utils import ( +from tests.integration_tests.dashboards.filter_sets.utils import ( call_update_filter_set, collect_all_ids, get_filter_set_by_name, diff --git a/tests/integration_tests/charts/filter_sets/utils.py b/tests/integration_tests/dashboards/filter_sets/utils.py similarity index 100% rename from tests/integration_tests/charts/filter_sets/utils.py rename to tests/integration_tests/dashboards/filter_sets/utils.py From e2cb5520c897459ec8193bc48815273c6983214b Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 24 Aug 2021 10:26:31 +0300 Subject: [PATCH 43/60] fix: bad ref 3 --- tests/integration_tests/dashboards/filter_sets/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/dashboards/filter_sets/utils.py b/tests/integration_tests/dashboards/filter_sets/utils.py index df2914009b5ab..a63e4164d8959 100644 --- a/tests/integration_tests/dashboards/filter_sets/utils.py +++ b/tests/integration_tests/dashboards/filter_sets/utils.py @@ -19,8 +19,8 @@ from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union from superset.models.filter_set import FilterSet -from tests.dashboards.filter_sets.consts import FILTER_SET_URI -from tests.test_app import app +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 adfe51349f91ca4fed9e98c9a2fbd9fdc44c9cd8 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Tue, 24 Aug 2021 18:52:25 +0300 Subject: [PATCH 44/60] fix: add api in initiatior --- superset/initialization/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 79c466069e762..505ce4a1e0c1c 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -142,6 +142,7 @@ def init_views(self) -> None: 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 @@ def init_views(self) -> None: appbuilder.add_api(SavedQueryRestApi) appbuilder.add_api(ReportScheduleRestApi) appbuilder.add_api(ReportExecutionLogRestApi) + appbuilder.add_api(FilterSetRestApi) # # Setup regular views # From abef564bb61b40da0d6ba0ad085a9d2ae7eef3f0 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Wed, 25 Aug 2021 08:04:01 +0300 Subject: [PATCH 45/60] fix: open spec --- superset/dashboards/filter_sets/api.py | 20 ++++--------------- .../dashboards/filter_sets/get_api_tests.py | 5 ++++- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 271f29823ca38..a0e15009b7ec9 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -114,16 +114,7 @@ def _init_properties(self) -> None: @safe @permission_name("get") @rison(get_list_schema) - @merge_response_func(ModelRestApi.merge_order_columns, API_ORDER_COLUMNS_RIS_KEY) - @merge_response_func( - ModelRestApi.merge_list_label_columns, API_LABEL_COLUMNS_RIS_KEY - ) - @merge_response_func( - ModelRestApi.merge_description_columns, API_DESCRIPTION_COLUMNS_RIS_KEY - ) - @merge_response_func(ModelRestApi.merge_list_columns, API_LIST_COLUMNS_RIS_KEY) - @merge_response_func(ModelRestApi.merge_list_title, API_LIST_TITLE_RIS_KEY) - def get_list(self, **kwargs: Any) -> Response: + def get_list(self, dashboard_id: int, **kwargs: Any) -> Response: """ Gets a dashboard's Filter sets --- @@ -133,7 +124,7 @@ def get_list(self, **kwargs: Any) -> Response: parameters: - in: path schema: - type: int + type: integer name: dashboard_id description: The id of the dashboard responses: @@ -172,8 +163,6 @@ def get_list(self, **kwargs: Any) -> Response: 404: $ref: '#/components/responses/404' """ - - dashboard_id: Optional[int] = kwargs.get("dashboard_id", None) 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", {}) @@ -201,7 +190,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 parameters: - in: path schema: - type: int + type: integer name: dashboard_id description: The id of the dashboard requestBody: @@ -258,8 +247,7 @@ def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 log_to_statsd=False, ) def put(self, dashboard_id: int, pk: int) -> Response: - """ - Changes a Dashboard's Filter set + """Changes a Dashboard's Filter set --- put: description: >- diff --git a/tests/integration_tests/dashboards/filter_sets/get_api_tests.py b/tests/integration_tests/dashboards/filter_sets/get_api_tests.py index 18725c7e7a885..3db2d472eae71 100644 --- a/tests/integration_tests/dashboards/filter_sets/get_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/get_api_tests.py @@ -24,7 +24,10 @@ FILTER_SET_OWNER_USERNAME, REGULAR_USER, ) -from tests.integration_tests.dashboards.filter_sets.utils import call_get_filter_sets, collect_all_ids +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 d50027272bb1ee26b47aabd7e418d0e658c817e9 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Sun, 29 Aug 2021 12:38:03 +0300 Subject: [PATCH 46/60] fix: convert name to str to include int usecases --- .../dashboards/filter_sets/create_api_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/dashboards/filter_sets/create_api_tests.py b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py index 4dade9e4d6a2a..cbdaef9b95a01 100644 --- a/tests/integration_tests/dashboards/filter_sets/create_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py @@ -44,7 +44,7 @@ def assert_filterset_was_not_created(filter_set_data: Dict[str, Any]) -> None: - assert get_filter_set_by_name(filter_set_data["name"]) is 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: From 1dfb029958ebd792752bc7ad881cc5eaec39f802 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Mon, 30 Aug 2021 12:31:55 +0300 Subject: [PATCH 47/60] fix: pylint --- superset/dashboards/filter_sets/api.py | 15 ++++----------- superset/dashboards/filter_sets/commands/base.py | 6 ++---- .../dashboards/filter_sets/commands/delete.py | 4 ++-- .../dashboards/filter_sets/commands/update.py | 2 +- superset/dashboards/filter_sets/dao.py | 4 ++-- superset/dashboards/filter_sets/filters.py | 2 +- superset/models/dashboard.py | 1 + superset/models/filter_set.py | 6 +----- 8 files changed, 14 insertions(+), 26 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index a0e15009b7ec9..83f842b05cb8b 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -15,19 +15,12 @@ # specific language governing permissions and limitations # under the License. import logging -from typing import Any, cast, Optional +from typing import Any, cast from flask import g, request, Response from flask_appbuilder.api import ( - API_DESCRIPTION_COLUMNS_RIS_KEY, - API_LABEL_COLUMNS_RIS_KEY, - API_LIST_COLUMNS_RIS_KEY, - API_LIST_TITLE_RIS_KEY, - API_ORDER_COLUMNS_RIS_KEY, expose, get_list_schema, - merge_response_func, - ModelRestApi, permission_name, protect, rison, @@ -74,7 +67,7 @@ class FilterSetRestApi(BaseSupersetModelRestApi): - # pylint: disable=W0221 + # pylint: disable=arguments-differ include_route_methods = {"get_list", "put", "post", "delete"} datamodel = SQLAInterface(FilterSet) resource_name = "dashboard" @@ -180,7 +173,7 @@ def get_list(self, dashboard_id: int, **kwargs: Any) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post", log_to_statsd=False, ) - def post(self, dashboard_id: int) -> Response: # pylint: disable=W0221 + def post(self, dashboard_id: int) -> Response: """ Creates a new Dashboard's Filter Set --- @@ -317,7 +310,7 @@ def put(self, dashboard_id: int, pk: int) -> Response: action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete", log_to_statsd=False, ) - def delete(self, dashboard_id: int, pk: int) -> Response: # pylint: disable=W0221 + def delete(self, dashboard_id: int, pk: int) -> Response: """ Deletes a Dashboard's FilterSet --- diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index fe2e0c10f508e..3e24f1ecfc98f 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -89,9 +89,7 @@ def check_ownership(self) -> None: ) except NotAuthorizedException as err: raise FilterSetForbiddenError( - str(self._filter_set_id), - "user not authorized to access the filterset", - err, - ) + str(self._filter_set_id), "user not authorized to access the filterset", + ) from err except FilterSetForbiddenError as err: raise err diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py index ae10916bd875a..cdd9a28c4315f 100644 --- a/superset/dashboards/filter_sets/commands/delete.py +++ b/superset/dashboards/filter_sets/commands/delete.py @@ -41,7 +41,7 @@ def run(self) -> Model: self.validate() return FilterSetDAO.delete(self._filter_set, commit=True) except DAODeleteFailedError as err: - raise FilterSetDeleteFailedError(str(self._filter_set_id), "", err) + raise FilterSetDeleteFailedError(str(self._filter_set_id), "") from err def validate(self) -> None: super().validate() @@ -52,5 +52,5 @@ def validate(self) -> None: raise FilterSetForbiddenError( 'the filter-set does not related to dashboard "%s"' % str(self._dashboard_id) - ) + ) from err raise err diff --git a/superset/dashboards/filter_sets/commands/update.py b/superset/dashboards/filter_sets/commands/update.py index 6b20adbb9d45e..3346a16eb8676 100644 --- a/superset/dashboards/filter_sets/commands/update.py +++ b/superset/dashboards/filter_sets/commands/update.py @@ -49,7 +49,7 @@ def run(self) -> Model: 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), "", err) + raise FilterSetUpdateFailedError(str(self._filter_set_id), "") from err def validate(self) -> None: super().validate() diff --git a/superset/dashboards/filter_sets/dao.py b/superset/dashboards/filter_sets/dao.py index da30afd71646e..949aa6d3fdf25 100644 --- a/superset/dashboards/filter_sets/dao.py +++ b/superset/dashboards/filter_sets/dao.py @@ -43,7 +43,7 @@ class FilterSetDAO(BaseDAO): def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model: if cls.model_cls is None: raise DAOConfigError() - model = FilterSet() # pylint: disable=not-callable + 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)) @@ -60,5 +60,5 @@ def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model: db.session.commit() except SQLAlchemyError as ex: # pragma: no cover db.session.rollback() - raise DAOCreateFailedError(exception=ex) + raise DAOCreateFailedError() from ex return model diff --git a/superset/dashboards/filter_sets/filters.py b/superset/dashboards/filter_sets/filters.py index fefa265d45588..0083f40d1a578 100644 --- a/superset/dashboards/filter_sets/filters.py +++ b/superset/dashboards/filter_sets/filters.py @@ -30,7 +30,7 @@ from sqlalchemy.orm.query import Query -class FilterSetFilter(BaseFilter): +class FilterSetFilter(BaseFilter): # pylint: disable=too-few-public-methods) def apply(self, query: Query, value: Any) -> Query: if is_user_admin(): return query diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index 04dfd7939ffa0..b9fec40aa7e5b 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -132,6 +132,7 @@ def copy_dashboard( ) +# pylint: disable=too-many-public-methods class Dashboard(Model, AuditMixinNullable, ImportExportMixin): """The dashboard object!""" diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 867e5ef3d380b..5627bb4899a3c 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -27,16 +27,12 @@ from superset import app, db from superset.models.helpers import AuditMixinNullable -# pylint: disable=too-many-public-methods - metadata = Model.metadata # pylint: disable=no-member config = app.config logger = logging.getLogger(__name__) -class FilterSet( # pylint: disable=too-many-instance-attributes - Model, AuditMixinNullable -): +class FilterSet(Model, AuditMixinNullable): __tablename__ = "filter_sets" id = Column(Integer, primary_key=True) name = Column(String(500), nullable=False, unique=True) From 07792fa599dad7d6b7ab0c4bd4ede9b6bef88318 Mon Sep 17 00:00:00 2001 From: amitmiran137 Date: Mon, 30 Aug 2021 12:32:31 +0300 Subject: [PATCH 48/60] fix: pylint --- superset/dashboards/filter_sets/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 83f842b05cb8b..44d56958fb22d 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -99,7 +99,7 @@ def __init__(self) -> None: super().__init__() def _init_properties(self) -> None: - # pylint: disable=E1003 + # pylint: disable=bad-super-call super(BaseSupersetModelRestApi, self)._init_properties() @expose("//filtersets", methods=["GET"]) From 55dbfa4ae3f57943cf57f783d33bda5b0aa585f1 Mon Sep 17 00:00:00 2001 From: Amit Miran <47772523+amitmiran137@users.noreply.github.com> Date: Tue, 7 Sep 2021 22:38:10 +0300 Subject: [PATCH 49/60] Update superset/common/request_contexed_based.py Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> --- superset/common/request_contexed_based.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/common/request_contexed_based.py b/superset/common/request_contexed_based.py index 515672e61dc79..0b06a0ccbe1d5 100644 --- a/superset/common/request_contexed_based.py +++ b/superset/common/request_contexed_based.py @@ -34,6 +34,6 @@ def get_user_roles() -> List[Role]: def is_user_admin() -> bool: - user_roles = [role.name.lower() for role in list(get_user_roles())] + 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 From 568b3a5fb0eee38e9db9d9b9894004e73a376697 Mon Sep 17 00:00:00 2001 From: ofekisr Date: Mon, 13 Sep 2021 14:57:29 +0300 Subject: [PATCH 50/60] chore: resolve PR comments --- superset/commands/export.py | 2 +- superset/dashboards/filter_sets/commands/base.py | 2 +- superset/migrations/versions/3ebe0993c770_filterset_table.py | 2 +- superset/models/dashboard.py | 2 +- superset/models/filter_set.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/superset/commands/export.py b/superset/commands/export.py index 76a9694b0380b..2b54de87852e9 100644 --- a/superset/commands/export.py +++ b/superset/commands/export.py @@ -33,7 +33,7 @@ class ExportModelsCommand(BaseCommand): - dao = BaseDAO + dao: Type[BaseDAO] = BaseDAO not_found: Type[CommandException] = CommandException def __init__(self, model_ids: List[int]): diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index 3e24f1ecfc98f..e6bb717ea771f 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -59,7 +59,7 @@ def _validate_filterset_dashboard_exists(self) -> None: raise DashboardNotFoundError() def is_user_dashboard_owner(self) -> bool: - return self._is_actor_admin or self._dashboard.am_i_owner() + 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() diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 32cee5b28622b..36039fb1dd1b1 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -38,7 +38,7 @@ def upgrade(): 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.JSON(), nullable=False), + 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( diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index b9fec40aa7e5b..0db1ddf15048d 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -427,7 +427,7 @@ def get(cls, id_or_slug: str) -> Dashboard: qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug)) return qry.one_or_none() - def am_i_owner(self) -> bool: + 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)) diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 5627bb4899a3c..35711208c3b8a 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -37,7 +37,7 @@ class FilterSet(Model, AuditMixinNullable): id = Column(Integer, primary_key=True) name = Column(String(500), nullable=False, unique=True) description = Column(Text, nullable=True) - json_metadata = Column(JSON, nullable=False) + 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) From 635981782f2ab30b1b29f0586b34ba229d7b2b55 Mon Sep 17 00:00:00 2001 From: ofekisr Date: Mon, 13 Sep 2021 16:20:37 +0300 Subject: [PATCH 51/60] chore: resolve PR comments --- superset/migrations/versions/3ebe0993c770_filterset_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 36039fb1dd1b1..4556ac6caf92f 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -24,7 +24,7 @@ # revision identifiers, used by Alembic. revision = "3ebe0993c770" -down_revision = "07071313dd52" +down_revision = "021b81fe4fbb" import sqlalchemy as sa from alembic import op From 4af6a6a4a6d211d8d3ee62db9d5930ef752ace28 Mon Sep 17 00:00:00 2001 From: ofekisr Date: Mon, 13 Sep 2021 16:27:31 +0300 Subject: [PATCH 52/60] chore: resolve PR comments --- superset/models/filter_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 35711208c3b8a..1cd7360f21156 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -20,7 +20,7 @@ from typing import Any, Dict from flask_appbuilder import Model -from sqlalchemy import Column, ForeignKey, Integer, JSON, MetaData, String, Text +from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Text from sqlalchemy.orm import relationship from sqlalchemy_utils import generic_relationship From fd6decac40ccbe5cdbe97c9eeefa5d0c09526a9e Mon Sep 17 00:00:00 2001 From: ofekisr Date: Tue, 14 Sep 2021 18:50:20 +0300 Subject: [PATCH 53/60] fix failed tests --- superset/dashboards/filter_sets/api.py | 19 ++++++++--- superset/dashboards/filter_sets/consts.py | 1 + superset/dashboards/filter_sets/schemas.py | 33 ++++++++++++++----- superset/models/filter_set.py | 9 ++++- .../dashboards/filter_sets/conftest.py | 30 +++++++++++------ .../filter_sets/update_api_tests.py | 15 +++++++-- 6 files changed, 82 insertions(+), 25 deletions(-) diff --git a/superset/dashboards/filter_sets/api.py b/superset/dashboards/filter_sets/api.py index 44d56958fb22d..24c4e8655f4f7 100644 --- a/superset/dashboards/filter_sets/api.py +++ b/superset/dashboards/filter_sets/api.py @@ -53,6 +53,7 @@ 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 ( @@ -74,10 +75,20 @@ class FilterSetRestApi(BaseSupersetModelRestApi): class_permission_name = FILTER_SET_API_PERMISSIONS_NAME allow_browser_login = True csrf_exempt = False - add_exclude_columns = ["id", OWNER_OBJECT_FIELD, DASHBOARD_FIELD] + 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] + edit_exclude_columns = [ + "id", + OWNER_OBJECT_FIELD, + DASHBOARD_FIELD, + JSON_METADATA_FIELD, + ] list_columns = [ "created_on", "changed_on", @@ -88,9 +99,9 @@ class FilterSetRestApi(BaseSupersetModelRestApi): OWNER_TYPE_FIELD, OWNER_ID_FIELD, DASHBOARD_ID_FIELD, - JSON_METADATA_FIELD, + PARAMS_PROPERTY, ] - show_exclude_columns = [OWNER_OBJECT_FIELD, DASHBOARD_FIELD] + 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, ""]] diff --git a/superset/dashboards/filter_sets/consts.py b/superset/dashboards/filter_sets/consts.py index c2b694331bf53..ff60a4f8bc3ed 100644 --- a/superset/dashboards/filter_sets/consts.py +++ b/superset/dashboards/filter_sets/consts.py @@ -25,5 +25,6 @@ DASHBOARD_ID_FIELD = "dashboard_id" OWNER_OBJECT_FIELD = "owner_object" DASHBOARD_FIELD = "dashboard" +PARAMS_PROPERTY = "params" FILTER_SET_API_PERMISSIONS_NAME = "FilterSets" diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index ee58963bbb765..09435b12d390d 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -21,6 +21,7 @@ from superset.dashboards.filter_sets.consts import ( DASHBOARD_OWNER_TYPE, + JSON_METADATA_FIELD, OWNER_ID_FIELD, OWNER_TYPE_FIELD, USER_OWNER_TYPE, @@ -32,13 +33,24 @@ class JsonMetadataSchema(Schema): dataMask = fields.Mapping(required=False, allow_none=False) -class FilterSetPostSchema(Schema): +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.Nested(JsonMetadataSchema, required=True) + json_metadata = fields.String(allow_none=False, required=True) owner_type = fields.String( required=True, validate=OneOf([USER_OWNER_TYPE, DASHBOARD_OWNER_TYPE]) @@ -49,28 +61,33 @@ class FilterSetPostSchema(Schema): 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(Schema): +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.Nested(JsonMetadataSchema, required=False, allow_none=False) + 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( + 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) ) - - -class FilterSetMetadataSchema(Schema): - pass diff --git a/superset/models/filter_set.py b/superset/models/filter_set.py index 1cd7360f21156..2d3b218793dcf 100644 --- a/superset/models/filter_set.py +++ b/superset/models/filter_set.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import json import logging from typing import Any, Dict @@ -74,7 +75,7 @@ def to_dict(self) -> Dict[str, Any]: "id": self.id, "name": self.name, "description": self.description, - "json_metadata": self.json_metadata, + "params": self.params, "dashboard_id": self.dashboard_id, "owner_type": self.owner_type, "owner_id": self.owner_id, @@ -97,3 +98,9 @@ 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 {} diff --git a/tests/integration_tests/dashboards/filter_sets/conftest.py b/tests/integration_tests/dashboards/filter_sets/conftest.py index 807d6ccb023c8..2de7c4b0e5a4a 100644 --- a/tests/integration_tests/dashboards/filter_sets/conftest.py +++ b/tests/integration_tests/dashboards/filter_sets/conftest.py @@ -121,6 +121,10 @@ def build_user(username: str, filter_set_role: Role, admin_role: Role) -> User: 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 @@ -173,6 +177,7 @@ def dashboard() -> Generator[Dashboard, None, None]: yield dashboard except Exception as ex: print(str(ex)) + yield Dashboard.get(dashboard.slug) finally: with app.app_context() as ctx: session = ctx.app.appbuilder.get_session @@ -200,7 +205,7 @@ def dashboard_id(dashboard) -> int: @pytest.fixture def filtersets( - dashboard_id: int, test_users: Dict[str, int], valid_json_metadata: Dict[str, Any] + 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: @@ -208,27 +213,27 @@ def filtersets( first_filter_set = FilterSet( name="filter_set_1_of_" + str(dashboard_id), dashboard_id=dashboard_id, - json_metadata=json.dumps(valid_json_metadata), + 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=json.dumps(valid_json_metadata), + 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=json.dumps(valid_json_metadata), + 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=json.dumps(valid_json_metadata), + json_metadata=dumped_valid_json_metadata, dashboard_id=dashboard_id, owner_id=test_users[FILTER_SET_OWNER_USERNAME], owner_type="User", @@ -253,10 +258,15 @@ def filterset_id(filtersets: Dict[str, List[FilterSet]]) -> int: @pytest.fixture -def valid_json_metadata() -> Dict[Any, Any]: +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 @@ -264,13 +274,13 @@ def exists_user_id() -> int: @pytest.fixture def valid_filter_set_data_for_create( - dashboard_id: int, valid_json_metadata: Dict[Any, Any], exists_user_id: int + 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: valid_json_metadata, + JSON_METADATA_FIELD: dumped_valid_json_metadata, OWNER_TYPE_FIELD: USER_OWNER_TYPE, OWNER_ID_FIELD: exists_user_id, } @@ -278,13 +288,13 @@ def valid_filter_set_data_for_create( @pytest.fixture def valid_filter_set_data_for_update( - dashboard_id: int, valid_json_metadata: Dict[Any, Any], exists_user_id: int + 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: valid_json_metadata, + JSON_METADATA_FIELD: dumped_valid_json_metadata, } diff --git a/tests/integration_tests/dashboards/filter_sets/update_api_tests.py b/tests/integration_tests/dashboards/filter_sets/update_api_tests.py index f6557b80a0515..fcaeaab016c6c 100644 --- a/tests/integration_tests/dashboards/filter_sets/update_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/update_api_tests.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import json from typing import Any, Dict, List, TYPE_CHECKING from superset.dashboards.filter_sets.consts import ( @@ -23,6 +24,7 @@ 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 ( @@ -41,7 +43,14 @@ from superset.models.filter_set import FilterSet -def merge_two_filter_set_dict(first, second) -> Dict[Any, Any]: +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} @@ -293,7 +302,9 @@ def test_with_json_metadata__200( # arrange login(client, "admin") valid_json_metadata["nativeFilters"] = {"changed": "changed"} - valid_filter_set_data_for_update[JSON_METADATA_FIELD] = valid_json_metadata + valid_filter_set_data_for_update[JSON_METADATA_FIELD] = json.dumps( + valid_json_metadata + ) # act response = call_update_filter_set( From 4b0a9ede1a387cfa315c21e430bf32d3a75437b7 Mon Sep 17 00:00:00 2001 From: ofekisr Date: Tue, 14 Sep 2021 19:01:01 +0300 Subject: [PATCH 54/60] fix pylint --- superset/dashboards/filter_sets/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/dashboards/filter_sets/schemas.py b/superset/dashboards/filter_sets/schemas.py index 09435b12d390d..3c0436d697b23 100644 --- a/superset/dashboards/filter_sets/schemas.py +++ b/superset/dashboards/filter_sets/schemas.py @@ -78,7 +78,7 @@ class FilterSetPutSchema(FilterSetSchema): ) @post_load - def validate( + def validate( # pylint: disable=unused-argument self, data: Mapping[Any, Any], *, many: Any, partial: Any ) -> Dict[str, Any]: if JSON_METADATA_FIELD in data: From d1a59684a3c566bb7889b27fbd9a8f531bbae49e Mon Sep 17 00:00:00 2001 From: ofekisr <35701650+ofekisr@users.noreply.github.com> Date: Sun, 19 Sep 2021 11:37:26 +0300 Subject: [PATCH 55/60] Update conftest.py --- tests/integration_tests/dashboards/filter_sets/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration_tests/dashboards/filter_sets/conftest.py b/tests/integration_tests/dashboards/filter_sets/conftest.py index 2de7c4b0e5a4a..36642b194dbce 100644 --- a/tests/integration_tests/dashboards/filter_sets/conftest.py +++ b/tests/integration_tests/dashboards/filter_sets/conftest.py @@ -177,7 +177,6 @@ def dashboard() -> Generator[Dashboard, None, None]: yield dashboard except Exception as ex: print(str(ex)) - yield Dashboard.get(dashboard.slug) finally: with app.app_context() as ctx: session = ctx.app.appbuilder.get_session From e2bf8d706881dbc207081ce70d7c3dbcfe5b526d Mon Sep 17 00:00:00 2001 From: ofekisr Date: Sun, 19 Sep 2021 11:48:24 +0300 Subject: [PATCH 56/60] chore remove BaseCommand to remove abstraction --- superset/dashboards/filter_sets/commands/base.py | 5 +---- superset/dashboards/filter_sets/commands/create.py | 2 +- superset/dashboards/filter_sets/commands/delete.py | 2 +- superset/dashboards/filter_sets/commands/update.py | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index e6bb717ea771f..b18930e80901c 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -36,7 +36,7 @@ logger = logging.getLogger(__name__) -class BaseFilterSetCommand(BaseCommand): +class BaseFilterSetCommand: # pylint: disable=C0103 _dashboard: Dashboard _filter_set_id: Optional[int] @@ -50,9 +50,6 @@ def __init__(self, user: User, dashboard_id: int): def run(self) -> Model: pass - def validate(self) -> None: - self._validate_filterset_dashboard_exists() - def _validate_filterset_dashboard_exists(self) -> None: self._dashboard = DashboardDAO.get_by_id_or_slug(str(self._dashboard_id)) if not self._dashboard: diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index 4513403390ed2..dc6fab7e6c6a5 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -52,7 +52,7 @@ def run(self) -> Model: return filter_set def validate(self) -> None: - super().validate() + self.validate_exist_filter_use_cases_set() if self._properties[OWNER_TYPE_FIELD] == DASHBOARD_OWNER_TYPE: self._validate_owner_id_is_dashboard_id() self._validate_user_is_the_dashboard_owner() diff --git a/superset/dashboards/filter_sets/commands/delete.py b/superset/dashboards/filter_sets/commands/delete.py index cdd9a28c4315f..18d7fed8f2bb8 100644 --- a/superset/dashboards/filter_sets/commands/delete.py +++ b/superset/dashboards/filter_sets/commands/delete.py @@ -44,7 +44,7 @@ def run(self) -> Model: raise FilterSetDeleteFailedError(str(self._filter_set_id), "") from err def validate(self) -> None: - super().validate() + self._validate_filterset_dashboard_exists() try: self.validate_exist_filter_use_cases_set() except FilterSetNotFoundError as err: diff --git a/superset/dashboards/filter_sets/commands/update.py b/superset/dashboards/filter_sets/commands/update.py index 3346a16eb8676..d2c43f085212b 100644 --- a/superset/dashboards/filter_sets/commands/update.py +++ b/superset/dashboards/filter_sets/commands/update.py @@ -52,5 +52,5 @@ def run(self) -> Model: raise FilterSetUpdateFailedError(str(self._filter_set_id), "") from err def validate(self) -> None: - super().validate() + self._validate_filterset_dashboard_exists() self.validate_exist_filter_use_cases_set() From 2d63fe66fec1e86ec3dff979b34883b0c4d416ad Mon Sep 17 00:00:00 2001 From: ofekisr Date: Sun, 19 Sep 2021 12:00:56 +0300 Subject: [PATCH 57/60] chore remove BaseCommand to remove abstraction --- superset/dashboards/filter_sets/commands/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/superset/dashboards/filter_sets/commands/base.py b/superset/dashboards/filter_sets/commands/base.py index b18930e80901c..af31bbd7a1a94 100644 --- a/superset/dashboards/filter_sets/commands/base.py +++ b/superset/dashboards/filter_sets/commands/base.py @@ -20,7 +20,6 @@ from flask_appbuilder.models.sqla import Model from flask_appbuilder.security.sqla.models import User -from superset.commands.base import BaseCommand 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 2674d9a460060ef34614fadb11abd5f4fd6ac271 Mon Sep 17 00:00:00 2001 From: ofekisr Date: Sun, 19 Sep 2021 14:17:49 +0300 Subject: [PATCH 58/60] chore remove BaseCommand to remove abstraction --- superset/dashboards/filter_sets/commands/create.py | 2 +- .../dashboards/filter_sets/create_api_tests.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/superset/dashboards/filter_sets/commands/create.py b/superset/dashboards/filter_sets/commands/create.py index dc6fab7e6c6a5..b74e6d3041628 100644 --- a/superset/dashboards/filter_sets/commands/create.py +++ b/superset/dashboards/filter_sets/commands/create.py @@ -52,7 +52,7 @@ def run(self) -> Model: return filter_set def validate(self) -> None: - self.validate_exist_filter_use_cases_set() + 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() diff --git a/tests/integration_tests/dashboards/filter_sets/create_api_tests.py b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py index cbdaef9b95a01..85ec82c3b0e41 100644 --- a/tests/integration_tests/dashboards/filter_sets/create_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py @@ -50,7 +50,9 @@ def assert_filterset_was_not_created(filter_set_data: Dict[str, Any]) -> 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 +import pytest +@pytest.mark.ofek class TestCreateFilterSetsApi: def test_with_extra_field__400( self, From 414b0c054393a05e6682af5005822921f1ad2720 Mon Sep 17 00:00:00 2001 From: ofekisr Date: Sun, 19 Sep 2021 14:26:07 +0300 Subject: [PATCH 59/60] chore remove BaseCommand to remove abstraction --- .../dashboards/filter_sets/create_api_tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration_tests/dashboards/filter_sets/create_api_tests.py b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py index 85ec82c3b0e41..cbdaef9b95a01 100644 --- a/tests/integration_tests/dashboards/filter_sets/create_api_tests.py +++ b/tests/integration_tests/dashboards/filter_sets/create_api_tests.py @@ -50,9 +50,7 @@ def assert_filterset_was_not_created(filter_set_data: Dict[str, Any]) -> 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 -import pytest -@pytest.mark.ofek class TestCreateFilterSetsApi: def test_with_extra_field__400( self, From 51e2084380d67b502bf1c8f3f6018bc08803fbae Mon Sep 17 00:00:00 2001 From: ofekisr Date: Thu, 23 Sep 2021 10:07:40 +0300 Subject: [PATCH 60/60] chore fix migration --- superset/migrations/versions/3ebe0993c770_filterset_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/migrations/versions/3ebe0993c770_filterset_table.py b/superset/migrations/versions/3ebe0993c770_filterset_table.py index 4556ac6caf92f..b509f895a0386 100644 --- a/superset/migrations/versions/3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/3ebe0993c770_filterset_table.py @@ -24,7 +24,7 @@ # revision identifiers, used by Alembic. revision = "3ebe0993c770" -down_revision = "021b81fe4fbb" +down_revision = "181091c0ef16" import sqlalchemy as sa from alembic import op