Compare commits

...

35 Commits

Author SHA1 Message Date
Arik Fraimovich
3cbdae6e5c Merge pull request #111 from EverythingMe/fix_graphite_settings
Fix: added JSON parsing of the Graphite settings
2014-02-25 08:38:32 +02:00
Arik Fraimovich
edcf0661a6 Fix: add parsing of graphite settings 2014-02-25 08:37:19 +02:00
Arik Fraimovich
6d14c5c555 Fix graphite settings example 2014-02-25 08:36:59 +02:00
Arik Fraimovich
a0662d5323 Remove outdated vagrant file 2014-02-25 08:17:16 +02:00
Arik Fraimovich
cbd1cf7c25 Make sure visualization don't overflow 2014-02-25 08:16:36 +02:00
Arik Fraimovich
a55225b5e8 Merge pull request #110 from ekampf/feature/fixmyql
Fixed mysql error handling
2014-02-24 20:20:20 +02:00
Eran Kampf
b81c3ba614 Fixed MySQL Errors 2014-02-24 16:44:08 +02:00
Arik Fraimovich
2d0998a995 Update Getting Started instructions. 2014-02-24 14:40:47 +02:00
Arik Fraimovich
dda92477cf Merge pull request #103 from EverythingMe/refresh_button
Use database number from redis url if available.
2014-02-17 18:02:22 +02:00
Arik Fraimovich
07455e5821 Use database number from redis url if available. 2014-02-17 18:01:44 +02:00
Arik Fraimovich
1b9aae0137 Merge pull request #102 from EverythingMe/refresh_button
Only refresh widgets that have their query data updated.
2014-02-17 17:59:27 +02:00
Arik Fraimovich
30b86ea781 Only refresh widgets that have their query data updated. 2014-02-17 17:57:26 +02:00
Arik Fraimovich
a186d44d8f Merge pull request #101 from EverythingMe/refresh_button
Auto-refresh button for dashboards & every minute refresh rate
2014-02-17 17:22:22 +02:00
Arik Fraimovich
574f75b293 Option to set every minute refresh rate. 2014-02-17 17:19:48 +02:00
Arik Fraimovich
252ae7455a Auto-refresh button for dashboards. 2014-02-17 17:19:32 +02:00
Arik Fraimovich
72065c0ee2 Merge pull request #99 from EverythingMe/feature_allow_external_users
Procfile changes:
2014-02-13 20:21:08 +02:00
Arik Fraimovich
07caee1d12 Procfile changes:
1. Renamed Honchofile -> Procfile.heroku and changed it to work better with Heroku.
2. Added Procfile.dev for development.
2014-02-13 20:16:36 +02:00
Arik Fraimovich
4c3904760c Merge pull request #98 from EverythingMe/feature_allow_external_users
Feature: allow external users
2014-02-13 20:15:54 +02:00
Arik Fraimovich
8ad2c2a59e If only domain specified and not external users, use federated login. 2014-02-13 20:13:08 +02:00
Arik Fraimovich
e5a365ba41 Bring back the ability to set allowed external users & publicly open re:dash. 2014-02-13 20:04:28 +02:00
Arik Fraimovich
fc0b118188 Merge pull request #96 from EverythingMe/fix_description_nullable
Fix: allow queries.description to be null (+ migration)
2014-02-13 19:18:39 +02:00
Arik Fraimovich
a207b93d0d Fix: allow queries.description to be null. 2014-02-13 19:08:35 +02:00
Arik Fraimovich
b1d588b1f2 Merge pull request #95 from EverythingMe/feature_stacking_selection
Allow user to set the stacking of the chart.
2014-02-13 16:24:39 +02:00
Arik Fraimovich
95a6bab8b5 Allow user to set the stacking of the chart. 2014-02-13 16:19:15 +02:00
Arik Fraimovich
c82433e6b4 CirlceCI: no longer need to delete settings.py. 2014-02-13 14:50:42 +02:00
Arik Fraimovich
2e84852519 Merge pull request #94 from EverythingMe/fix_query_hash_not_updating
Fix: when updating query text the hash should change.
2014-02-13 13:13:38 +02:00
Arik Fraimovich
da746d15a0 Fix: when updating query text the hash should change. 2014-02-13 13:08:48 +02:00
Arik Fraimovich
1b519269d8 Merge pull request #93 from EverythingMe/feature_env
Feature: better Heroku support - move configuration to environment variables & Procfile
2014-02-13 12:15:52 +02:00
Arik Fraimovich
5ffaf1aead Fix CircleCI configuration 2014-02-12 21:37:56 +02:00
Arik Fraimovich
b704406164 Example .env file. 2014-02-12 20:53:32 +02:00
Arik Fraimovich
5c9fe40702 Bump version. 2014-02-12 20:52:36 +02:00
Arik Fraimovich
fe7c4f96aa Fix: allow passing relative path for assets. 2014-02-12 20:52:19 +02:00
Arik Fraimovich
83909a07fa Read settings from environment variables instead of a settings file.
This is mostly done to make it easier to run re:dash on Heroku but should be convenient in other platforms too.
2014-02-12 20:43:41 +02:00
Arik Fraimovich
cd99927881 Add Honcho (foreman alternative in Python) file(s).
The reason we have both Procfile and Honchofile is to be able to run both the workers and the web server in a single dyno on Heroku.
2014-02-12 20:42:32 +02:00
Arik Fraimovich
8bbb485d5b Rename test files to test_. 2014-02-12 20:41:36 +02:00
29 changed files with 343 additions and 160 deletions

9
.env.example Normal file
View 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
View File

@@ -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

View File

@@ -1,3 +0,0 @@
cookbook 'apt'
cookbook 'postgresql'
cookbook 'redash', git: 'git@github.com:EverythingMe/chef-redash.git'

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: honcho start -f Procfile.heroku -p $PORT

2
Procfile.dev Normal file
View File

@@ -0,0 +1,2 @@
web: ./manage.py runserver -p $PORT
worker: ./manage.py runworkers

2
Procfile.heroku Normal file
View File

@@ -0,0 +1,2 @@
web: ./manage.py runserver -p $PORT --host 0.0.0.0 -d -r
worker: ./manage.py runworkers

View File

@@ -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
View File

@@ -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

View File

@@ -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:

View File

@@ -20,6 +20,7 @@ def version():
"""Displays re:dash version."""
print __version__
@manager.command
def runworkers():
"""Starts the re:dash query executors/workers."""

View 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)

View File

@@ -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])

View File

@@ -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() {

View File

@@ -213,4 +213,8 @@ to add those CSS styles here. */
color: white;
background-color: #FF8080;
border-radius: 50%;
}
visualization-renderer > div {
overflow: scroll;
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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
View 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", "")

View File

@@ -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 = ""

View File

@@ -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

View File

@@ -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)

View File

@@ -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
View 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
View 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')