diff --git a/redash/admin.py b/redash/admin.py new file mode 100644 index 0000000000..7144b8c7a0 --- /dev/null +++ b/redash/admin.py @@ -0,0 +1,116 @@ +import json +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 wtforms.widgets import TextInput + +from redash import models +from redash import query_runner +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 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'' + + 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, + DateTimeTZField: self.handle_datetime_tz_field} + super(PgModelConverter, self).__init__(view, additional) + self.view = view + + def handle_array_field(self, model, field, **kwargs): + return field.name, ArrayListField(**kwargs) + + def handle_datetime_tz_field(self, model, field, **kwargs): + kwargs['widget'] = DateTimePickerWidget() + return field.name, fields.DateTimeField(**kwargs) + + +class BaseModelView(ModelView): + model_form_converter = PgModelConverter + + @require_permission('admin') + 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 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') + + views = { + 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]) + else: + admin.add_view(BaseModelView(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..1c6b290174 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,4 @@ celery==3.1.11 jsonschema==2.4.0 click==3.3 RestrictedPython==3.6.0 +wtf-peewee==0.2.3