From 2d93ef3041e4b7facb8163efea41c0ba873fbb8a Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Wed, 1 Apr 2020 13:05:51 +0100 Subject: [PATCH 1/6] [dashboards] New, tittle and slug OR filter --- requirements.txt | 2 +- setup.py | 2 +- superset/dashboards/api.py | 3 ++- superset/dashboards/filters.py | 14 ++++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 578e7ed5761e6..86192c9c09a25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ croniter==0.3.31 # via apache-superset (setup.py) cryptography==2.8 # via apache-superset (setup.py) decorator==4.4.1 # via retry defusedxml==0.6.0 # via python3-openid -flask-appbuilder==2.3.1 # via apache-superset (setup.py) +flask-appbuilder==2.3.2rc1 # via apache-superset (setup.py) flask-babel==1.0.0 # via flask-appbuilder flask-caching==1.8.0 flask-compress==1.4.0 diff --git a/setup.py b/setup.py index e620ab2ac2348..12272ba37ea7b 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,7 @@ def get_git_sha(): "croniter>=0.3.28", "cryptography>=2.4.2", "flask>=1.1.0, <2.0.0", - "flask-appbuilder>=2.3.1, <2.4.0", + "flask-appbuilder==2.3.2rc1", "flask-caching", "flask-compress", "flask-talisman", diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index e960c4cbd2b48..caecd16866a41 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -35,7 +35,7 @@ DashboardUpdateFailedError, ) from superset.dashboards.commands.update import UpdateDashboardCommand -from superset.dashboards.filters import DashboardFilter +from superset.dashboards.filters import DashboardFilter, DashboardTitleOrSlugFilter from superset.dashboards.schemas import ( DashboardPostSchema, DashboardPutSchema, @@ -100,6 +100,7 @@ class DashboardRestApi(BaseSupersetModelRestApi): "published", ] search_columns = ("dashboard_title", "slug", "owners", "published") + search_filters = {"dashboard_title": [DashboardTitleOrSlugFilter]} add_columns = edit_columns base_order = ("changed_on", "desc") diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py index 0b4338dfdbad5..bdf011e24daf9 100644 --- a/superset/dashboards/filters.py +++ b/superset/dashboards/filters.py @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from flask_babel import lazy_gettext as _ from sqlalchemy import and_, or_ from superset import db, security_manager @@ -23,6 +24,19 @@ from superset.views.base import BaseFilter, get_user_roles +class DashboardTitleOrSlugFilter(BaseFilter): # pylint: disable=too-few-public-methods + name = _("Title or Slug") + arg_name = "title_or_slug" + + def apply(self, query, value): + return query.filter( + or_( + Dashboard.dashboard_title.ilike(value + "%"), + Dashboard.slug.ilike(value + "%"), + ) + ) + + class DashboardFilter(BaseFilter): # pylint: disable=too-few-public-methods """ List dashboards with the following criteria: From 17885f9a0b7797e21aa5503ae3be64f86571b231 Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Wed, 1 Apr 2020 13:18:40 +0100 Subject: [PATCH 2/6] Update requirements, because of prison bump --- requirements.txt | 69 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/requirements.txt b/requirements.txt index 86192c9c09a25..379a4eaed22e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,10 +9,10 @@ amqp==2.5.2 # via kombu apispec[yaml]==1.3.3 # via flask-appbuilder attrs==19.3.0 # via jsonschema babel==2.8.0 # via flask-babel -backoff==1.10.0 +backoff==1.10.0 # via apache-superset (setup.py) billiard==3.6.1.0 # via celery -bleach==3.1.0 -celery==4.4.0 +bleach==3.1.0 # via apache-superset (setup.py) +celery==4.4.0 # via apache-superset (setup.py) cffi==1.13.2 # via cryptography click==7.1.1 # via apache-superset (setup.py), flask, flask-appbuilder colorama==0.4.3 # via apache-superset (setup.py), flask-appbuilder @@ -21,65 +21,68 @@ croniter==0.3.31 # via apache-superset (setup.py) cryptography==2.8 # via apache-superset (setup.py) decorator==4.4.1 # via retry defusedxml==0.6.0 # via python3-openid -flask-appbuilder==2.3.2rc1 # via apache-superset (setup.py) +flask-appbuilder==2.3.2rc1 # via apache-superset (setup.py) flask-babel==1.0.0 # via flask-appbuilder -flask-caching==1.8.0 -flask-compress==1.4.0 +flask-caching==1.8.0 # via apache-superset (setup.py) +flask-compress==1.4.0 # via apache-superset (setup.py) flask-jwt-extended==3.24.1 # via flask-appbuilder flask-login==0.4.1 # via flask-appbuilder -flask-migrate==2.5.2 +flask-migrate==2.5.2 # via apache-superset (setup.py) flask-openid==1.2.5 # via flask-appbuilder flask-sqlalchemy==2.4.1 # via flask-appbuilder, flask-migrate -flask-talisman==0.7.0 -flask-wtf==0.14.2 -flask==1.1.1 +flask-talisman==0.7.0 # via apache-superset (setup.py) +flask-wtf==0.14.2 # via apache-superset (setup.py), flask-appbuilder +flask==1.1.1 # via apache-superset (setup.py), flask-appbuilder, flask-babel, flask-caching, flask-compress, flask-jwt-extended, flask-login, flask-migrate, flask-openid, flask-sqlalchemy, flask-wtf geographiclib==1.50 # via geopy -geopy==1.20.0 -gunicorn==20.0.4 -humanize==0.5.1 +geopy==1.20.0 # via apache-superset (setup.py) +gunicorn==20.0.4 # via apache-superset (setup.py) +humanize==0.5.1 # via apache-superset (setup.py) importlib-metadata==1.4.0 # via jsonschema, kombu -isodate==0.6.0 +isodate==0.6.0 # via apache-superset (setup.py) itsdangerous==1.1.0 # via flask jinja2==2.10.3 # via flask, flask-babel jsonschema==3.2.0 # via flask-appbuilder kombu==4.6.7 # via celery mako==1.1.1 # via alembic -markdown==3.1.1 +markdown==3.1.1 # via apache-superset (setup.py) markupsafe==1.1.1 # via jinja2, mako marshmallow-enum==1.5.1 # via flask-appbuilder marshmallow-sqlalchemy==0.21.0 # via flask-appbuilder marshmallow==2.19.5 # via flask-appbuilder, marshmallow-enum, marshmallow-sqlalchemy more-itertools==8.1.0 # via zipp -msgpack==0.6.2 +msgpack==0.6.2 # via apache-superset (setup.py) numpy==1.18.1 # via pandas, pyarrow -pandas==0.25.3 -parsedatetime==2.5 -pathlib2==2.3.5 -polyline==1.4.0 -prison==0.1.2 # via flask-appbuilder +pandas==0.25.3 # via apache-superset (setup.py) +parsedatetime==2.5 # via apache-superset (setup.py) +pathlib2==2.3.5 # via apache-superset (setup.py) +polyline==1.4.0 # via apache-superset (setup.py) +prison==0.1.3 # via flask-appbuilder py==1.8.1 # via retry -pyarrow==0.16.0 +pyarrow==0.16.0 # via apache-superset (setup.py) pycparser==2.19 # via cffi pyjwt==1.7.1 # via flask-appbuilder, flask-jwt-extended pyrsistent==0.15.7 # via jsonschema -python-dateutil==2.8.1 -python-dotenv==0.10.5 +python-dateutil==2.8.1 # via alembic, apache-superset (setup.py), croniter, flask-appbuilder, pandas +python-dotenv==0.10.5 # via apache-superset (setup.py) python-editor==1.0.4 # via alembic -python-geohash==0.8.5 +python-geohash==0.8.5 # via apache-superset (setup.py) python3-openid==3.1.0 # via flask-openid pytz==2019.3 # via babel, celery, flask-babel, pandas -pyyaml==5.3 -retry==0.9.2 -selenium==3.141.0 -simplejson==3.17.0 +pyyaml==5.3 # via apache-superset (setup.py), apispec +retry==0.9.2 # via apache-superset (setup.py) +selenium==3.141.0 # via apache-superset (setup.py) +simplejson==3.17.0 # via apache-superset (setup.py) six==1.14.0 # via bleach, cryptography, flask-jwt-extended, flask-talisman, isodate, jsonschema, pathlib2, polyline, prison, pyarrow, pyrsistent, python-dateutil, sqlalchemy-utils, wtforms-json -sqlalchemy-utils==0.36.1 -sqlalchemy==1.3.12 -sqlparse==0.3.0 +sqlalchemy-utils==0.36.1 # via apache-superset (setup.py), flask-appbuilder +sqlalchemy==1.3.12 # via alembic, apache-superset (setup.py), flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils +sqlparse==0.3.0 # via apache-superset (setup.py) urllib3==1.25.8 # via selenium vine==1.3.0 # via amqp, celery webencodings==0.5.1 # via bleach werkzeug==0.16.0 # via flask, flask-jwt-extended -wtforms-json==0.3.3 +wtforms-json==0.3.3 # via apache-superset (setup.py) wtforms==2.2.1 # via flask-wtf, wtforms-json zipp==2.0.0 # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools From 620c9932bfcff8b2281a509671231e11daee363a Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Mon, 6 Apr 2020 11:09:32 +0100 Subject: [PATCH 3/6] Tests --- superset/dashboards/filters.py | 5 +++-- tests/dashboards/api_tests.py | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py index bdf011e24daf9..e0366c7ab4865 100644 --- a/superset/dashboards/filters.py +++ b/superset/dashboards/filters.py @@ -29,10 +29,11 @@ class DashboardTitleOrSlugFilter(BaseFilter): # pylint: disable=too-few-public- arg_name = "title_or_slug" def apply(self, query, value): + ilike_value = f"%{value}%" return query.filter( or_( - Dashboard.dashboard_title.ilike(value + "%"), - Dashboard.slug.ilike(value + "%"), + Dashboard.dashboard_title.ilike(ilike_value), + Dashboard.slug.ilike(ilike_value), ) ) diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py index 45b588f04cc92..2b9ecb767db16 100644 --- a/tests/dashboards/api_tests.py +++ b/tests/dashboards/api_tests.py @@ -172,6 +172,42 @@ def test_get_dashboards_filter(self): db.session.delete(dashboard) db.session.commit() + def test_get_dashboards_custom_filter(self): + """ + Dashboard API: Test get dashboards custom filter + """ + admin = self.get_user("admin") + dashboard1 = self.insert_dashboard("foo", "ZY_bar", [admin.id]) + dashboard2 = self.insert_dashboard("zy_foo", "slug1", [admin.id]) + dashboard2 = self.insert_dashboard("foo", "slug1zy_", [admin.id]) + dashboard3 = self.insert_dashboard("bar", "foo", [admin.id]) + + arguments = { + "filters": [ + {"col": "dashboard_title", "opr": "title_or_slug", "value": "zy_"} + ] + } + self.login(username="admin") + uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual(data["count"], 3) + + self.logout() + self.login(username="gamma") + uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual(data["count"], 0) + + # rollback changes + db.session.delete(dashboard1) + db.session.delete(dashboard2) + db.session.delete(dashboard3) + db.session.commit() + def test_get_dashboards_no_data_access(self): """ Dashboard API: Test get dashboards no data access From 46d2004c2f39f7382165be9e1f61fde739a8a5b2 Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Mon, 6 Apr 2020 18:09:48 +0100 Subject: [PATCH 4/6] Fix tests --- tests/dashboards/api_tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py index 2b9ecb767db16..d563fbb50381b 100644 --- a/tests/dashboards/api_tests.py +++ b/tests/dashboards/api_tests.py @@ -179,8 +179,8 @@ def test_get_dashboards_custom_filter(self): admin = self.get_user("admin") dashboard1 = self.insert_dashboard("foo", "ZY_bar", [admin.id]) dashboard2 = self.insert_dashboard("zy_foo", "slug1", [admin.id]) - dashboard2 = self.insert_dashboard("foo", "slug1zy_", [admin.id]) - dashboard3 = self.insert_dashboard("bar", "foo", [admin.id]) + dashboard3 = self.insert_dashboard("foo", "slug1zy_", [admin.id]) + dashboard4 = self.insert_dashboard("bar", "foo", [admin.id]) arguments = { "filters": [ @@ -206,6 +206,7 @@ def test_get_dashboards_custom_filter(self): db.session.delete(dashboard1) db.session.delete(dashboard2) db.session.delete(dashboard3) + db.session.delete(dashboard4) db.session.commit() def test_get_dashboards_no_data_access(self): From db93b2b1e38ebabc0e2c547eb35491dc01ced259 Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Tue, 7 Apr 2020 10:39:00 +0100 Subject: [PATCH 5/6] Avoid like filter on empty string value --- superset/dashboards/filters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py index e0366c7ab4865..6b1e2fbd6386f 100644 --- a/superset/dashboards/filters.py +++ b/superset/dashboards/filters.py @@ -29,6 +29,8 @@ class DashboardTitleOrSlugFilter(BaseFilter): # pylint: disable=too-few-public- arg_name = "title_or_slug" def apply(self, query, value): + if not value: + return query ilike_value = f"%{value}%" return query.filter( or_( From c47fdbbb73016cd72b2c7a5296c3a99b2e0f0ca8 Mon Sep 17 00:00:00 2001 From: Daniel Gaspar Date: Thu, 9 Apr 2020 10:46:42 +0100 Subject: [PATCH 6/6] merge master brings strict typing to the table --- superset/dashboards/filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py index 12dfa2d4179e9..05a6f6e4a844a 100644 --- a/superset/dashboards/filters.py +++ b/superset/dashboards/filters.py @@ -14,9 +14,9 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from flask_babel import lazy_gettext as _ from typing import Any +from flask_babel import lazy_gettext as _ from sqlalchemy import and_, or_ from sqlalchemy.orm.query import Query @@ -31,7 +31,7 @@ class DashboardTitleOrSlugFilter(BaseFilter): # pylint: disable=too-few-public- name = _("Title or Slug") arg_name = "title_or_slug" - def apply(self, query, value): + def apply(self, query: Query, value: Any) -> Query: if not value: return query ilike_value = f"%{value}%"