diff --git a/migrations/0027_add_draft_toggle.py b/migrations/0027_add_draft_toggle.py new file mode 100644 index 0000000000..0a07b14abe --- /dev/null +++ b/migrations/0027_add_draft_toggle.py @@ -0,0 +1,18 @@ +from playhouse.migrate import PostgresqlMigrator, migrate + +from redash.models import db +from redash import models + +if __name__ == '__main__': + db.connect_db() + migrator = PostgresqlMigrator(db.database) + + with db.database.transaction(): + migrate( + migrator.add_column('queries', 'is_draft', models.Query.is_draft) + ) + migrate( + migrator.add_column('dashboards', 'is_draft', models.Query.is_draft) + ) + db.database.execute_sql("UPDATE queries SET is_draft = (name = 'New Query')") + db.close_db(None) diff --git a/rd_ui/app/scripts/controllers/dashboard.js b/rd_ui/app/scripts/controllers/dashboard.js index 8f0c5db112..bc8b0097b7 100644 --- a/rd_ui/app/scripts/controllers/dashboard.js +++ b/rd_ui/app/scripts/controllers/dashboard.js @@ -129,6 +129,17 @@ }); }; + $scope.togglePublished = function () { + Events.record(currentUser, "toggle_published", "dashboard", $scope.dashboard.id); + $scope.dashboard.is_draft = !$scope.dashboard.is_draft; + $scope.saveInProgress = true; + Dashboard.save({slug: $scope.dashboard.id, name: $scope.dashboard.name, + layout: JSON.stringify($scope.dashboard.layout), + is_draft: $scope.dashboard.is_draft}, + function() {$scope.saveInProgress = false;}); + + }; + $scope.toggleFullscreen = function() { $scope.isFullscreen = !$scope.isFullscreen; $('body').toggleClass('headless'); diff --git a/rd_ui/app/scripts/controllers/query_view.js b/rd_ui/app/scripts/controllers/query_view.js index 6182bde93b..462bbc13b3 100644 --- a/rd_ui/app/scripts/controllers/query_view.js +++ b/rd_ui/app/scripts/controllers/query_view.js @@ -132,7 +132,7 @@ data.id = $scope.query.id; data.version = $scope.query.version; } else { - data = _.pick($scope.query, ["schedule", "query", "id", "description", "name", "data_source_id", "options", "latest_query_data_id", "version"]); + data = _.pick($scope.query, ["schedule", "query", "id", "description", "name", "data_source_id", "options", "latest_query_data_id", "version", "is_draft"]); } options = _.extend({}, { @@ -163,6 +163,12 @@ $scope.saveQuery(undefined, {'name': $scope.query.name}); }; + $scope.togglePublished = function() { + Events.record(currentUser, 'toggle_published', 'query', $scope.query.id); + $scope.query.is_draft = !$scope.query.is_draft; + $scope.saveQuery(undefined, {'is_draft': $scope.query.is_draft}); + }; + $scope.executeQuery = function() { if (!$scope.canExecuteQuery()) { return; diff --git a/rd_ui/app/views/dashboard.html b/rd_ui/app/views/dashboard.html index 055903b435..a4b093afda 100644 --- a/rd_ui/app/views/dashboard.html +++ b/rd_ui/app/views/dashboard.html @@ -22,6 +22,8 @@
  • Edit Dashboard
  • Add Widget
  • Manage Permissions
  • +
  • Unpublish Dashboard
  • +
  • Publish Dashboard
  • Archive Dashboard
  • @@ -29,6 +31,9 @@
    This dashboard is archived and won't appear in the dashboards list or search results.
    +
    + This dashboard is a draft. +
    diff --git a/rd_ui/app/views/query.html b/rd_ui/app/views/query.html index ed8ead1361..a5f6e738bf 100644 --- a/rd_ui/app/views/query.html +++ b/rd_ui/app/views/query.html @@ -91,6 +91,9 @@

    This query is archived and can't be used in dashboards, and won't appear in search results.
    +
    + This query is a draft. +

    @@ -126,6 +129,8 @@

    diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py index 01b36f6843..39109af670 100644 --- a/redash/handlers/dashboards.py +++ b/redash/handlers/dashboards.py @@ -31,13 +31,12 @@ def get(self): @require_permission('create_dashboard') def post(self): dashboard_properties = request.get_json(force=True) - dashboard = models.Dashboard.create(name=dashboard_properties['name'], - org=self.current_org, - user=self.current_user, - layout='[]') - - result = dashboard.to_dict() - return result + dashboard = models.Dashboard(name=dashboard_properties['name'], + org=self.current_org, + user=self.current_user, + is_draft=True, + layout='[]') + return dashboard.to_dict() class DashboardResource(BaseResource): @@ -63,7 +62,8 @@ def post(self, dashboard_slug): require_object_modify_permission(dashboard, self.current_user) - updates = project(dashboard_properties, ('name', 'layout', 'version')) + updates = project(dashboard_properties, ('name', 'layout', 'version', + 'is_draft')) updates['changed_by'] = self.current_user try: diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py index 4879e63f0b..38d4251229 100644 --- a/redash/handlers/queries.py +++ b/redash/handlers/queries.py @@ -62,6 +62,7 @@ def post(self): query_def['user'] = self.current_user query_def['data_source'] = data_source query_def['org'] = self.current_org + query_def['is_draft'] = True query = models.Query.create(**query_def) self.record_event({ diff --git a/redash/models.py b/redash/models.py index 4dd77eacb8..54ae72e51a 100644 --- a/redash/models.py +++ b/redash/models.py @@ -702,6 +702,7 @@ class Query(ChangeTrackingMixin, ModelTimestampsMixin, BaseVersionedModel, Belon user = peewee.ForeignKeyField(User) last_modified_by = peewee.ForeignKeyField(User, null=True, related_name="modified_queries") is_archived = peewee.BooleanField(default=False, index=True) + is_draft = peewee.BooleanField(default=True, index=True) schedule = peewee.CharField(max_length=10, null=True) options = JSONField(default={}) @@ -719,6 +720,7 @@ def to_dict(self, with_stats=False, with_visualizations=False, with_user=True, w 'schedule': self.schedule, 'api_key': self.api_key, 'is_archived': self.is_archived, + 'is_draft': self.is_draft, 'updated_at': self.updated_at, 'created_at': self.created_at, 'data_source_id': self.data_source_id, @@ -771,10 +773,9 @@ def all_queries(cls, groups, drafts=False): .order_by(cls.created_at.desc()) if drafts: - q = q.where(Query.name == 'New Query') + q = q.where(Query.is_draft == True) else: - q = q.where(Query.name != 'New Query') - + q = q.where(Query.is_draft == False) return q @classmethod @@ -818,17 +819,22 @@ def search(cls, term, groups): @classmethod def recent(cls, groups, user_id=None, limit=20): - query = cls.select(Query, User).where(Event.created_at > peewee.SQL("current_date - 7")).\ - join(Event, on=(Query.id == Event.object_id.cast('integer'))). \ - join(DataSourceGroup, on=(Query.data_source==DataSourceGroup.data_source)). \ - switch(Query).join(User).\ - where(Event.action << ('edit', 'execute', 'edit_name', 'edit_description', 'view_source')).\ - where(~(Event.object_id >> None)).\ - where(Event.object_type == 'query'). \ - where(DataSourceGroup.group << groups).\ - where(cls.is_archived == False).\ - group_by(Event.object_id, Query.id, User.id).\ - order_by(peewee.SQL("count(0) desc")) + query = ( + cls.select(Query, User) + .where(Event.created_at > peewee.SQL("current_date - 7")) + .join(Event, on=(Query.id == Event.object_id.cast('integer'))) + .join(DataSourceGroup, on=(Query.data_source==DataSourceGroup.data_source)) + .switch(Query).join(User) + .where(Event.action << ('edit', 'execute', 'edit_name', + 'edit_description', 'toggle_published', + 'view_source')) + .where(~(Event.object_id >> None)) + .where(Event.object_type == 'query') + .where(DataSourceGroup.group << groups) + .where(cls.is_archived == False) + .where(cls.is_draft == False) + .group_by(Event.object_id, Query.id, User.id) + .order_by(peewee.SQL("count(0) desc"))) if user_id: query = query.where(Event.user == user_id) @@ -1077,6 +1083,7 @@ class Dashboard(ChangeTrackingMixin, ModelTimestampsMixin, BaseVersionedModel, B layout = peewee.TextField() dashboard_filters_enabled = peewee.BooleanField(default=False) is_archived = peewee.BooleanField(default=False, index=True) + is_draft = peewee.BooleanField(default=False, index=True) class Meta: db_table = 'dashboards' @@ -1129,6 +1136,7 @@ def to_dict(self, with_widgets=False, user=None): 'dashboard_filters_enabled': self.dashboard_filters_enabled, 'widgets': widgets_layout, 'is_archived': self.is_archived, + 'is_draft': self.is_draft, 'updated_at': self.updated_at, 'created_at': self.created_at, 'version': self.version @@ -1136,17 +1144,21 @@ def to_dict(self, with_widgets=False, user=None): @classmethod def all(cls, org, groups, user_id): - query = cls.select().\ - join(Widget, peewee.JOIN_LEFT_OUTER, on=(Dashboard.id == Widget.dashboard)). \ - join(Visualization, peewee.JOIN_LEFT_OUTER, on=(Widget.visualization == Visualization.id)). \ - join(Query, peewee.JOIN_LEFT_OUTER, on=(Visualization.query == Query.id)). \ - join(DataSourceGroup, peewee.JOIN_LEFT_OUTER, on=(Query.data_source == DataSourceGroup.data_source)). \ - where(Dashboard.is_archived == False). \ - where((DataSourceGroup.group << groups) | - (Dashboard.user == user_id) | - (~(Widget.dashboard >> None) & (Widget.visualization >> None))). \ - where(Dashboard.org == org). \ - group_by(Dashboard.id) + query = (cls.select() + .join(Widget, peewee.JOIN_LEFT_OUTER, + on=(Dashboard.id == Widget.dashboard)) + .join(Visualization, peewee.JOIN_LEFT_OUTER, + on=(Widget.visualization == Visualization.id)) + .join(Query, peewee.JOIN_LEFT_OUTER, + on=(Visualization.query == Query.id)) + .join(DataSourceGroup, peewee.JOIN_LEFT_OUTER, + on=(Query.data_source == DataSourceGroup.data_source)) + .where(Dashboard.is_archived == False) + .where((DataSourceGroup.group << groups & (Dashboard.is_draft != True)) | + (Dashboard.user == user_id) | + (~(Widget.dashboard >> None) & (Widget.visualization >> None))) + .where(Dashboard.org == org) + .group_by(Dashboard.id)) return query @@ -1162,6 +1174,7 @@ def recent(cls, org, groups, user_id, for_user=False, limit=20): where(~(Event.object_id >> None)). \ where(Event.object_type == 'dashboard'). \ where(Dashboard.is_archived == False). \ + where(Dashboard.is_draft == False). \ where(Dashboard.org == org). \ where((DataSourceGroup.group << groups) | (Dashboard.user == user_id) | diff --git a/tests/factories.py b/tests/factories.py index 524b4bf115..fe6c5a1b9e 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -69,6 +69,7 @@ def __call__(self): query='SELECT 1', user=user_factory.create, is_archived=False, + is_draft=False, schedule=None, data_source=data_source_factory.create, org=1) @@ -79,6 +80,7 @@ def __call__(self): query='SELECT {{param1}}', user=user_factory.create, is_archived=False, + is_draft=False, schedule=None, data_source=data_source_factory.create, org=1) diff --git a/tests/handlers/test_queries.py b/tests/handlers/test_queries.py index 75049926d1..ce7e24484c 100644 --- a/tests/handlers/test_queries.py +++ b/tests/handlers/test_queries.py @@ -90,7 +90,6 @@ def test_works_for_non_owner_with_permission(self): self.assertEqual(rv.json['last_modified_by']['id'], user.id) - class TestQueryListResourcePost(BaseTestCase): def test_create_query(self): query_data = { @@ -110,6 +109,7 @@ def test_create_query(self): query = models.Query.get_by_id(rv.json['id']) self.assertEquals(len(list(query.visualizations)), 1) + self.assertTrue(query.is_draft) class QueryRefreshTest(BaseTestCase): diff --git a/tests/test_models.py b/tests/test_models.py index a3814924b8..18eeaeacfe 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -129,6 +129,22 @@ def test_global_recent(self): self.assertIn(q1, recent) self.assertNotIn(q2, recent) + def test_recent_excludes_drafts(self): + q1 = self.factory.create_query() + q2 = self.factory.create_query(is_draft=True) + + models.Event.create(org=self.factory.org, user=self.factory.user, + action="edit", object_type="query", + object_id=q1.id) + models.Event.create(org=self.factory.org, user=self.factory.user, + action="edit", object_type="query", + object_id=q2.id) + + recent = models.Query.recent([self.factory.default_group]) + + self.assertIn(q1, recent) + self.assertNotIn(q2, recent) + def test_recent_for_user(self): q1 = self.factory.create_query() q2 = self.factory.create_query() @@ -657,6 +673,16 @@ def test_returns_recent_dashboards_basic(self): self.assertNotIn(self.w2.dashboard, models.Dashboard.recent(self.u1.org, self.u1.groups, None)) self.assertNotIn(self.w1.dashboard, models.Dashboard.recent(self.u1.org, self.u2.groups, None)) + def test_recent_excludes_drafts(self): + models.Event.create(org=self.factory.org, user=self.u1, action="view", + object_type="dashboard", object_id=self.w1.dashboard.id) + models.Event.create(org=self.factory.org, user=self.u1, action="view", + object_type="dashboard", object_id=self.w2.dashboard.id) + + self.w2.dashboard.update_instance(is_draft=True) + self.assertIn(self.w1.dashboard, models.Dashboard.recent(self.u1.org, self.u1.groups, None)) + self.assertNotIn(self.w2.dashboard, models.Dashboard.recent(self.u1.org, self.u1.groups, None)) + def test_returns_recent_dashboards_created_by_user(self): d1 = self.factory.create_dashboard(user=self.u1) models.Event.create(org=self.factory.org, user=self.u1, action="view",