diff --git a/rd_ui/app/scripts/controllers/query_source.js b/rd_ui/app/scripts/controllers/query_source.js index 075b9dee93..a104e365ee 100644 --- a/rd_ui/app/scripts/controllers/query_source.js +++ b/rd_ui/app/scripts/controllers/query_source.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function QuerySourceCtrl(Events, $controller, $scope, $location, Query, Visualization, KeyboardShortcuts) { + function QuerySourceCtrl(Events, growl, $controller, $scope, $location, Query, Visualization, KeyboardShortcuts) { // extends QueryViewCtrl $controller('QueryViewCtrl', {$scope: $scope}); // TODO: @@ -67,15 +67,19 @@ if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) { Events.record(currentUser, 'delete', 'visualization', vis.id); - Visualization.delete(vis); - if ($scope.selectedTab == vis.id) { - $scope.selectedTab = DEFAULT_TAB; - $location.hash($scope.selectedTab); - } - $scope.query.visualizations = - $scope.query.visualizations.filter(function(v) { - return vis.id !== v.id; - }); + Visualization.delete(vis, function() { + if ($scope.selectedTab == vis.id) { + $scope.selectedTab = DEFAULT_TAB; + $location.hash($scope.selectedTab); + } + $scope.query.visualizations = + $scope.query.visualizations.filter(function (v) { + return vis.id !== v.id; + }); + }, function () { + growl.addErrorMessage("Error deleting visualization. Maybe it's used in a dashboard?"); + }); + } }; @@ -99,7 +103,7 @@ } angular.module('redash.controllers').controller('QuerySourceCtrl', [ - 'Events', '$controller', '$scope', '$location', 'Query', + 'Events', 'growl', '$controller', '$scope', '$location', 'Query', 'Visualization', 'KeyboardShortcuts', QuerySourceCtrl ]); })(); \ No newline at end of file diff --git a/rd_ui/app/scripts/visualizations/chart.js b/rd_ui/app/scripts/visualizations/chart.js index e0208d358c..0968399886 100644 --- a/rd_ui/app/scripts/visualizations/chart.js +++ b/rd_ui/app/scripts/visualizations/chart.js @@ -101,6 +101,8 @@ } }); + scope.xAxisType = (scope.visualization.options.xAxis && scope.visualization.options.xAxis.type) || scope.xAxisType; + xAxisUnwatch = scope.$watch("xAxisType", function (xAxisType) { scope.visualization.options.xAxis = scope.visualization.options.xAxis || {}; scope.visualization.options.xAxis.type = xAxisType; diff --git a/redash/data/worker.py b/redash/data/worker.py index fcbbfd774d..5b2fbdafd6 100644 --- a/redash/data/worker.py +++ b/redash/data/worker.py @@ -314,7 +314,7 @@ def _fork_and_process(self, job_id): self.name, job_id) job.done(None, "Interrupted/Cancelled while running.") - job.expire(24 * 3600) + job.expire(settings.JOB_EXPIRY_TIME) logging.info("[%s] Finished Processing %s (pid: %d status: %d)", self.name, job_id, self.child_pid, status) diff --git a/redash/models.py b/redash/models.py index a79c48f460..bdc061e072 100644 --- a/redash/models.py +++ b/redash/models.py @@ -133,8 +133,13 @@ def to_dict(self): def get_latest(cls, data_source, query, ttl=0): query_hash = utils.gen_query_hash(query) - query = cls.select().where(cls.query_hash == query_hash, cls.data_source == data_source, - peewee.SQL("retrieved_at + interval '%s second' >= now() at time zone 'utc'", ttl)).order_by(cls.retrieved_at.desc()) + if ttl == -1: + query = cls.select().where(cls.query_hash == query_hash, + cls.data_source == data_source).order_by(cls.retrieved_at.desc()) + else: + query = cls.select().where(cls.query_hash == query_hash, cls.data_source == data_source, + peewee.SQL("retrieved_at + interval '%s second' >= now() at time zone 'utc'", + ttl)).order_by(cls.retrieved_at.desc()) return query.first() @@ -261,7 +266,23 @@ def to_dict(self, with_widgets=False): .switch(Query)\ .join(QueryResult, join_type=peewee.JOIN_LEFT_OUTER) widgets = {w.id: w.to_dict() for w in widgets} - widgets_layout = map(lambda row: map(lambda widget_id: widgets.get(widget_id, None), row), layout) + + # The following is a workaround for cases when the widget object gets deleted without the dashboard layout + # updated. This happens for users with old databases that didn't have a foreign key relationship between + # visualizations and widgets. + # It's temporary until better solution is implemented (we probably should move the position information + # to the widget). + widgets_layout = [] + for row in layout: + new_row = [] + for widget_id in row: + widget = widgets.get(widget_id, None) + if widget: + new_row.append(widget) + + widgets_layout.append(new_row) + + # widgets_layout = map(lambda row: map(lambda widget_id: widgets.get(widget_id, None), row), layout) else: widgets_layout = None diff --git a/redash/settings.py b/redash/settings.py index a225c1206a..50421354c6 100644 --- a/redash/settings.py +++ b/redash/settings.py @@ -63,6 +63,7 @@ def parse_boolean(str): ALLOWED_EXTERNAL_USERS = array_from_string(os.environ.get("REDASH_ALLOWED_EXTERNAL_USERS", '')) STATIC_ASSETS_PATH = fix_assets_path(os.environ.get("REDASH_STATIC_ASSETS_PATH", "../rd_ui/app/")) WORKERS_COUNT = int(os.environ.get("REDASH_WORKERS_COUNT", "2")) +JOB_EXPIRY_TIME = int(os.environ.get("REDASH_JOB_EXPIRY_TIME", 3600*24)) COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f") LOG_LEVEL = os.environ.get("REDASH_LOG_LEVEL", "INFO") EVENTS_LOG_PATH = os.environ.get("REDASH_EVENTS_LOG_PATH", "") diff --git a/tests/test_models.py b/tests/test_models.py index 854929c5b8..4ec0f10e62 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -80,4 +80,14 @@ def test_get_latest_returns_the_most_recent_result(self): found_query_result = models.QueryResult.get_latest(qr.data_source, qr.query, 60) + self.assertEqual(found_query_result.id, qr.id) + + def test_get_latest_returns_the_last_cached_result_for_negative_ttl(self): + yesterday = datetime.datetime.now() + datetime.timedelta(days=-100) + very_old = query_result_factory.create(retrieved_at=yesterday) + + yesterday = datetime.datetime.now() + datetime.timedelta(days=-1) + qr = query_result_factory.create(retrieved_at=yesterday) + found_query_result = models.QueryResult.get_latest(qr.data_source, qr.query, -1) + self.assertEqual(found_query_result.id, qr.id) \ No newline at end of file