Use of user object (fix views, update migrations and some).

This commit is contained in:
Arik Fraimovich 2014-02-22 14:42:08 +02:00
parent 52bcb8dfb6
commit 9defa45428
11 changed files with 117 additions and 56 deletions

View File

@ -1,5 +1,6 @@
import json
import itertools
import peewee
from playhouse.migrate import Migrator
from redash import db, settings
from redash import models
@ -14,14 +15,21 @@ if __name__ == '__main__':
migrator = Migrator(db.database)
with db.database.transaction():
print "Creating user field on dashboard and queries..."
migrator.rename_column(models.Query, '"user"', "user_email")
migrator.rename_column(models.Dashboard, '"user"', "user_email")
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
migrator.add_column(models.Query, models.Query.user, "user_id")
migrator.add_column(models.Dashboard, models.Dashboard.user, "user_id")
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()):
@ -35,12 +43,14 @@ if __name__ == '__main__':
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="", is_admin=is_admin)
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, True)
migrator.set_nullable(models.Dashboard, models.Dashboard.user, True)
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 File

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

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

@ -34,7 +34,7 @@
<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 File

@ -14,7 +14,6 @@ app = Flask(__name__,
static_folder=settings.STATIC_ASSETS_PATH,
static_path='/static')
api = Api(app)
# configure our database

View File

@ -79,6 +79,9 @@ def create_user(_, user):
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)

View File

@ -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 @@ class DashboardAPI(BaseResource):
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()

View File

@ -19,8 +19,19 @@ class User(BaseModel):
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 '<User %r, %r>' % (self.name, self.email)
return '%r, %r' % (self.name, self.email)
class QueryResult(db.Model):
@ -57,7 +68,7 @@ class Query(BaseModel):
query_hash = peewee.CharField(max_length=32)
api_key = peewee.CharField(max_length=40)
ttl = peewee.IntegerField()
user_email = 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)
@ -70,7 +81,7 @@ class Query(BaseModel):
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),
@ -79,11 +90,15 @@ class Query(BaseModel):
'query': self.query,
'query_hash': self.query_hash,
'ttl': self.ttl,
'user': self.user.email,
'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
@ -102,20 +117,17 @@ class Query(BaseModel):
@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):
@ -139,11 +151,11 @@ LEFT OUTER JOIN
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_email = 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)
@ -156,8 +168,13 @@ class Dashboard(db.Model):
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:
@ -167,14 +184,14 @@ class Dashboard(db.Model):
'id': self.id,
'slug': self.slug,
'name': self.name,
'user': self.user.email,
'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:

View File

@ -26,15 +26,33 @@ class ModelFactory(object):
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 File

@ -4,7 +4,7 @@ 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
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 @@ from redash.authentication import sign
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,7 +52,7 @@ class AuthenticationTestMixin():
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("/")
@ -56,13 +60,13 @@ class TestAuthentication(TestCase):
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)
@ -70,7 +74,7 @@ class TestAuthentication(TestCase):
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)
@ -78,7 +82,7 @@ class TestAuthentication(TestCase):
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)
@ -122,7 +126,7 @@ class DashboardAPITest(BaseTestCase, AuthenticationTestMixin):
def test_create_new_dashboard(self):
user_email = 'test@example.com'
with app.test_client() as c, authenticated_user(c, user=user_email):
with app.test_client() as c, authenticated_user(c, user=user_factory.create(email=user_email)):
dashboard_name = 'Test Dashboard'
rv = json_request(c.post, '/api/dashboards', data={'name': dashboard_name})
self.assertEquals(rv.status_code, 200)
@ -227,7 +231,7 @@ class QueryAPITest(BaseTestCase, AuthenticationTestMixin):
'ttl': 3600
}
with app.test_client() as c, authenticated_user(c, user=user):
with app.test_client() as c, authenticated_user(c, user=user_factory.create(email=user)):
rv = json_request(c.post, '/api/queries', data=query_data)
self.assertEquals(rv.status_code, 200)

View File

@ -8,10 +8,10 @@ class DashboardTest(BaseTestCase):
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)