From e5665879bd28e4f766df58ea6046c54dae7ceb5c Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Thu, 25 Aug 2016 17:56:07 +0900 Subject: [PATCH 1/8] add query fork to models.py --- redash/models.py | 25 +++++++++++++++++ tests/models/test_queries.py | 52 +++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/redash/models.py b/redash/models.py index 4dd77eacb..b95f02a30 100644 --- a/redash/models.py +++ b/redash/models.py @@ -837,6 +837,31 @@ class Query(ChangeTrackingMixin, ModelTimestampsMixin, BaseVersionedModel, Belon return query + @classmethod + def fork(cls, id, user, org): + query = cls.get_by_id_and_org(id, org) + forked_query = Query() + forked_query.name = 'Copy of (#{}) {}'.format(id, query.name) + forked_query.user = user + forked_list = ['org', 'data_source', 'latest_query_data', 'description', 'query', 'query_hash'] + for a in forked_list: + setattr(forked_query, a, getattr(query, a)) + forked_query.save() + + forked_visualizations = [] + for v in query.visualizations: + if v.type == 'TABLE': + continue + forked_v = v.to_dict() + forked_v['options'] = v.options + forked_v['query'] = forked_query + forked_v.pop('id') + forked_visualizations.append(forked_v) + + with db.database.atomic(): + Visualization.insert_many(forked_visualizations).execute() + return forked_query + def pre_save(self, created): super(Query, self).pre_save(created) self.query_hash = utils.gen_query_hash(self.query) diff --git a/tests/models/test_queries.py b/tests/models/test_queries.py index 16a9a13f6..2f941c0c0 100644 --- a/tests/models/test_queries.py +++ b/tests/models/test_queries.py @@ -1,5 +1,55 @@ from tests import BaseTestCase +from redash.models import Query -# Add tests for change tracking +class TestApiKeyGetByObject(BaseTestCase): + def assert_visualizations(self, origin_q, origin_v, forked_q, forked_v): + self.assertEqual(origin_v.options, forked_v.options) + self.assertEqual(origin_v.type, forked_v.type) + self.assertNotEqual(origin_v.id, forked_v.id) + self.assertNotEqual(origin_v.query, forked_v.query) + self.assertEqual(forked_q.id, forked_v.query.id) + + + def test_returns_none_if_not_exists(self): + # prepare original query and visualizations + data_source = self.factory.create_data_source(group=self.factory.create_group()) + query = self.factory.create_query(data_source=data_source, description="this is description") + visualization_chart = self.factory.create_visualization(query=query, description="chart vis", type="CHART", options="""{"yAxis": [{"type": "linear"}, {"type": "linear", "opposite": true}], "series": {"stacking": null}, "globalSeriesType": "line", "sortX": true, "seriesOptions": {"count": {"zIndex": 0, "index": 0, "type": "line", "yAxis": 0}}, "xAxis": {"labels": {"enabled": true}, "type": "datetime"}, "columnMapping": {"count": "y", "created_at": "x"}, "bottomMargin": 50, "legend": {"enabled": true}}""") + visualization_box = self.factory.create_visualization(query=query, description="box vis", type="BOXPLOT", options="{}") + fork_user = self.factory.create_user() + + + forked_query = Query.fork(query.id, fork_user, self.factory.org) + + + forked_visualization_chart = None + forked_visualization_box = None + forked_table = None + count_table = 0 + for v in forked_query.visualizations: + if v.description == "chart vis": + forked_visualization_chart = v + if v.description == "box vis": + forked_visualization_box = v + if v.type == "TABLE": + count_table += 1 + forked_table = v + self.assert_visualizations(query, visualization_chart, forked_query, forked_visualization_chart) + self.assert_visualizations(query, visualization_box, forked_query, forked_visualization_box) + + self.assertEqual(forked_query.org, query.org) + self.assertEqual(forked_query.data_source, query.data_source) + self.assertEqual(forked_query.latest_query_data, query.latest_query_data) + self.assertEqual(forked_query.description, query.description) + self.assertEqual(forked_query.query, query.query) + self.assertEqual(forked_query.query_hash, query.query_hash) + self.assertEqual(forked_query.user, fork_user) + self.assertEqual(forked_query.description, query.description) + self.assertTrue(forked_query.name.startswith('Copy')) + self.assertEqual(count_table, 1) + # num of TABLE must be 1. default table only + self.assertEqual(forked_table.name, "Table") + self.assertEqual(forked_table.description, "") + self.assertEqual(forked_table.options, "{}") From 3216e67b41e064a49d6faa55c8bd7d4959745f5f Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Wed, 22 Jun 2016 23:49:10 +0900 Subject: [PATCH 2/8] add query fork api --- redash/handlers/api.py | 3 ++- redash/handlers/queries.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/redash/handlers/api.py b/redash/handlers/api.py index 7658e2687..2db4f63c7 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -9,7 +9,7 @@ from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscr from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource from redash.handlers.events import EventResource -from redash.handlers.queries import QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource +from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource from redash.handlers.query_results import QueryResultListResource, QueryResultResource, JobResource from redash.handlers.users import UserResource, UserListResource, UserInviteResource, UserResetPasswordResource from redash.handlers.visualizations import VisualizationListResource @@ -71,6 +71,7 @@ api.add_org_resource(QueryListResource, '/api/queries', endpoint='queries') api.add_org_resource(MyQueriesResource, '/api/queries/my', endpoint='my_queries') api.add_org_resource(QueryRefreshResource, '/api/queries//refresh', endpoint='query_refresh') api.add_org_resource(QueryResource, '/api/queries/', endpoint='query') +api.add_org_resource(QueryForkResource, '/api/queries//fork', endpoint='query_fork') api.add_org_resource(ObjectPermissionsListResource, '/api///acl', endpoint='object_permissions') api.add_org_resource(CheckPermissionResource, '/api///acl/', endpoint='check_permissions') diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py index 4879e63f0..049ada5bb 100644 --- a/redash/handlers/queries.py +++ b/redash/handlers/queries.py @@ -138,6 +138,13 @@ class QueryResource(BaseResource): query.archive(self.current_user) +class QueryForkResource(BaseResource): + @require_permission('edit_query') + def post(self, query_id): + query = get_object_or_404(models.Query.fork, query_id, self.current_user, self.current_org) + return query.to_dict(with_visualizations=True) + + class QueryRefreshResource(BaseResource): def post(self, query_id): query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) From ad3d01280a6c3a5123abd7c599b0e3a16cc20415 Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Thu, 25 Aug 2016 18:59:10 +0900 Subject: [PATCH 3/8] modify front scripts to use query fork api --- rd_ui/app/scripts/controllers/query_source.js | 27 +++++++++++++++---- rd_ui/app/scripts/controllers/query_view.js | 10 ++++++- rd_ui/app/scripts/services/resources.js | 6 +++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/rd_ui/app/scripts/controllers/query_source.js b/rd_ui/app/scripts/controllers/query_source.js index d005363f2..e9dc04ba0 100644 --- a/rd_ui/app/scripts/controllers/query_source.js +++ b/rd_ui/app/scripts/controllers/query_source.js @@ -14,7 +14,8 @@ var isNewQuery = !$scope.query.id, queryText = $scope.query.query, // ref to QueryViewCtrl.saveQuery - saveQuery = $scope.saveQuery; + saveQuery = $scope.saveQuery, + forkQuery = $scope.forkQuery; $scope.sourceMode = true; $scope.canEdit = currentUser.canEdit($scope.query) || $scope.query.can_edit;// TODO: bring this back? || clientConfig.allowAllToEditQueries; @@ -77,12 +78,28 @@ return savePromise; }; + $scope.forkQuery = function(options, data) { + var savePromise = forkQuery(options, data); + + if (!savePromise) { + return; + } + + savePromise.then(function(savedQuery) { + queryText = savedQuery.query; + + if (isNewQuery) { + // redirect to new created query (keep hash) + $location.path(savedQuery.getSourceLink()); + } + }); + + return savePromise; + }; + $scope.duplicateQuery = function() { Events.record(currentUser, 'fork', 'query', $scope.query.id); - $scope.query.name = 'Copy of (#'+$scope.query.id+') '+$scope.query.name; - $scope.query.id = null; - $scope.query.schedule = null; - $scope.saveQuery({ + $scope.forkQuery({ successMessage: 'Query forked', errorMessage: 'Query could not be forked' }).then(function redirect(savedQuery) { diff --git a/rd_ui/app/scripts/controllers/query_view.js b/rd_ui/app/scripts/controllers/query_view.js index 6182bde93..7c60fe8be 100644 --- a/rd_ui/app/scripts/controllers/query_view.js +++ b/rd_ui/app/scripts/controllers/query_view.js @@ -151,7 +151,15 @@ growl.addErrorMessage(options.errorMessage); } }).$promise; - } + }; + + $scope.forkQuery = function(options, data) { + return Query.fork({id:$scope.query.id}, function() { + growl.addSuccessMessage(options.successMessage); + }, function(httpResponse) { + growl.addErrorMessage(options.errorMessage); + }).$promise; + }; $scope.saveDescription = function() { Events.record(currentUser, 'edit_description', 'query', $scope.query.id); diff --git a/rd_ui/app/scripts/services/resources.js b/rd_ui/app/scripts/services/resources.js index e6a9bc614..7606bdba7 100644 --- a/rd_ui/app/scripts/services/resources.js +++ b/rd_ui/app/scripts/services/resources.js @@ -454,6 +454,12 @@ method: 'get', isArray: false, url: "api/queries/my" + }, + fork: { + method: 'post', + isArray: false, + url: "api/queries/:id/fork", + params: {id: '@id'} } }); From 1d486938c1962bea83c907a418f7306f10b7cd33 Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Thu, 25 Aug 2016 20:19:24 +0900 Subject: [PATCH 4/8] fix bugs --- redash/models.py | 7 ++++--- tests/models/test_queries.py | 27 +++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/redash/models.py b/redash/models.py index b95f02a30..14f435b2b 100644 --- a/redash/models.py +++ b/redash/models.py @@ -857,9 +857,10 @@ class Query(ChangeTrackingMixin, ModelTimestampsMixin, BaseVersionedModel, Belon forked_v['query'] = forked_query forked_v.pop('id') forked_visualizations.append(forked_v) - - with db.database.atomic(): - Visualization.insert_many(forked_visualizations).execute() + + if len(forked_visualizations) > 0: + with db.database.atomic(): + Visualization.insert_many(forked_visualizations).execute() return forked_query def pre_save(self, created): diff --git a/tests/models/test_queries.py b/tests/models/test_queries.py index 2f941c0c0..085bf7db4 100644 --- a/tests/models/test_queries.py +++ b/tests/models/test_queries.py @@ -12,7 +12,7 @@ class TestApiKeyGetByObject(BaseTestCase): self.assertEqual(forked_q.id, forked_v.query.id) - def test_returns_none_if_not_exists(self): + def test_fork_with_visualizations(self): # prepare original query and visualizations data_source = self.factory.create_data_source(group=self.factory.create_group()) query = self.factory.create_query(data_source=data_source, description="this is description") @@ -20,10 +20,8 @@ class TestApiKeyGetByObject(BaseTestCase): visualization_box = self.factory.create_visualization(query=query, description="box vis", type="BOXPLOT", options="{}") fork_user = self.factory.create_user() - forked_query = Query.fork(query.id, fork_user, self.factory.org) - forked_visualization_chart = None forked_visualization_box = None forked_table = None @@ -48,8 +46,29 @@ class TestApiKeyGetByObject(BaseTestCase): self.assertEqual(forked_query.user, fork_user) self.assertEqual(forked_query.description, query.description) self.assertTrue(forked_query.name.startswith('Copy')) - self.assertEqual(count_table, 1) # num of TABLE must be 1. default table only + self.assertEqual(count_table, 1) self.assertEqual(forked_table.name, "Table") self.assertEqual(forked_table.description, "") self.assertEqual(forked_table.options, "{}") +<<<<<<< HEAD +======= + + def test_fork_from_query_that_has_no_visualization(self): + # prepare original query and visualizations + data_source = self.factory.create_data_source(group=self.factory.create_group()) + query = self.factory.create_query(data_source=data_source, description="this is description") + fork_user = self.factory.create_user() + + forked_query = Query.fork(query.id, fork_user, self.factory.org) + + count_table = 0 + count_vis = 0 + for v in forked_query.visualizations: + count_vis += 1 + if v.type == "TABLE": + count_table += 1 + + self.assertEqual(count_table, 1) + self.assertEqual(count_vis, 1) +>>>>>>> fix bugs From 519964a179d6a16eb112b9e73d6c38fb12281e02 Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Fri, 16 Sep 2016 18:57:59 +0900 Subject: [PATCH 5/8] delete unused code --- rd_ui/app/scripts/controllers/query_source.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rd_ui/app/scripts/controllers/query_source.js b/rd_ui/app/scripts/controllers/query_source.js index e9dc04ba0..30fc90dbc 100644 --- a/rd_ui/app/scripts/controllers/query_source.js +++ b/rd_ui/app/scripts/controllers/query_source.js @@ -87,11 +87,6 @@ savePromise.then(function(savedQuery) { queryText = savedQuery.query; - - if (isNewQuery) { - // redirect to new created query (keep hash) - $location.path(savedQuery.getSourceLink()); - } }); return savePromise; From 7bc71c9cb504c20a24f4d31700349bc14f8713ef Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Mon, 3 Oct 2016 02:07:20 +0900 Subject: [PATCH 6/8] apply review. --- redash/handlers/queries.py | 5 +++-- redash/models.py | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py index 049ada5bb..5dae9cc9e 100644 --- a/redash/handlers/queries.py +++ b/redash/handlers/queries.py @@ -141,8 +141,9 @@ class QueryResource(BaseResource): class QueryForkResource(BaseResource): @require_permission('edit_query') def post(self, query_id): - query = get_object_or_404(models.Query.fork, query_id, self.current_user, self.current_org) - return query.to_dict(with_visualizations=True) + query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) + forked_query = query.fork(self.current_user) + return forked_query.to_dict(with_visualizations=True) class QueryRefreshResource(BaseResource): diff --git a/redash/models.py b/redash/models.py index 14f435b2b..7a4aa43e0 100644 --- a/redash/models.py +++ b/redash/models.py @@ -837,11 +837,10 @@ class Query(ChangeTrackingMixin, ModelTimestampsMixin, BaseVersionedModel, Belon return query - @classmethod - def fork(cls, id, user, org): - query = cls.get_by_id_and_org(id, org) + def fork(self, user): + query = self forked_query = Query() - forked_query.name = 'Copy of (#{}) {}'.format(id, query.name) + forked_query.name = 'Copy of (#{}) {}'.format(query.id, query.name) forked_query.user = user forked_list = ['org', 'data_source', 'latest_query_data', 'description', 'query', 'query_hash'] for a in forked_list: From 1ad0c9c75f0c497465b466907f7851b34c599ae7 Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Wed, 9 Nov 2016 00:37:16 +0900 Subject: [PATCH 7/8] fix errors --- tests/models/test_queries.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/models/test_queries.py b/tests/models/test_queries.py index 085bf7db4..19c24b80b 100644 --- a/tests/models/test_queries.py +++ b/tests/models/test_queries.py @@ -51,8 +51,6 @@ class TestApiKeyGetByObject(BaseTestCase): self.assertEqual(forked_table.name, "Table") self.assertEqual(forked_table.description, "") self.assertEqual(forked_table.options, "{}") -<<<<<<< HEAD -======= def test_fork_from_query_that_has_no_visualization(self): # prepare original query and visualizations @@ -71,4 +69,4 @@ class TestApiKeyGetByObject(BaseTestCase): self.assertEqual(count_table, 1) self.assertEqual(count_vis, 1) ->>>>>>> fix bugs + From 8c78252ea257c1ddcc5cdd3509190c2f84341358 Mon Sep 17 00:00:00 2001 From: "yohei.naruse" Date: Fri, 11 Nov 2016 02:21:30 +0900 Subject: [PATCH 8/8] fix tests --- tests/models/test_queries.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/models/test_queries.py b/tests/models/test_queries.py index 19c24b80b..a6c78c9cb 100644 --- a/tests/models/test_queries.py +++ b/tests/models/test_queries.py @@ -19,8 +19,9 @@ class TestApiKeyGetByObject(BaseTestCase): visualization_chart = self.factory.create_visualization(query=query, description="chart vis", type="CHART", options="""{"yAxis": [{"type": "linear"}, {"type": "linear", "opposite": true}], "series": {"stacking": null}, "globalSeriesType": "line", "sortX": true, "seriesOptions": {"count": {"zIndex": 0, "index": 0, "type": "line", "yAxis": 0}}, "xAxis": {"labels": {"enabled": true}, "type": "datetime"}, "columnMapping": {"count": "y", "created_at": "x"}, "bottomMargin": 50, "legend": {"enabled": true}}""") visualization_box = self.factory.create_visualization(query=query, description="box vis", type="BOXPLOT", options="{}") fork_user = self.factory.create_user() + + forked_query = query.fork(fork_user) - forked_query = Query.fork(query.id, fork_user, self.factory.org) forked_visualization_chart = None forked_visualization_box = None @@ -58,7 +59,7 @@ class TestApiKeyGetByObject(BaseTestCase): query = self.factory.create_query(data_source=data_source, description="this is description") fork_user = self.factory.create_user() - forked_query = Query.fork(query.id, fork_user, self.factory.org) + forked_query = query.fork(fork_user) count_table = 0 count_vis = 0