Permalink
Browse files

Merge pull request #105 from EverythingMe/feature_user_object

Resolve #17: User model
  • Loading branch information...
2 parents 3cbdae6 + 51a37ca commit 8debd01a366d461556f82479d3d39860ff8bdb38 @arikfr arikfr committed Feb 27, 2014
View
56 migrations/create_users.py
@@ -0,0 +1,56 @@
+import json
+import itertools
+import peewee
+from playhouse.migrate import Migrator
+from redash import db, settings
+from redash import models
+
+if __name__ == '__main__':
+ db.connect_db()
+
+ if not models.User.table_exists():
+ print "Creating user table..."
+ models.User.create_table()
+
+ migrator = Migrator(db.database)
+ with db.database.transaction():
+ print "Creating user field on dashboard and queries..."
+ try:
+ migrator.rename_column(models.Query, '"user"', "user_email")
+ migrator.rename_column(models.Dashboard, '"user"', "user_email")
+ except peewee.ProgrammingError:
+ print "Failed to rename user column -- assuming it already exists"
+
+ with db.database.transaction():
+ models.Query.user.null = True
+ models.Dashboard.user.null = True
+
+ try:
+ migrator.add_column(models.Query, models.Query.user, "user_id")
+ migrator.add_column(models.Dashboard, models.Dashboard.user, "user_id")
+ except peewee.ProgrammingError:
+ print "Failed to create user_id column -- assuming it already exists"
+
+ print "Creating user for all queries and dashboards..."
+ for obj in itertools.chain(models.Query.select(), models.Dashboard.select()):
+ # Some old databases might have queries with empty string as user email:
+ email = obj.user_email or settings.ADMINS[0]
+ email = email.split(',')[0]
+
+ print ".. {} , {}, {}".format(type(obj), obj.id, email)
+
+ try:
+ user = models.User.get(models.User.email == email)
+ except models.User.DoesNotExist:
+ is_admin = email in settings.ADMINS
+ user = models.User.create(email=email, name=email, is_admin=is_admin)
+
+ obj.user = user
+ obj.save()
+
+ print "Set user_id to non null..."
+ with db.database.transaction():
+ migrator.set_nullable(models.Query, models.Query.user, False)
+ migrator.set_nullable(models.Dashboard, models.Dashboard.user, False)
+ migrator.set_nullable(models.Query, models.Query.user_email, True)
+ migrator.set_nullable(models.Dashboard, models.Dashboard.user_email, True)
View
3 rd_ui/app/index.html
@@ -124,7 +124,8 @@
var currentUser = {{ user|safe }};
currentUser.canEdit = function(object) {
- return object.user && (object.user.indexOf(currentUser.name) != -1);
+ var user_id = object.user_id || (object.user && object.user.id);
+ return user_id && (user_id == currentUser.id);
};
{{ analytics|safe }}
View
8 rd_ui/app/scripts/controllers.js
@@ -239,7 +239,7 @@
$scope.queryResult = $scope.query.getQueryResult();
});
} else {
- $scope.query = new Query({query: "", name: "New Query", ttl: -1, user: currentUser.name});
+ $scope.query = new Query({query: "", name: "New Query", ttl: -1, user: currentUser});
$scope.lockButton(false);
}
@@ -303,9 +303,9 @@
}
if ($scope.selectedTab.key == 'my') {
- return query.user == currentUser.name && query.name != 'New Query';
+ return query.user.id == currentUser.id && query.name != 'New Query';
} else if ($scope.selectedTab.key == 'drafts') {
- return query.user == currentUser.name && query.name == 'New Query';
+ return query.user.id == currentUser.id && query.name == 'New Query';
}
return query.name != 'New Query';
@@ -330,7 +330,7 @@
},
{
'label': 'Created By',
- 'map': 'user'
+ 'map': 'user.name'
},
{
'label': 'Created At',
View
2 rd_ui/app/views/queryfiddle.html
@@ -34,7 +34,7 @@ <h3 class="panel-title">
<span ng-show="queryResult.getRuntime()>=0">Query runtime: {{queryResult.getRuntime() | durationHumanize}} | </span>
<span ng-show="queryResult.query_result.retrieved_at">Last update time: <span am-time-ago="queryResult.query_result.retrieved_at"></span> | </span>
<span ng-show="queryResult.getStatus() == 'done'">Rows: {{queryResult.getData().length}} | </span>
- Created by: {{query.user}}
+ Created by: {{query.user.name}}
<div class="pull-right">Refresh query: <select ng-model="query.ttl" ng-options="c.value as c.name for c in refreshOptions"></select><br></div>
</div>
</div>
View
1 redash/__init__.py
@@ -14,7 +14,6 @@
static_folder=settings.STATIC_ASSETS_PATH,
static_path='/static')
-
api = Api(app)
# configure our database
View
21 redash/authentication.py
@@ -2,7 +2,7 @@
import hashlib
import hmac
from flask import current_app, request, make_response, g, redirect, url_for
-from flask.ext.googleauth import GoogleAuth
+from flask.ext.googleauth import GoogleAuth, login
import time
from werkzeug.contrib.fixers import ProxyFix
from redash import models, settings
@@ -67,6 +67,25 @@ def decorated(*args, **kwargs):
return decorated
+def create_user(_, user):
+ try:
+ u = models.User.get(models.User.email == user.email)
+ if u.name != user.name:
+ current_app.logger.debug("Updating user name (%r -> %r)", u.name, user.name)
+ u.name = user.name
+ u.save()
+ except models.User.DoesNotExist:
+ current_app.logger.debug("Creating user object (%r)", user.name)
+ u = models.User(name=user.name, email=user.email)
+ u.save()
+
+ user['id'] = u.id
+ user['is_admin'] = u.is_admin
+
+
+login.connect(create_user)
+
+
def setup_authentication(app):
openid_auth = GoogleAuth(app)
# If we don't have a list of external users, we can use Google's federated login, which limits
View
21 redash/controllers.py
@@ -39,8 +39,10 @@ def index(anything=None):
user = {
'gravatar_url': gravatar_url,
- 'is_admin': g.user['email'] in settings.ADMINS,
- 'name': g.user['email']
+ 'is_admin': g.user['is_admin'],
+ 'id': g.user['id'],
+ 'name': g.user['name'],
+ 'email': g.user['email']
}
return render_template("index.html", user=json.dumps(user), analytics=settings.ANALYTICS)
@@ -80,9 +82,16 @@ def format_sql_query():
class BaseResource(Resource):
decorators = [auth.required]
+ def __init__(self, *args, **kwargs):
+ super(BaseResource, self).__init__(*args, **kwargs)
+ self._user = None
+
@property
def current_user(self):
- return g.user['email']
+ if not self._user:
+ self._user = models.User(id=g.user['id'], email=g.user['email'], name=g.user['name'],
+ is_admin=g.user['is_admin'])
+ return self._user
class DashboardListAPI(BaseResource):
@@ -111,9 +120,9 @@ def get(self, dashboard_slug=None):
return dashboard.to_dict(with_widgets=True)
def post(self, dashboard_slug):
- # TODO: either convert all requests to use slugs or ids
dashboard_properties = request.get_json(force=True)
- dashboard = models.Dashboard.get(models.Dashboard.id == dashboard_slug)
+ # TODO: either convert all requests to use slugs or ids
+ dashboard = models.Dashboard.get_by_id(dashboard_slug)
dashboard.layout = dashboard_properties['layout']
dashboard.name = dashboard_properties['name']
dashboard.save()
@@ -198,7 +207,7 @@ def get(self):
class QueryAPI(BaseResource):
def post(self, query_id):
query_def = request.get_json(force=True)
- for field in ['id', 'created_at', 'api_key', 'visualizations', 'latest_query_data']:
+ for field in ['id', 'created_at', 'api_key', 'visualizations', 'latest_query_data', 'user']:
query_def.pop(field, None)
if 'latest_query_data_id' in query_def:
View
88 redash/models.py
@@ -7,21 +7,33 @@
from redash import db, utils
-#class User(db.Model):
-# id = db.Column(db.Integer, primary_key=True)
-# name = db.Column(db.String(320))
-# email = db.Column(db.String(160), unique=True)
-#
-# def __repr__(self):
-# return '<User %r, %r>' % (self.name, self.email)
-
-
class BaseModel(db.Model):
@classmethod
def get_by_id(cls, model_id):
return cls.get(cls.id == model_id)
+class User(BaseModel):
+ id = peewee.PrimaryKeyField()
+ name = peewee.CharField(max_length=320)
+ email = peewee.CharField(max_length=320, index=True, unique=True)
+ is_admin = peewee.BooleanField(default=False)
+
+ class Meta:
+ db_table = 'users'
+
+ def to_dict(self):
+ return {
+ 'id': self.id,
+ 'name': self.name,
+ 'email': self.email,
+ 'is_admin': self.is_admin
+ }
+
+ def __unicode__(self):
+ return '%r, %r' % (self.name, self.email)
+
+
class QueryResult(db.Model):
id = peewee.PrimaryKeyField()
query_hash = peewee.CharField(max_length=32, index=True)
@@ -56,7 +68,8 @@ class Query(BaseModel):
query_hash = peewee.CharField(max_length=32)
api_key = peewee.CharField(max_length=40)
ttl = peewee.IntegerField()
- user = peewee.CharField(max_length=360)
+ user_email = peewee.CharField(max_length=360, null=True)
+ user = peewee.ForeignKeyField(User)
created_at = peewee.DateTimeField(default=datetime.datetime.now)
class Meta:
@@ -68,7 +81,7 @@ def create_default_visualizations(self):
type="TABLE", options="{}")
table_visualization.save()
- def to_dict(self, with_result=True, with_stats=False, with_visualizations=False):
+ def to_dict(self, with_result=True, with_stats=False, with_visualizations=False, with_user=True):
d = {
'id': self.id,
'latest_query_data_id': self._data.get('latest_query_data', None),
@@ -77,11 +90,15 @@ def to_dict(self, with_result=True, with_stats=False, with_visualizations=False)
'query': self.query,
'query_hash': self.query_hash,
'ttl': self.ttl,
- 'user': self.user,
'api_key': self.api_key,
'created_at': self.created_at,
}
+ if with_user:
+ d['user'] = self.user.to_dict()
+ else:
+ d['user_id'] = self._data['user']
+
if with_stats:
d['avg_runtime'] = self.avg_runtime
d['min_runtime'] = self.min_runtime
@@ -100,20 +117,17 @@ def to_dict(self, with_result=True, with_stats=False, with_visualizations=False)
@classmethod
def all_queries(cls):
- query = """SELECT queries.*, query_stats.*
-FROM queries
-LEFT OUTER JOIN
- (SELECT qu.query_hash,
- count(0) AS "times_retrieved",
- avg(runtime) AS "avg_runtime",
- min(runtime) AS "min_runtime",
- max(runtime) AS "max_runtime",
- max(retrieved_at) AS "last_retrieved_at"
- FROM queries qu
- JOIN query_results qr ON qu.query_hash=qr.query_hash
- GROUP BY qu.query_hash) query_stats ON query_stats.query_hash = queries.query_hash
- """
- return cls.raw(query)
+ q = Query.select(Query, User,
+ peewee.fn.Count(QueryResult.id).alias('times_retrieved'),
+ peewee.fn.Avg(QueryResult.runtime).alias('avg_runtime'),
+ peewee.fn.Min(QueryResult.runtime).alias('min_runtime'),
+ peewee.fn.Max(QueryResult.runtime).alias('max_runtime'),
+ peewee.fn.Max(QueryResult.retrieved_at).alias('last_retrieved_at'))\
+ .join(QueryResult, join_type=peewee.JOIN_LEFT_OUTER)\
+ .switch(Query).join(User)\
+ .group_by(Query.id, User.id)
+
+ return q
@classmethod
def update_instance(cls, query_id, **kwargs):
@@ -131,17 +145,18 @@ def save(self, *args, **kwargs):
def _set_api_key(self):
if not self.api_key:
self.api_key = hashlib.sha1(
- u''.join([str(time.time()), self.query, self.user, self.name])).hexdigest()
+ u''.join((str(time.time()), self.query, str(self._data['user']), self.name)).encode('utf-8')).hexdigest()
def __unicode__(self):
return unicode(self.id)
-class Dashboard(db.Model):
+class Dashboard(BaseModel):
id = peewee.PrimaryKeyField()
slug = peewee.CharField(max_length=140, index=True)
name = peewee.CharField(max_length=100)
- user = peewee.CharField(max_length=360)
+ user_email = peewee.CharField(max_length=360, null=True)
+ user = peewee.ForeignKeyField(User)
layout = peewee.TextField()
is_archived = peewee.BooleanField(default=False, index=True)
created_at = peewee.DateTimeField(default=datetime.datetime.now)
@@ -153,8 +168,13 @@ def to_dict(self, with_widgets=False):
layout = json.loads(self.layout)
if with_widgets:
- widgets = Widget.select(Widget, Visualization, Query, QueryResult).\
- where(Widget.dashboard == self.id).join(Visualization).join(Query).join(QueryResult)
+ widgets = Widget.select(Widget, Visualization, Query, QueryResult, User)\
+ .where(Widget.dashboard == self.id)\
+ .join(Visualization)\
+ .join(Query)\
+ .join(User)\
+ .switch(Query)\
+ .join(QueryResult)
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)
else:
@@ -164,14 +184,14 @@ def to_dict(self, with_widgets=False):
'id': self.id,
'slug': self.slug,
'name': self.name,
- 'user': self.user,
+ 'user_id': self._data['user'],
'layout': layout,
'widgets': widgets_layout
}
@classmethod
def get_by_slug(cls, slug):
- return cls.get(cls.slug==slug)
+ return cls.get(cls.slug == slug)
def save(self, *args, **kwargs):
if not self.slug:
@@ -245,7 +265,7 @@ def to_dict(self):
def __unicode__(self):
return u"%s" % self.id
-all_models = (QueryResult, Query, Dashboard, Visualization, Widget)
+all_models = (User, QueryResult, Query, Dashboard, Visualization, Widget)
def create_db(create_tables, drop_tables):
View
2 tests/__init__.py
@@ -2,7 +2,7 @@
from redash import settings, db, app
import redash.models
-# TODO: this isn't pretty... :-)
+# TODO: this isn't pretty...
settings.DATABASE_CONFIG = {
'name': 'circle_test',
'engine': 'peewee.PostgresqlDatabase',
View
22 tests/factories.py
@@ -26,15 +26,33 @@ def create(self, **override_kwargs):
kwargs = self._get_kwargs(override_kwargs)
return self.model.create(**kwargs)
+
+class Sequence(object):
+ def __init__(self, string):
+ self.sequence = 0
+ self.string = string
+
+ def __call__(self):
+ self.sequence += 1
+
+ return self.string.format(self.sequence)
+
+
+user_factory = ModelFactory(redash.models.User,
+ name='John Doe', email=Sequence('test{}@example.com'),
+ is_admin=False)
+
+
dashboard_factory = ModelFactory(redash.models.Dashboard,
- name='test', user='test@everything.me', layout='[]')
+ name='test', user=user_factory.create, layout='[]')
+
query_factory = ModelFactory(redash.models.Query,
name='New Query',
description='',
query='SELECT 1',
ttl=-1,
- user='test@everything.me')
+ user=user_factory.create)
query_result_factory = ModelFactory(redash.models.QueryResult,
data='{"columns":{}, "rows":[]}',
View
30 tests/test_controllers.py
@@ -4,7 +4,7 @@
from unittest import TestCase
from tests import BaseTestCase
from tests.factories import dashboard_factory, widget_factory, visualization_factory, query_factory, \
- query_result_factory
+ query_result_factory, user_factory
from redash import app, models, settings
from redash.utils import json_dumps
from redash.authentication import sign
@@ -13,9 +13,13 @@
settings.GOOGLE_APPS_DOMAIN = "example.com"
@contextmanager
-def authenticated_user(c, user='test@example.com', name='John Test'):
+def authenticated_user(c, user=None):
+ if not user:
+ user = user_factory.create()
+
with c.session_transaction() as sess:
- sess['openid'] = {'email': user, 'name': name}
+ sess['openid'] = {'email': user.email, 'name': user.name,
+ 'id': user.id, 'is_admin': user.is_admin}
yield
@@ -48,37 +52,37 @@ def test_returns_content_when_authenticated(self):
self.assertEquals(200, rv.status_code)
-class TestAuthentication(TestCase):
+class TestAuthentication(BaseTestCase):
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"):
+ with app.test_client() as c, authenticated_user(c, user=user_factory.create(email="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"):
+ with app.test_client() as c, authenticated_user(c, user=user_factory.create(email="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"):
+ with app.test_client() as c, authenticated_user(c, user=user_factory.create(email="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"):
+ with app.test_client() as c, authenticated_user(c, user=user_factory.create(email="test@whatever.com")):
rv = c.get("/")
self.assertEquals(200, rv.status_code)
@@ -121,13 +125,13 @@ def test_get_non_existint_dashbaord(self):
self.assertEquals(rv.status_code, 404)
def test_create_new_dashboard(self):
- user_email = 'test@example.com'
- with app.test_client() as c, authenticated_user(c, user=user_email):
+ user = user_factory.create()
+ with app.test_client() as c, authenticated_user(c, user=user):
dashboard_name = 'Test Dashboard'
rv = json_request(c.post, '/api/dashboards', data={'name': dashboard_name})
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.json['name'], 'Test Dashboard')
- self.assertEquals(rv.json['user'], user_email)
+ self.assertEquals(rv.json['user_id'], user.id)
self.assertEquals(rv.json['layout'], [])
def test_update_dashboard(self):
@@ -220,7 +224,7 @@ def test_update_query(self):
self.assertEquals(rv.json['name'], 'Testing')
def test_create_query(self):
- user = 'test@example.com'
+ user = user_factory.create()
query_data = {
'name': 'Testing',
'query': 'SELECT 1',
@@ -232,7 +236,7 @@ def test_create_query(self):
self.assertEquals(rv.status_code, 200)
self.assertDictContainsSubset(query_data, rv.json)
- self.assertEquals(rv.json['user'], user)
+ self.assertEquals(rv.json['user']['id'], user.id)
self.assertIsNotNone(rv.json['api_key'])
self.assertIsNotNone(rv.json['query_hash'])
View
4 tests/test_models.py
@@ -8,10 +8,10 @@ def test_appends_suffix_to_slug_when_duplicate(self):
d1 = dashboard_factory.create()
self.assertEquals(d1.slug, 'test')
- d2 = dashboard_factory.create()
+ d2 = dashboard_factory.create(user=d1.user)
self.assertNotEquals(d1.slug, d2.slug)
- d3 = dashboard_factory.create()
+ d3 = dashboard_factory.create(user=d1.user)
self.assertNotEquals(d1.slug, d3.slug)
self.assertNotEquals(d2.slug, d3.slug)

0 comments on commit 8debd01

Please sign in to comment.