From ef763b7157a9725ec6a5ea6c78b349ba3fddf82f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 18 Apr 2015 04:07:01 -0700 Subject: [PATCH 1/7] Use Flask-Admin to provide basic Web-based /admin page --- redash/admin.py | 39 +++++++++++++++++++++++++++++++++++++++ redash/wsgi.py | 3 +++ requirements.txt | 3 +++ 3 files changed, 45 insertions(+) create mode 100644 redash/admin.py diff --git a/redash/admin.py b/redash/admin.py new file mode 100644 index 0000000000..704d81506e --- /dev/null +++ b/redash/admin.py @@ -0,0 +1,39 @@ +from flask_admin.contrib.peewee import ModelView +from flask.ext.admin import Admin +from flask_admin.contrib.peewee.form import CustomModelConverter +from flask_admin.form.widgets import DateTimePickerWidget +from playhouse.postgres_ext import ArrayField, DateTimeTZField +from wtforms import fields + +from redash import models +from redash.permissions import require_permission + + +class PgModelConverter(CustomModelConverter): + def __init__(self, view, additional=None): + additional = {ArrayField: self.handle_array_field, + DateTimeTZField: self.handle_datetime_tz_field} + super(CustomModelConverter, self).__init__(additional) + self.view = view + + def handle_array_field(self, model, field, **kwargs): + return field.name, fields.StringField(**kwargs) + + def handle_datetime_tz_field(self, model, field, **kwargs): + kwargs['widget'] = DateTimePickerWidget() + return field.name, fields.DateTimeField(**kwargs) + + +class PgModelView(ModelView): + model_form_converter = PgModelConverter + + @require_permission('admin') + def is_accessible(self): + return True + + +def init_admin(app): + admin = Admin(app, name='redash') + + for m in models.all_models: + admin.add_view(PgModelView(m)) diff --git a/redash/wsgi.py b/redash/wsgi.py index 59e68dfd84..c775362f86 100644 --- a/redash/wsgi.py +++ b/redash/wsgi.py @@ -4,6 +4,8 @@ from redash import settings, utils from redash.models import db +from redash.admin import init_admin + __version__ = '0.4.0' @@ -14,6 +16,7 @@ api = Api(app) +init_admin(app) # configure our database settings.DATABASE_CONFIG.update({'threadlocals': True}) diff --git a/requirements.txt b/requirements.txt index b600f93fe9..16ec00d9eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Flask==0.10.1 +Flask-Admin==1.1.0 Flask-RESTful==0.2.10 Flask-Login==0.2.9 Flask-OAuth==0.12 @@ -26,3 +27,5 @@ celery==3.1.11 jsonschema==2.4.0 click==3.3 RestrictedPython==3.6.0 +peewee==2.4.7 +wtf-peewee==0.2.3 From 7f8b738b9ea9f8ad4dcd4038d3ec2dbfe76d8609 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Sat, 18 Apr 2015 16:58:05 +0300 Subject: [PATCH 2/7] Fix requirements.txt (peewee was specified twice) --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 16ec00d9eb..1c6b290174 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,5 +27,4 @@ celery==3.1.11 jsonschema==2.4.0 click==3.3 RestrictedPython==3.6.0 -peewee==2.4.7 wtf-peewee==0.2.3 From 68e3e8e1c5e39804f2dd1b48028aaf1a139ba201 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Sat, 18 Apr 2015 18:00:52 +0300 Subject: [PATCH 3/7] Update name in admin screens --- redash/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redash/admin.py b/redash/admin.py index 704d81506e..3c5862313f 100644 --- a/redash/admin.py +++ b/redash/admin.py @@ -33,7 +33,7 @@ def is_accessible(self): def init_admin(app): - admin = Admin(app, name='redash') + admin = Admin(app, name='re:dash') for m in models.all_models: admin.add_view(PgModelView(m)) From fcd9ab533cf0ff88a09b98b6145939064907c9f5 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Sat, 18 Apr 2015 18:46:32 +0300 Subject: [PATCH 4/7] Fix: correctly call CustomModelConverter __init__. --- redash/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redash/admin.py b/redash/admin.py index 3c5862313f..d606699f94 100644 --- a/redash/admin.py +++ b/redash/admin.py @@ -13,7 +13,7 @@ class PgModelConverter(CustomModelConverter): def __init__(self, view, additional=None): additional = {ArrayField: self.handle_array_field, DateTimeTZField: self.handle_datetime_tz_field} - super(CustomModelConverter, self).__init__(additional) + super(PgModelConverter, self).__init__(view, additional) self.view = view def handle_array_field(self, model, field, **kwargs): From dba325e9a2256c5f3b8febabd1d481c724f513b0 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Sat, 18 Apr 2015 18:47:54 +0300 Subject: [PATCH 5/7] Use ArrayListField for Array fields. --- redash/admin.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/redash/admin.py b/redash/admin.py index d606699f94..21964b098d 100644 --- a/redash/admin.py +++ b/redash/admin.py @@ -4,11 +4,39 @@ from flask_admin.form.widgets import DateTimePickerWidget from playhouse.postgres_ext import ArrayField, DateTimeTZField from wtforms import fields +from wtforms.widgets import TextInput from redash import models from redash.permissions import require_permission +class ArrayListField(fields.Field): + widget = TextInput() + + def _value(self): + if self.data: + return u', '.join(self.data) + else: + return u'' + + def process_formdata(self, valuelist): + if valuelist: + self.data = [x.strip() for x in valuelist[0].split(',')] + else: + self.data = [] + + +class PasswordHashField(fields.PasswordField): + def _value(self): + return u'' + + def process_formdata(self, valuelist): + if valuelist: + self.data = models.pwd_context.encrypt(valuelist[0]) + else: + self.data = u'' + + class PgModelConverter(CustomModelConverter): def __init__(self, view, additional=None): additional = {ArrayField: self.handle_array_field, @@ -17,7 +45,7 @@ def __init__(self, view, additional=None): self.view = view def handle_array_field(self, model, field, **kwargs): - return field.name, fields.StringField(**kwargs) + return field.name, ArrayListField(**kwargs) def handle_datetime_tz_field(self, model, field, **kwargs): kwargs['widget'] = DateTimePickerWidget() From 10a6ac931318fe54ff4f3f4c5e64aaf43d94a24e Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Sat, 18 Apr 2015 18:48:44 +0300 Subject: [PATCH 6/7] Dedicated view for User model --- redash/admin.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/redash/admin.py b/redash/admin.py index 21964b098d..5efe6c0091 100644 --- a/redash/admin.py +++ b/redash/admin.py @@ -52,7 +52,7 @@ def handle_datetime_tz_field(self, model, field, **kwargs): return field.name, fields.DateTimeField(**kwargs) -class PgModelView(ModelView): +class BaseModelView(ModelView): model_form_converter = PgModelConverter @require_permission('admin') @@ -60,8 +60,26 @@ def is_accessible(self): return True +class UserModelView(BaseModelView): + column_searchable_list = ('name', 'email') + form_excluded_columns = ('created_at', 'updated_at') + column_exclude_list = ('password_hash',) + + form_overrides = dict(password_hash=PasswordHashField) + form_args = { + 'password_hash': {'label': 'Password'} + } + + def init_admin(app): admin = Admin(app, name='re:dash') + views = { + models.User: UserModelView + } + for m in models.all_models: - admin.add_view(PgModelView(m)) + if m in views: + admin.add_view(views[m](m)) + else: + admin.add_view(BaseModelView(m)) From 9683a8ed82f09a1ffc650fb13ca5a118ec62ad55 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Sat, 18 Apr 2015 19:40:18 +0300 Subject: [PATCH 7/7] Dedicated view for data source --- redash/admin.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/redash/admin.py b/redash/admin.py index 5efe6c0091..7144b8c7a0 100644 --- a/redash/admin.py +++ b/redash/admin.py @@ -1,3 +1,4 @@ +import json from flask_admin.contrib.peewee import ModelView from flask.ext.admin import Admin from flask_admin.contrib.peewee.form import CustomModelConverter @@ -7,6 +8,7 @@ from wtforms.widgets import TextInput from redash import models +from redash import query_runner from redash.permissions import require_permission @@ -26,6 +28,17 @@ def process_formdata(self, valuelist): self.data = [] +class JSONTextAreaField(fields.TextAreaField): + def process_formdata(self, valuelist): + if valuelist: + try: + json.loads(valuelist[0]) + except ValueError: + raise ValueError(self.gettext(u'Invalid JSON')) + self.data = valuelist[0] + else: + self.data = '' + class PasswordHashField(fields.PasswordField): def _value(self): return u'' @@ -71,15 +84,33 @@ class UserModelView(BaseModelView): } +def query_runner_type_formatter(view, context, model, name): + qr = query_runner.query_runners.get(model.type, None) + if qr: + return qr.name() + + return model.type + + +class DataSourceModelView(BaseModelView): + form_overrides = dict(type=fields.SelectField, options=JSONTextAreaField) + form_args = dict(type={ + 'choices': [(k, r.name()) for k, r in query_runner.query_runners.iteritems()] + }) + column_formatters = dict(type=query_runner_type_formatter) + column_filters = ('type',) + + def init_admin(app): - admin = Admin(app, name='re:dash') + admin = Admin(app, name='re:dash admin') views = { - models.User: UserModelView + models.User: UserModelView(models.User), + models.DataSource: DataSourceModelView(models.DataSource) } for m in models.all_models: if m in views: - admin.add_view(views[m](m)) + admin.add_view(views[m]) else: admin.add_view(BaseModelView(m))