Skip to content

Commit 9f68dee

Browse files
authored
Merge pull request #1256 from ninneko/fork_with_vis
Change: when forking a query, copy all visualizations
2 parents 0947491 + 8c78252 commit 9f68dee

File tree

7 files changed

+136
-8
lines changed

7 files changed

+136
-8
lines changed

rd_ui/app/scripts/controllers/query_source.js

+17-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
var isNewQuery = !$scope.query.id,
1515
queryText = $scope.query.query,
1616
// ref to QueryViewCtrl.saveQuery
17-
saveQuery = $scope.saveQuery;
17+
saveQuery = $scope.saveQuery,
18+
forkQuery = $scope.forkQuery;
1819

1920
$scope.sourceMode = true;
2021
$scope.canEdit = currentUser.canEdit($scope.query) || $scope.query.can_edit;// TODO: bring this back? || clientConfig.allowAllToEditQueries;
@@ -77,12 +78,23 @@
7778
return savePromise;
7879
};
7980

81+
$scope.forkQuery = function(options, data) {
82+
var savePromise = forkQuery(options, data);
83+
84+
if (!savePromise) {
85+
return;
86+
}
87+
88+
savePromise.then(function(savedQuery) {
89+
queryText = savedQuery.query;
90+
});
91+
92+
return savePromise;
93+
};
94+
8095
$scope.duplicateQuery = function() {
8196
Events.record(currentUser, 'fork', 'query', $scope.query.id);
82-
$scope.query.name = 'Copy of (#'+$scope.query.id+') '+$scope.query.name;
83-
$scope.query.id = null;
84-
$scope.query.schedule = null;
85-
$scope.saveQuery({
97+
$scope.forkQuery({
8698
successMessage: 'Query forked',
8799
errorMessage: 'Query could not be forked'
88100
}).then(function redirect(savedQuery) {

rd_ui/app/scripts/controllers/query_view.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,15 @@
151151
growl.addErrorMessage(options.errorMessage);
152152
}
153153
}).$promise;
154-
}
154+
};
155+
156+
$scope.forkQuery = function(options, data) {
157+
return Query.fork({id:$scope.query.id}, function() {
158+
growl.addSuccessMessage(options.successMessage);
159+
}, function(httpResponse) {
160+
growl.addErrorMessage(options.errorMessage);
161+
}).$promise;
162+
};
155163

156164
$scope.saveDescription = function() {
157165
Events.record(currentUser, 'edit_description', 'query', $scope.query.id);

rd_ui/app/scripts/services/resources.js

+6
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,12 @@
454454
method: 'get',
455455
isArray: false,
456456
url: "api/queries/my"
457+
},
458+
fork: {
459+
method: 'post',
460+
isArray: false,
461+
url: "api/queries/:id/fork",
462+
params: {id: '@id'}
457463
}
458464
});
459465

redash/handlers/api.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource
1010
from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource
1111
from redash.handlers.events import EventResource
12-
from redash.handlers.queries import QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource
12+
from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource
1313
from redash.handlers.query_results import QueryResultListResource, QueryResultResource, JobResource
1414
from redash.handlers.users import UserResource, UserListResource, UserInviteResource, UserResetPasswordResource
1515
from redash.handlers.visualizations import VisualizationListResource
@@ -71,6 +71,7 @@ def json_representation(data, code, headers=None):
7171
api.add_org_resource(MyQueriesResource, '/api/queries/my', endpoint='my_queries')
7272
api.add_org_resource(QueryRefreshResource, '/api/queries/<query_id>/refresh', endpoint='query_refresh')
7373
api.add_org_resource(QueryResource, '/api/queries/<query_id>', endpoint='query')
74+
api.add_org_resource(QueryForkResource, '/api/queries/<query_id>/fork', endpoint='query_fork')
7475

7576
api.add_org_resource(ObjectPermissionsListResource, '/api/<object_type>/<object_id>/acl', endpoint='object_permissions')
7677
api.add_org_resource(CheckPermissionResource, '/api/<object_type>/<object_id>/acl/<access_type>', endpoint='check_permissions')

redash/handlers/queries.py

+8
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ def delete(self, query_id):
139139
query.archive(self.current_user)
140140

141141

142+
class QueryForkResource(BaseResource):
143+
@require_permission('edit_query')
144+
def post(self, query_id):
145+
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
146+
forked_query = query.fork(self.current_user)
147+
return forked_query.to_dict(with_visualizations=True)
148+
149+
142150
class QueryRefreshResource(BaseResource):
143151
def post(self, query_id):
144152
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)

redash/models.py

+25
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,31 @@ def recent(cls, groups, user_id=None, limit=20):
843843

844844
return query
845845

846+
def fork(self, user):
847+
query = self
848+
forked_query = Query()
849+
forked_query.name = 'Copy of (#{}) {}'.format(query.id, query.name)
850+
forked_query.user = user
851+
forked_list = ['org', 'data_source', 'latest_query_data', 'description', 'query', 'query_hash']
852+
for a in forked_list:
853+
setattr(forked_query, a, getattr(query, a))
854+
forked_query.save()
855+
856+
forked_visualizations = []
857+
for v in query.visualizations:
858+
if v.type == 'TABLE':
859+
continue
860+
forked_v = v.to_dict()
861+
forked_v['options'] = v.options
862+
forked_v['query'] = forked_query
863+
forked_v.pop('id')
864+
forked_visualizations.append(forked_v)
865+
866+
if len(forked_visualizations) > 0:
867+
with db.database.atomic():
868+
Visualization.insert_many(forked_visualizations).execute()
869+
return forked_query
870+
846871
def pre_save(self, created):
847872
super(Query, self).pre_save(created)
848873
self.query_hash = utils.gen_query_hash(self.query)

tests/models/test_queries.py

+69-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,73 @@
11
from tests import BaseTestCase
2+
from redash.models import Query
23

34

4-
# Add tests for change tracking
5+
class TestApiKeyGetByObject(BaseTestCase):
6+
7+
def assert_visualizations(self, origin_q, origin_v, forked_q, forked_v):
8+
self.assertEqual(origin_v.options, forked_v.options)
9+
self.assertEqual(origin_v.type, forked_v.type)
10+
self.assertNotEqual(origin_v.id, forked_v.id)
11+
self.assertNotEqual(origin_v.query, forked_v.query)
12+
self.assertEqual(forked_q.id, forked_v.query.id)
13+
14+
15+
def test_fork_with_visualizations(self):
16+
# prepare original query and visualizations
17+
data_source = self.factory.create_data_source(group=self.factory.create_group())
18+
query = self.factory.create_query(data_source=data_source, description="this is description")
19+
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}}""")
20+
visualization_box = self.factory.create_visualization(query=query, description="box vis", type="BOXPLOT", options="{}")
21+
fork_user = self.factory.create_user()
22+
23+
forked_query = query.fork(fork_user)
24+
25+
26+
forked_visualization_chart = None
27+
forked_visualization_box = None
28+
forked_table = None
29+
count_table = 0
30+
for v in forked_query.visualizations:
31+
if v.description == "chart vis":
32+
forked_visualization_chart = v
33+
if v.description == "box vis":
34+
forked_visualization_box = v
35+
if v.type == "TABLE":
36+
count_table += 1
37+
forked_table = v
38+
self.assert_visualizations(query, visualization_chart, forked_query, forked_visualization_chart)
39+
self.assert_visualizations(query, visualization_box, forked_query, forked_visualization_box)
40+
41+
self.assertEqual(forked_query.org, query.org)
42+
self.assertEqual(forked_query.data_source, query.data_source)
43+
self.assertEqual(forked_query.latest_query_data, query.latest_query_data)
44+
self.assertEqual(forked_query.description, query.description)
45+
self.assertEqual(forked_query.query, query.query)
46+
self.assertEqual(forked_query.query_hash, query.query_hash)
47+
self.assertEqual(forked_query.user, fork_user)
48+
self.assertEqual(forked_query.description, query.description)
49+
self.assertTrue(forked_query.name.startswith('Copy'))
50+
# num of TABLE must be 1. default table only
51+
self.assertEqual(count_table, 1)
52+
self.assertEqual(forked_table.name, "Table")
53+
self.assertEqual(forked_table.description, "")
54+
self.assertEqual(forked_table.options, "{}")
55+
56+
def test_fork_from_query_that_has_no_visualization(self):
57+
# prepare original query and visualizations
58+
data_source = self.factory.create_data_source(group=self.factory.create_group())
59+
query = self.factory.create_query(data_source=data_source, description="this is description")
60+
fork_user = self.factory.create_user()
61+
62+
forked_query = query.fork(fork_user)
63+
64+
count_table = 0
65+
count_vis = 0
66+
for v in forked_query.visualizations:
67+
count_vis += 1
68+
if v.type == "TABLE":
69+
count_table += 1
70+
71+
self.assertEqual(count_table, 1)
72+
self.assertEqual(count_vis, 1)
573

0 commit comments

Comments
 (0)