Repository: getredash/redash

Issue:
When forking a query, charts are not copied



Discussion:
+1

+1

I thought they were... Would like this as well.

+1

I will try this.

+1


Resolving patch:
diff --git a/redash/handlers/api.py b/redash/handlers/api.py
--- a/redash/handlers/api.py
+++ b/redash/handlers/api.py
@@ -9,7 +9,7 @@
 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 @@ def json_representation(data, code, headers=None):
 api.add_org_resource(MyQueriesResource, '/api/queries/my', endpoint='my_queries')
 api.add_org_resource(QueryRefreshResource, '/api/queries/<query_id>/refresh', endpoint='query_refresh')
 api.add_org_resource(QueryResource, '/api/queries/<query_id>', endpoint='query')
+api.add_org_resource(QueryForkResource, '/api/queries/<query_id>/fork', endpoint='query_fork')
 
 api.add_org_resource(ObjectPermissionsListResource, '/api/<object_type>/<object_id>/acl', endpoint='object_permissions')
 api.add_org_resource(CheckPermissionResource, '/api/<object_type>/<object_id>/acl/<access_type>', endpoint='check_permissions')
diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py
--- a/redash/handlers/queries.py
+++ b/redash/handlers/queries.py
@@ -138,6 +138,14 @@ def delete(self, query_id):
         query.archive(self.current_user)
 
 
+class QueryForkResource(BaseResource):
+    @require_permission('edit_query')
+    def post(self, query_id):
+        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):
     def post(self, query_id):
         query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
diff --git a/redash/models.py b/redash/models.py
--- a/redash/models.py
+++ b/redash/models.py
@@ -837,6 +837,31 @@ def recent(cls, groups, user_id=None, limit=20):
 
         return query
 
+    def fork(self, user):
+        query = self
+        forked_query = Query()
+        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:
+            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)
+        
+        if len(forked_visualizations) > 0:
+            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)


Test patch:
diff --git a/tests/models/test_queries.py b/tests/models/test_queries.py
--- a/tests/models/test_queries.py
+++ b/tests/models/test_queries.py
@@ -1,5 +1,73 @@
 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_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")
+        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_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'))
+        # 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, "{}")
+
+    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(fork_user)
+
+        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)