mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 01:03:20 -05:00
Compare commits
35 Commits
v0.3.1+b98
...
v0.3.2+b13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cbdae6e5c | ||
|
|
edcf0661a6 | ||
|
|
6d14c5c555 | ||
|
|
a0662d5323 | ||
|
|
cbd1cf7c25 | ||
|
|
a55225b5e8 | ||
|
|
b81c3ba614 | ||
|
|
2d0998a995 | ||
|
|
dda92477cf | ||
|
|
07455e5821 | ||
|
|
1b9aae0137 | ||
|
|
30b86ea781 | ||
|
|
a186d44d8f | ||
|
|
574f75b293 | ||
|
|
252ae7455a | ||
|
|
72065c0ee2 | ||
|
|
07caee1d12 | ||
|
|
4c3904760c | ||
|
|
8ad2c2a59e | ||
|
|
e5a365ba41 | ||
|
|
fc0b118188 | ||
|
|
a207b93d0d | ||
|
|
b1d588b1f2 | ||
|
|
95a6bab8b5 | ||
|
|
c82433e6b4 | ||
|
|
2e84852519 | ||
|
|
da746d15a0 | ||
|
|
1b519269d8 | ||
|
|
5ffaf1aead | ||
|
|
b704406164 | ||
|
|
5c9fe40702 | ||
|
|
fe7c4f96aa | ||
|
|
83909a07fa | ||
|
|
cd99927881 | ||
|
|
8bbb485d5b |
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
REDASH_CONNECTION_ADAPTER=pg
|
||||
REDASH_CONNECTION_STRING="dbname=data"
|
||||
REDASH_STATIC_ASSETS_PATH=../rd_ui/app/
|
||||
REDASH_GOOGLE_APPS_DOMAIN=
|
||||
REDASH_ADMINS=
|
||||
REDASH_WORKERS_COUNT=2
|
||||
REDASH_COOKIE_SECRET=
|
||||
REDASH_DATABASE_URL='postgresql://rd'
|
||||
REDASH_LOG_LEVEL = "INFO"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,7 +2,6 @@
|
||||
.idea
|
||||
*.pyc
|
||||
.coverage
|
||||
redash/settings.py
|
||||
rd_ui/dist
|
||||
.DS_Store
|
||||
|
||||
@@ -10,3 +9,4 @@ rd_ui/dist
|
||||
.vagrant
|
||||
Berksfile.lock
|
||||
redash/dump.rdb
|
||||
.env
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
cookbook 'apt'
|
||||
cookbook 'postgresql'
|
||||
cookbook 'redash', git: 'git@github.com:EverythingMe/chef-redash.git'
|
||||
2
Procfile.dev
Normal file
2
Procfile.dev
Normal file
@@ -0,0 +1,2 @@
|
||||
web: ./manage.py runserver -p $PORT
|
||||
worker: ./manage.py runworkers
|
||||
2
Procfile.heroku
Normal file
2
Procfile.heroku
Normal file
@@ -0,0 +1,2 @@
|
||||
web: ./manage.py runserver -p $PORT --host 0.0.0.0 -d -r
|
||||
worker: ./manage.py runworkers
|
||||
10
README.md
10
README.md
@@ -46,14 +46,8 @@ It's very likely that in the future we will switch to [D3.js](http://d3js.org/)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Download the [latest release](https://github.com/everythingme/redash/releases).
|
||||
2. Make sure you have `Python` v2.7, `pip`, PostgreSQL and Redis installed.
|
||||
3. Install Python requirements: `pip install -r requirements.txt`.
|
||||
4. Make a copy of the examples settings file: `cp redash/settings_example.py redash/settings.py` and edit the relevant settings.
|
||||
5. Create database: `./manage.py database create_tables`.
|
||||
6. Start the web server: `./manage.py runserver`.
|
||||
7. Start the worker: `./manage.py runworker`.
|
||||
8. Open `http://localhost:5000/` and query away.
|
||||
* [Setting up re:dash on Heroku in 5 minutes](https://github.com/EverythingMe/redash/wiki/Setting-up-re:dash-on-Heroku-in-5-minutes)
|
||||
* [Setting re:dash on your own server (Ubuntu)](https://github.com/EverythingMe/redash/wiki/Setting-re:dash-on-your-own-server-(Ubuntu))
|
||||
|
||||
**Need help setting re:dash or one of the dependencies up?** Ping @arikfr on the IRC #redash channel or send a message to the [mailing list](https://groups.google.com/forum/#!forum/redash-users), and he will gladly help.
|
||||
|
||||
|
||||
60
Vagrantfile
vendored
60
Vagrantfile
vendored
@@ -1,60 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = '2'
|
||||
|
||||
POSTGRES_PASSWORD = 'securepass'
|
||||
|
||||
# Currently, chef postgress cookbook works with cleartext paswords,
|
||||
# unless the password begins with 'md5'
|
||||
# See https://github.com/hw-cookbooks/postgresql/issues/95
|
||||
require "digest/md5"
|
||||
postgres_password_md5 = 'md5'+Digest::MD5.hexdigest(POSTGRES_PASSWORD+'postgres')
|
||||
|
||||
# After starting the vagrant machine, the application is accessible via the URL
|
||||
# http://localhost:9999
|
||||
HOST_PORT_TO_FORWARD_TO_REDASH = 9999
|
||||
|
||||
# Deploy direcly the code in parent dir; Don't download a release tarball
|
||||
live_testing_deployment = true
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
config.vm.box = 'ubuntu-precise-cloudimg-amd64'
|
||||
config.vm.box_url = 'http://cloud-images.ubuntu.com/vagrant/precise/current/precise-server-cloudimg-amd64-vagrant-disk1.box'
|
||||
|
||||
if config.respond_to? :cache
|
||||
config.cache.auto_detect = true
|
||||
end
|
||||
|
||||
config.berkshelf.enabled = true
|
||||
config.omnibus.chef_version = :latest
|
||||
|
||||
config.vm.network 'forwarded_port', guest: 8888, host: HOST_PORT_TO_FORWARD_TO_REDASH
|
||||
|
||||
if live_testing_deployment
|
||||
config.vm.synced_folder "..", "/opt/redash"
|
||||
end
|
||||
|
||||
config.vm.provision :chef_solo do |chef|
|
||||
# run apt-get update before anything else (specifically postgresql)..
|
||||
chef.add_recipe 'apt'
|
||||
chef.add_recipe 'redash::redis_for_redash'
|
||||
chef.add_recipe 'postgresql::client'
|
||||
chef.add_recipe 'postgresql::server'
|
||||
chef.add_recipe 'redash::redash_pg_schema'
|
||||
chef.add_recipe 'redash::redash'
|
||||
# chef.log_level = :debug
|
||||
chef.json = {
|
||||
'apt' => { 'compiletime' => true },
|
||||
'postgresql' => { 'password' => {'postgres' => postgres_password_md5 } },
|
||||
'redash' => { 'db' => {'host' => 'localhost',
|
||||
'user' => 'postgres',
|
||||
'password' => POSTGRES_PASSWORD },
|
||||
'allow' => {'google_app_domain' => 'gmail.com',
|
||||
'admins' => ['joe@egmail.com','jack@gmail.com']},
|
||||
'install_tarball' => !live_testing_deployment,
|
||||
'user' => live_testing_deployment ? 'vagrant' : 'redash'}
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,6 @@ dependencies:
|
||||
- make deps
|
||||
- pip install requests coverage nose
|
||||
- pip install -r requirements.txt
|
||||
- cp redash/settings_example.py redash/settings.py
|
||||
cache_directories:
|
||||
- rd_ui/node_modules/
|
||||
- rd_ui/app/bower_components/
|
||||
@@ -18,7 +17,6 @@ test:
|
||||
override:
|
||||
- make test
|
||||
post:
|
||||
- rm redash/settings.py
|
||||
- make pack
|
||||
deployment:
|
||||
github:
|
||||
|
||||
@@ -20,6 +20,7 @@ def version():
|
||||
"""Displays re:dash version."""
|
||||
print __version__
|
||||
|
||||
|
||||
@manager.command
|
||||
def runworkers():
|
||||
"""Starts the re:dash query executors/workers."""
|
||||
|
||||
12
migrations/change_queries_description_to_nullable.py
Normal file
12
migrations/change_queries_description_to_nullable.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from playhouse.migrate import Migrator
|
||||
from redash import db
|
||||
from redash import models
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
db.connect_db()
|
||||
migrator = Migrator(db.database)
|
||||
with db.database.transaction():
|
||||
migrator.set_nullable(models.Query, models.Query.description, True)
|
||||
|
||||
db.close_db(None)
|
||||
@@ -1,8 +1,46 @@
|
||||
(function () {
|
||||
var DashboardCtrl = function ($scope, $routeParams, $http, Dashboard) {
|
||||
var DashboardCtrl = function ($scope, $routeParams, $http, $timeout, Dashboard) {
|
||||
$scope.refreshEnabled = false;
|
||||
$scope.refreshRate = 60;
|
||||
$scope.dashboard = Dashboard.get({slug: $routeParams.dashboardSlug}, function(dashboard) {
|
||||
$scope.$parent.pageTitle = dashboard.name;
|
||||
});
|
||||
|
||||
var autoRefresh = function() {
|
||||
if ($scope.refreshEnabled) {
|
||||
$timeout(function() {
|
||||
Dashboard.get({slug: $routeParams.dashboardSlug}, function(dashboard) {
|
||||
var newWidgets = _.groupBy(_.flatten(dashboard.widgets), 'id');
|
||||
|
||||
_.each($scope.dashboard.widgets, function(row) {
|
||||
_.each(row, function(widget, i) {
|
||||
var newWidget = newWidgets[widget.id];
|
||||
if (newWidget && newWidget[0].visualization.query.latest_query_data_id != widget.visualization.query.latest_query_data_id ) {
|
||||
row[i] = newWidget[0];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
autoRefresh();
|
||||
});
|
||||
|
||||
}, $scope.refreshRate);
|
||||
};
|
||||
}
|
||||
|
||||
$scope.triggerRefresh = function(){
|
||||
$scope.refreshEnabled = !$scope.refreshEnabled;
|
||||
|
||||
if ($scope.refreshEnabled) {
|
||||
var refreshRate = _.min(_.flatten($scope.dashboard.widgets), function(widget) {
|
||||
return widget.visualization.query.ttl;
|
||||
}).visualization.query.ttl;
|
||||
|
||||
$scope.refreshRate = _.max([120, refreshRate * 2])*1000;
|
||||
|
||||
autoRefresh();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var WidgetCtrl = function ($scope, $http, $location, Query) {
|
||||
@@ -143,6 +181,7 @@
|
||||
|
||||
$scope.refreshOptions = [
|
||||
{value: -1, name: 'No Refresh'},
|
||||
{value: 60, name: 'Every minute'},
|
||||
]
|
||||
|
||||
_.each(_.range(1, 13), function(i) {
|
||||
@@ -390,7 +429,7 @@
|
||||
}
|
||||
|
||||
angular.module('redash.controllers', [])
|
||||
.controller('DashboardCtrl', ['$scope', '$routeParams', '$http', 'Dashboard', DashboardCtrl])
|
||||
.controller('DashboardCtrl', ['$scope', '$routeParams', '$http', '$timeout', 'Dashboard', DashboardCtrl])
|
||||
.controller('WidgetCtrl', ['$scope', '$http', '$location', 'Query', WidgetCtrl])
|
||||
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
|
||||
.controller('QueryFiddleCtrl', ['$scope', '$window', '$location', '$routeParams', '$http', '$location', 'growl', 'notifications', 'Query', 'Visualization', QueryFiddleCtrl])
|
||||
|
||||
@@ -69,6 +69,14 @@
|
||||
'Pie': 'pie'
|
||||
};
|
||||
|
||||
scope.stackingOptions = {
|
||||
"None": "none",
|
||||
"Normal": "normal",
|
||||
"Percent": "percent"
|
||||
};
|
||||
|
||||
scope.stacking = "none";
|
||||
|
||||
if (!scope.vis) {
|
||||
// create new visualization
|
||||
// wait for query to load to populate with defaults
|
||||
@@ -90,20 +98,46 @@
|
||||
if (chartType === Visualization.prototype.TYPES.CHART) {
|
||||
return {
|
||||
'series': {
|
||||
'type': 'column'
|
||||
'type': 'column',
|
||||
'stacking': null
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
var chartOptionsUnwatch = null;
|
||||
|
||||
scope.$watch('vis.type', function(type) {
|
||||
// if not edited by user, set name to match type
|
||||
if (type && scope.vis && !scope.visForm.name.$dirty) {
|
||||
// poor man's titlecase
|
||||
scope.vis.name = scope.vis.type[0] + scope.vis.type.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
if (type && type == Visualization.prototype.TYPES.CHART) {
|
||||
if (scope.vis.options.series.stacking === null) {
|
||||
scope.stacking = "none";
|
||||
} else if (scope.vis.options.series.stacking === undefined) {
|
||||
scope.stacking = "normal";
|
||||
} else {
|
||||
scope.stacking = scope.vis.options.series.stacking ;
|
||||
}
|
||||
|
||||
chartOptionsUnwatch = scope.$watch("stacking", function(stacking) {
|
||||
if (stacking == "none") {
|
||||
scope.vis.options.series.stacking = null;
|
||||
} else {
|
||||
scope.vis.options.series.stacking = stacking;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (chartOptionsUnwatch) {
|
||||
chartOptionsUnwatch();
|
||||
chartOptionsUnwatch = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scope.toggleAdvancedMode = function() {
|
||||
@@ -111,7 +145,7 @@
|
||||
};
|
||||
|
||||
scope.typeChanged = function() {
|
||||
scope.vis.options = newOptions();
|
||||
scope.vis.options = newOptions(scope.vis.type);
|
||||
};
|
||||
|
||||
scope.submit = function() {
|
||||
|
||||
@@ -213,4 +213,8 @@ to add those CSS styles here. */
|
||||
color: white;
|
||||
background-color: #FF8080;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
visualization-renderer > div {
|
||||
overflow: scroll;
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
<div class="container">
|
||||
<h2 id="dashboard_title">
|
||||
{{dashboard.name}}
|
||||
|
||||
<button type="button" class="btn btn-default btn-xs" ng-class="{active: refreshEnabled}" tooltip="Enable/Disable Auto Refresh" ng-click="triggerRefresh()"><span class="glyphicon glyphicon-refresh"></span></button>
|
||||
<span ng-show="dashboard.canEdit()">
|
||||
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" href="#edit_dashboard_dialog" tooltip="Edit Dashboard (Name/Layout)"><span
|
||||
class="glyphicon glyphicon-cog"></span></button>
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<div class="form-group" ng-show="vis.type == visTypes.Chart">
|
||||
<label class="control-label">Chart Type</label>
|
||||
<select required ng-model="vis.options.series.type" ng-options="value as key for (key, value) in seriesTypes" class="form-control"></select>
|
||||
|
||||
<label class="control-label">Stacking</label>
|
||||
<select required ng-model="stacking" ng-options="value as key for (key, value) in stackingOptions" class="form-control"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -7,7 +7,7 @@ from flask_peewee.db import Database
|
||||
import redis
|
||||
from redash import settings, utils
|
||||
|
||||
__version__ = '0.3.1'
|
||||
__version__ = '0.3.2'
|
||||
|
||||
app = Flask(__name__,
|
||||
template_folder=settings.STATIC_ASSETS_PATH,
|
||||
@@ -33,7 +33,11 @@ def json_representation(data, code, headers=None):
|
||||
|
||||
|
||||
redis_url = urlparse.urlparse(settings.REDIS_URL)
|
||||
redis_connection = redis.StrictRedis(host=redis_url.hostname, port=redis_url.port, db=0, password=redis_url.password)
|
||||
if redis_url.path:
|
||||
redis_db = redis_url.path[1]
|
||||
else:
|
||||
redis_db = 0
|
||||
redis_connection = redis.StrictRedis(host=redis_url.hostname, port=redis_url.port, db=redis_db, password=redis_url.password)
|
||||
|
||||
from redash import data
|
||||
data_manager = data.Manager(redis_connection, db)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import functools
|
||||
import hashlib
|
||||
import hmac
|
||||
from flask import request, make_response
|
||||
from flask.ext.googleauth import GoogleFederated
|
||||
from flask import current_app, request, make_response, g, redirect, url_for
|
||||
from flask.ext.googleauth import GoogleAuth
|
||||
import time
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
import werkzeug.wrappers
|
||||
from redash import models, settings
|
||||
|
||||
|
||||
@@ -23,36 +22,57 @@ class HMACAuthentication(object):
|
||||
def __init__(self, auth):
|
||||
self.auth = auth
|
||||
|
||||
def required(self, fn):
|
||||
wrapped_fn = self.auth.required(fn)
|
||||
@staticmethod
|
||||
def api_key_authentication():
|
||||
signature = request.args.get('signature')
|
||||
expires = float(request.args.get('expires') or 0)
|
||||
query_id = request.view_args.get('query_id', None)
|
||||
|
||||
# TODO: 3600 should be a setting
|
||||
if signature and query_id and time.time() < expires <= time.time() + 3600:
|
||||
query = models.Query.get(models.Query.id == query_id)
|
||||
calculated_signature = sign(query.api_key, request.path, expires)
|
||||
|
||||
if query.api_key and signature == calculated_signature:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_user_logged_in():
|
||||
return g.user is not None
|
||||
|
||||
@staticmethod
|
||||
def valid_user():
|
||||
email = g.user['email']
|
||||
if not settings.GOOGLE_APPS_DOMAIN:
|
||||
return True
|
||||
|
||||
return email in settings.ALLOWED_EXTERNAL_USERS or email.endswith("@%s" % settings.GOOGLE_APPS_DOMAIN)
|
||||
|
||||
def required(self, fn):
|
||||
@functools.wraps(fn)
|
||||
def decorated(*args, **kwargs):
|
||||
signature = request.args.get('signature')
|
||||
expires = float(request.args.get('expires') or 0)
|
||||
query_id = request.view_args.get('query_id', None)
|
||||
if self.is_user_logged_in() and self.valid_user():
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
# TODO: 3600 should be a setting
|
||||
if signature and query_id and time.time() < expires <= time.time() + 3600:
|
||||
query = models.Query.get(models.Query.id == query_id)
|
||||
calculated_signature = sign(query.api_key, request.path, expires)
|
||||
if self.api_key_authentication():
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
if query.api_key and signature == calculated_signature:
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
# Work around for flask-restful testing only for flask.wrappers.Resource instead of
|
||||
# werkzeug.wrappers.Response
|
||||
resp = wrapped_fn(*args, **kwargs)
|
||||
if isinstance(resp, werkzeug.wrappers.Response):
|
||||
resp = make_response(resp)
|
||||
|
||||
return resp
|
||||
blueprint = current_app.extensions['googleauth'].blueprint
|
||||
# The make_response call is a work around for flask-restful testing only for
|
||||
# flask.wrappers.Resource instead of werkzeug.wrappers.Response
|
||||
return make_response(redirect(url_for("%s.login" % blueprint.name, next=request.url)))
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def setup_authentication(app):
|
||||
openid_auth = GoogleFederated(settings.GOOGLE_APPS_DOMAIN, app)
|
||||
openid_auth = GoogleAuth(app)
|
||||
# If we don't have a list of external users, we can use Google's federated login, which limits
|
||||
# the domain with which you can sign in.
|
||||
if not settings.ALLOWED_EXTERNAL_USERS and settings.GOOGLE_APPS_DOMAIN:
|
||||
openid_auth._OPENID_ENDPOINT = "https://www.google.com/a/%s/o8/ud?be=o8" % settings.GOOGLE_APPS_DOMAIN
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
app.secret_key = settings.COOKIE_SECRET
|
||||
|
||||
|
||||
@@ -204,9 +204,8 @@ class QueryAPI(BaseResource):
|
||||
if 'latest_query_data_id' in query_def:
|
||||
query_def['latest_query_data'] = query_def.pop('latest_query_data_id')
|
||||
|
||||
update = models.Query.update(**query_def).where(models.Query.id == query_id)
|
||||
update.execute()
|
||||
|
||||
models.Query.update_instance(query_id, **query_def)
|
||||
|
||||
query = models.Query.get_by_id(query_id)
|
||||
|
||||
return query.to_dict(with_result=False, with_visualizations=True)
|
||||
|
||||
@@ -158,7 +158,12 @@ class Manager(object):
|
||||
runner = query_runner_mysql.mysql(connection_string)
|
||||
elif connection_type == 'graphite':
|
||||
from redash.data import query_runner_graphite
|
||||
runner = query_runner_graphite.graphite(connection_string)
|
||||
connection_params = json.loads(connection_string)
|
||||
if connection_params['auth']:
|
||||
connection_params['auth'] = tuple(connection_params['auth'])
|
||||
else:
|
||||
connection_params['auth'] = None
|
||||
runner = query_runner_graphite.graphite(connection_params)
|
||||
else:
|
||||
from redash.data import query_runner
|
||||
runner = query_runner.redshift(connection_string)
|
||||
|
||||
@@ -43,7 +43,10 @@ def mysql(connection_string):
|
||||
cursor.close()
|
||||
except MySQLdb.Error, e:
|
||||
json_data = None
|
||||
error = e.message
|
||||
error = e.args[1]
|
||||
except KeyboardInterrupt:
|
||||
error = "Query cancelled by user."
|
||||
json_data = None
|
||||
except Exception as e:
|
||||
raise sys.exc_info()[1], None, sys.exc_info()[2]
|
||||
finally:
|
||||
|
||||
@@ -51,7 +51,7 @@ class Query(BaseModel):
|
||||
id = peewee.PrimaryKeyField()
|
||||
latest_query_data = peewee.ForeignKeyField(QueryResult, null=True)
|
||||
name = peewee.CharField(max_length=255)
|
||||
description = peewee.CharField(max_length=4096)
|
||||
description = peewee.CharField(max_length=4096, null=True)
|
||||
query = peewee.TextField()
|
||||
query_hash = peewee.CharField(max_length=32)
|
||||
api_key = peewee.CharField(max_length=40)
|
||||
@@ -115,6 +115,14 @@ LEFT OUTER JOIN
|
||||
"""
|
||||
return cls.raw(query)
|
||||
|
||||
@classmethod
|
||||
def update_instance(cls, query_id, **kwargs):
|
||||
if 'query' in kwargs:
|
||||
kwargs['query_hash'] = utils.gen_query_hash(kwargs['query'])
|
||||
|
||||
update = cls.update(**kwargs).where(cls.id == query_id)
|
||||
return update.execute()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.query_hash = utils.gen_query_hash(self.query)
|
||||
self._set_api_key()
|
||||
|
||||
58
redash/settings.py
Normal file
58
redash/settings.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import urlparse
|
||||
|
||||
|
||||
def parse_db_url(url):
|
||||
url_parts = urlparse.urlparse(url)
|
||||
connection = {
|
||||
'engine': 'peewee.PostgresqlDatabase',
|
||||
}
|
||||
|
||||
if url_parts.hostname and not url_parts.path:
|
||||
connection['name'] = url_parts.hostname
|
||||
else:
|
||||
connection['name'] = url_parts.path[1:]
|
||||
connection['host'] = url_parts.hostname
|
||||
connection['port'] = url_parts.port
|
||||
connection['user'] = url_parts.username
|
||||
connection['password'] = url_parts.password
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
def fix_assets_path(path):
|
||||
fullpath = os.path.join(os.path.dirname(__file__), path)
|
||||
return fullpath
|
||||
|
||||
|
||||
def array_from_string(str):
|
||||
array = str.split(',')
|
||||
if "" in array:
|
||||
array.remove("")
|
||||
|
||||
return array
|
||||
|
||||
REDIS_URL = os.environ.get('REDASH_REDIS_URL', "redis://localhost:6379")
|
||||
|
||||
# "pg", "graphite" or "mysql"
|
||||
CONNECTION_ADAPTER = os.environ.get("REDASH_CONNECTION_ADAPTER", "pg")
|
||||
# Connection string for the database that is used to run queries against. Examples:
|
||||
# -- mysql: CONNECTION_STRING = "Server=;User=;Pwd=;Database="
|
||||
# -- pg: CONNECTION_STRING = "user= password= host= port=5439 dbname="
|
||||
# -- graphite: CONNECTION_STRING = {"url": "https://graphite.yourcompany.com", "auth": ["user", "password"], "verify": true}
|
||||
CONNECTION_STRING = os.environ.get("REDASH_CONNECTION_STRING", "user= password= host= port=5439 dbname=")
|
||||
|
||||
# Connection settings for re:dash's own database (where we store the queries, results, etc)
|
||||
DATABASE_CONFIG = parse_db_url(os.environ.get("REDASH_DATABASE_URL", "postgresql://postgres"))
|
||||
|
||||
# Google Apps domain to allow access from; any user with email in this Google Apps will be allowed
|
||||
# access
|
||||
GOOGLE_APPS_DOMAIN = os.environ.get("REDASH_GOOGLE_APPS_DOMAIN", "")
|
||||
# Email addresses of admin users (comma separated)
|
||||
ADMINS = array_from_string(os.environ.get("REDASH_ADMINS", ''))
|
||||
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/dist/"))
|
||||
WORKERS_COUNT = int(os.environ.get("REDASH_WORKERS_COUNT", "2"))
|
||||
COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f")
|
||||
LOG_LEVEL = os.environ.get("REDASH_LOG_LEVEL", "INFO")
|
||||
ANALYTICS = os.environ.get("REDASH_ANALYTICS", "")
|
||||
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
Example settings module. You should make your own copy as settings.py and enter the real settings.
|
||||
"""
|
||||
REDIS_URL = "redis://localhost:6379"
|
||||
|
||||
# "pg", "graphite" or "mysql"
|
||||
CONNECTION_ADAPTER = "pg"
|
||||
# Connection string for the database that is used to run queries against. Examples:
|
||||
# -- mysql: CONNECTION_STRING = "Server=;User=;Pwd=;Database="
|
||||
# -- pg: CONNECTION_STRING = "user= password= host= port=5439 dbname="
|
||||
# -- graphite: CONNECTION_STRING = {'url': 'https://graphite.yourcompany.com', 'auth': ('user', 'password'), 'verify': True}
|
||||
CONNECTION_STRING = "user= password= host= port=5439 dbname="
|
||||
# Connection settings for re:dash's own database (where we store the queries, results, etc)
|
||||
DATABASE_CONFIG = {
|
||||
'name': 'postgres',
|
||||
'engine': 'peewee.PostgresqlDatabase',
|
||||
}
|
||||
# Google Apps domain to allow access from; any user with email in this Google Apps will be allowed
|
||||
# access
|
||||
GOOGLE_APPS_DOMAIN = ""
|
||||
# Email addresses of specific users not from the above set Google Apps Domain, that you want to
|
||||
# allow access to re:dash
|
||||
ALLOWED_USERS = []
|
||||
# Email addresses of admin users
|
||||
ADMINS = []
|
||||
STATIC_ASSETS_PATH = "../rd_ui/dist/"
|
||||
WORKERS_COUNT = 2
|
||||
COOKIE_SECRET = "c292a0a3aa32397cdb050e233733900f"
|
||||
LOG_LEVEL = "INFO"
|
||||
ANALYTICS = ""
|
||||
@@ -23,3 +23,4 @@ sqlparse==0.1.8
|
||||
wsgiref==0.1.2
|
||||
wtf-peewee==0.2.2
|
||||
Flask-Script==0.6.6
|
||||
honcho==0.5.0
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
from tests import BaseTestCase
|
||||
from factories import dashboard_factory
|
||||
|
||||
|
||||
class DashboardTest(BaseTestCase):
|
||||
def test_appends_suffix_to_slug_when_duplicate(self):
|
||||
d1 = dashboard_factory.create()
|
||||
self.assertEquals(d1.slug, 'test')
|
||||
|
||||
d2 = dashboard_factory.create()
|
||||
self.assertNotEquals(d1.slug, d2.slug)
|
||||
|
||||
d3 = dashboard_factory.create()
|
||||
self.assertNotEquals(d1.slug, d3.slug)
|
||||
self.assertNotEquals(d2.slug, d3.slug)
|
||||
@@ -1,14 +1,17 @@
|
||||
from contextlib import contextmanager
|
||||
import json
|
||||
import time
|
||||
from unittest import TestCase
|
||||
from tests import BaseTestCase
|
||||
from tests.factories import dashboard_factory, widget_factory, visualization_factory, query_factory, \
|
||||
query_result_factory
|
||||
from redash import app, models
|
||||
from redash import app, models, settings
|
||||
from redash.utils import json_dumps
|
||||
from redash.authentication import sign
|
||||
|
||||
|
||||
settings.GOOGLE_APPS_DOMAIN = "example.com"
|
||||
|
||||
@contextmanager
|
||||
def authenticated_user(c, user='test@example.com', name='John Test'):
|
||||
with c.session_transaction() as sess:
|
||||
@@ -45,7 +48,42 @@ class AuthenticationTestMixin():
|
||||
self.assertEquals(200, rv.status_code)
|
||||
|
||||
|
||||
class PingTest(BaseTestCase):
|
||||
class TestAuthentication(TestCase):
|
||||
def test_redirects_for_nonsigned_in_user(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get("/")
|
||||
self.assertEquals(302, rv.status_code)
|
||||
|
||||
def test_returns_content_when_authenticated_with_correct_domain(self):
|
||||
settings.GOOGLE_APPS_DOMAIN = "example.com"
|
||||
with app.test_client() as c, authenticated_user(c, user="test@example.com"):
|
||||
rv = c.get("/")
|
||||
self.assertEquals(200, rv.status_code)
|
||||
|
||||
def test_redirects_when_authenticated_with_wrong_domain(self):
|
||||
settings.GOOGLE_APPS_DOMAIN = "example.com"
|
||||
with app.test_client() as c, authenticated_user(c, user="test@not-example.com"):
|
||||
rv = c.get("/")
|
||||
self.assertEquals(302, rv.status_code)
|
||||
|
||||
def test_returns_content_when_user_in_allowed_list(self):
|
||||
settings.GOOGLE_APPS_DOMAIN = "example.com"
|
||||
settings.ALLOWED_EXTERNAL_USERS = ["test@not-example.com"]
|
||||
|
||||
with app.test_client() as c, authenticated_user(c, user="test@not-example.com"):
|
||||
rv = c.get("/")
|
||||
self.assertEquals(200, rv.status_code)
|
||||
|
||||
def test_returns_content_when_google_apps_domain_empty(self):
|
||||
settings.GOOGLE_APPS_DOMAIN = ""
|
||||
settings.ALLOWED_EXTERNAL_USERS = []
|
||||
|
||||
with app.test_client() as c, authenticated_user(c, user="test@whatever.com"):
|
||||
rv = c.get("/")
|
||||
self.assertEquals(200, rv.status_code)
|
||||
|
||||
|
||||
class PingTest(TestCase):
|
||||
def test_ping(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/ping')
|
||||
@@ -83,7 +121,7 @@ class DashboardAPITest(BaseTestCase, AuthenticationTestMixin):
|
||||
self.assertEquals(rv.status_code, 404)
|
||||
|
||||
def test_create_new_dashboard(self):
|
||||
user_email = 'test@everything.me'
|
||||
user_email = 'test@example.com'
|
||||
with app.test_client() as c, authenticated_user(c, user=user_email):
|
||||
dashboard_name = 'Test Dashboard'
|
||||
rv = json_request(c.post, '/api/dashboards', data={'name': dashboard_name})
|
||||
@@ -182,10 +220,9 @@ class QueryAPITest(BaseTestCase, AuthenticationTestMixin):
|
||||
self.assertEquals(rv.json['name'], 'Testing')
|
||||
|
||||
def test_create_query(self):
|
||||
user = 'test@everything.me'
|
||||
user = 'test@example.com'
|
||||
query_data = {
|
||||
'name': 'Testing',
|
||||
'description': 'Description',
|
||||
'query': 'SELECT 1',
|
||||
'ttl': 3600
|
||||
}
|
||||
28
tests/test_models.py
Normal file
28
tests/test_models.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from tests import BaseTestCase
|
||||
from redash import models
|
||||
from factories import dashboard_factory, query_factory
|
||||
|
||||
|
||||
class DashboardTest(BaseTestCase):
|
||||
def test_appends_suffix_to_slug_when_duplicate(self):
|
||||
d1 = dashboard_factory.create()
|
||||
self.assertEquals(d1.slug, 'test')
|
||||
|
||||
d2 = dashboard_factory.create()
|
||||
self.assertNotEquals(d1.slug, d2.slug)
|
||||
|
||||
d3 = dashboard_factory.create()
|
||||
self.assertNotEquals(d1.slug, d3.slug)
|
||||
self.assertNotEquals(d2.slug, d3.slug)
|
||||
|
||||
|
||||
class QueryTest(BaseTestCase):
|
||||
def test_changing_query_text_changes_hash(self):
|
||||
q = query_factory.create()
|
||||
|
||||
old_hash = q.query_hash
|
||||
models.Query.update_instance(q.id, query="SELECT 2;")
|
||||
|
||||
q = models.Query.get_by_id(q.id)
|
||||
|
||||
self.assertNotEquals(old_hash, q.query_hash)
|
||||
27
tests/test_settings.py
Normal file
27
tests/test_settings.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from redash import settings as settings
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class TestDatabaseUrlParser(TestCase):
|
||||
def test_only_database_name(self):
|
||||
config = settings.parse_db_url("postgresql://postgres")
|
||||
self.assertEquals(config['name'], 'postgres')
|
||||
|
||||
def test_host_and_database_name(self):
|
||||
config = settings.parse_db_url("postgresql://localhost/postgres")
|
||||
self.assertEquals(config['name'], 'postgres')
|
||||
self.assertEquals(config['host'], 'localhost')
|
||||
|
||||
def test_host_with_port_and_database_name(self):
|
||||
config = settings.parse_db_url("postgresql://localhost:5432/postgres")
|
||||
self.assertEquals(config['name'], 'postgres')
|
||||
self.assertEquals(config['host'], 'localhost')
|
||||
self.assertEquals(config['port'], 5432)
|
||||
|
||||
def test_full_url(self):
|
||||
config = settings.parse_db_url("postgresql://user:pass@localhost:5432/postgres")
|
||||
self.assertEquals(config['name'], 'postgres')
|
||||
self.assertEquals(config['host'], 'localhost')
|
||||
self.assertEquals(config['port'], 5432)
|
||||
self.assertEquals(config['user'], 'user')
|
||||
self.assertEquals(config['password'], 'pass')
|
||||
Reference in New Issue
Block a user