Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit

Permalink
Merge pull request #78 from john-bodley/john-bodley-cherry-picks
Browse files Browse the repository at this point in the history
[cherries] Picking a few cherries
  • Loading branch information
john-bodley authored Aug 13, 2018
2 parents 6713e4a + 9bcb6e3 commit 42bdef3
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 16 deletions.
19 changes: 10 additions & 9 deletions superset/assets/src/explore/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -821,22 +821,23 @@ export const controls = {
'column in the table. Also note that the ' +
'filter below is applied against this column or ' +
'expression'),
default: (c) => {
if (c.options && c.options.length > 0) {
return c.options[0].column_name;
}
return null;
},
default: control => control.default,
clearable: false,
optionRenderer: c => <ColumnOption column={c} showType />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: (state) => {
const newState = {};
const props = {};
if (state.datasource) {
newState.options = state.datasource.columns.filter(c => c.is_dttm);
props.options = state.datasource.columns.filter(c => c.is_dttm);
props.default = null;
if (state.datasource.main_dttm_col) {
props.default = state.datasource.main_dttm_col;
} else if (props.options && props.options.length > 0) {
props.default = props.options[0].column_name;
}
}
return newState;
return props;
},
},

Expand Down
3 changes: 3 additions & 0 deletions superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ class CeleryConfig(object):
# an XSS security vulnerability
ENABLE_JAVASCRIPT_CONTROLS = False

# The id of a template dashboard that should be copied to every new user
DASHBOARD_TEMPLATE_ID = None

# A callable that allows altering the database conneciton URL and params
# on the fly, at runtime. This allows for things like impersonation or
# arbitrary logic. For instance you can wire different users to
Expand Down
1 change: 1 addition & 0 deletions superset/connectors/sqla/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def data(self):
grains = [(g.duration, g.name) for g in grains]
d['granularity_sqla'] = utils.choicify(self.dttm_cols)
d['time_grain_sqla'] = grains
d['main_dttm_col'] = self.main_dttm_col
return d

def values_for_column(self, column_name, limit=10000):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""add user attributes table
Revision ID: 0c5070e96b57
Revises: 7fcdcde0761c
Create Date: 2018-08-06 14:38:18.965248
"""

# revision identifiers, used by Alembic.
revision = '0c5070e96b57'
down_revision = '7fcdcde0761c'

from alembic import op
import sqlalchemy as sa


def upgrade():
op.create_table('user_attribute',
sa.Column('created_on', sa.DateTime(), nullable=True),
sa.Column('changed_on', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('welcome_dashboard_id', sa.Integer(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['welcome_dashboard_id'], ['dashboards.id'], ),
sa.PrimaryKeyConstraint('id')
)


def downgrade():
op.drop_table('user_attribute')
36 changes: 36 additions & 0 deletions superset/migrations/versions/1a1d627ebd8e_position_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""position_json
Revision ID: 1a1d627ebd8e
Revises: 0c5070e96b57
Create Date: 2018-08-13 11:30:07.101702
"""

# revision identifiers, used by Alembic.
revision = '1a1d627ebd8e'
down_revision = '0c5070e96b57'

from alembic import op
import sqlalchemy as sa

from superset.utils import MediumText


def upgrade():
with op.batch_alter_table('dashboards') as batch_op:
batch_op.alter_column(
'position_json',
existing_type=sa.Text(),
type_=MediumText(),
existing_nullable=True,
)


def downgrade():
with op.batch_alter_table('dashboards') as batch_op:
batch_op.alter_column(
'position_json',
existing_type=MediumText(),
type_=sa.Text(),
existing_nullable=True,
)
1 change: 1 addition & 0 deletions superset/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import core # noqa
from . import sql_lab # noqa
from . import user_attributes # noqa
42 changes: 40 additions & 2 deletions superset/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from flask import escape, g, Markup, request
from flask_appbuilder import Model
from flask_appbuilder.models.decorators import renders
from flask_appbuilder.security.sqla.models import User
from future.standard_library import install_aliases
import numpy
import pandas as pd
Expand All @@ -28,7 +29,7 @@
)
from sqlalchemy.engine import url
from sqlalchemy.engine.url import make_url
from sqlalchemy.orm import relationship, subqueryload
from sqlalchemy.orm import relationship, sessionmaker, subqueryload
from sqlalchemy.orm.session import make_transient
from sqlalchemy.pool import NullPool
from sqlalchemy.schema import UniqueConstraint
Expand All @@ -40,6 +41,8 @@
from superset import app, db, db_engine_specs, security_manager, utils
from superset.connectors.connector_registry import ConnectorRegistry
from superset.models.helpers import AuditMixinNullable, ImportMixin, set_perm
from superset.models.user_attributes import UserAttribute
from superset.utils import MediumText
from superset.viz import viz_types
install_aliases()
from urllib import parse # noqa
Expand All @@ -60,6 +63,41 @@ def set_related_perm(mapper, connection, target): # noqa
target.perm = ds.perm


def copy_dashboard(mapper, connection, target):
dashboard_id = config.get('DASHBOARD_TEMPLATE_ID')
if dashboard_id is None:
return

Session = sessionmaker(autoflush=False)
session = Session(bind=connection)
new_user = session.query(User).filter_by(id=target.id).first()

# copy template dashboard to user
template = session.query(Dashboard).filter_by(id=int(dashboard_id)).first()
dashboard = Dashboard(
dashboard_title=template.dashboard_title,
position_json=template.position_json,
description=template.description,
css=template.css,
json_metadata=template.json_metadata,
slices=template.slices,
owners=[new_user],
)
session.add(dashboard)
session.commit()

# set dashboard as the welcome dashboard
extra_attributes = UserAttribute(
user_id=target.id,
welcome_dashboard_id=dashboard.id,
)
session.add(extra_attributes)
session.commit()


sqla.event.listen(User, 'after_insert', copy_dashboard)


class Url(Model, AuditMixinNullable):
"""Used for the short url feature"""

Expand Down Expand Up @@ -326,7 +364,7 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
__tablename__ = 'dashboards'
id = Column(Integer, primary_key=True)
dashboard_title = Column(String(500))
position_json = Column(Text)
position_json = Column(MediumText())
description = Column(Text)
css = Column(Text)
json_metadata = Column(Text)
Expand Down
36 changes: 36 additions & 0 deletions superset/models/user_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from flask_appbuilder import Model
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.orm import relationship

from superset import security_manager
from superset.models.helpers import AuditMixinNullable


class UserAttribute(Model, AuditMixinNullable):

"""
Custom attributes attached to the user.
Extending the user attribute is tricky due to its dependency on the
authentication typew an circular dependencies in Superset. Instead, we use
a custom model for adding attributes.
"""

__tablename__ = 'user_attribute'
id = Column(Integer, primary_key=True) # pylint: disable=invalid-name
user_id = Column(Integer, ForeignKey('ab_user.id'))
user = relationship(
security_manager.user_model,
backref='extra_attributes',
foreign_keys=[user_id],
)

welcome_dashboard_id = Column(Integer, ForeignKey('dashboards.id'))
welcome_dashboard = relationship('Dashboard')
9 changes: 7 additions & 2 deletions superset/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
from pydruid.utils.having import Having
import pytz
import sqlalchemy as sa
from sqlalchemy import event, exc, select
from sqlalchemy import event, exc, select, Text
from sqlalchemy.dialects.mysql import MEDIUMTEXT
from sqlalchemy.types import TEXT, TypeDecorator

from superset.exceptions import SupersetException, SupersetTimeoutException
Expand Down Expand Up @@ -880,7 +881,7 @@ def ensure_path_exists(path):
def convert_legacy_filters_into_adhoc(fd):
mapping = {'having': 'having_filters', 'where': 'filters'}

if 'adhoc_filters' not in fd:
if not fd.get('adhoc_filters'):
fd['adhoc_filters'] = []

for clause, filters in mapping.items():
Expand Down Expand Up @@ -934,3 +935,7 @@ def split_adhoc_filters_into_base_filters(fd):
fd['having'] = ' AND '.join(['({})'.format(sql) for sql in sql_having_filters])
fd['having_filters'] = simple_having_filters
fd['filters'] = simple_where_filters


def MediumText():
return Text().with_variant(MEDIUMTEXT(), 'mysql')
10 changes: 10 additions & 0 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from superset.legacy import cast_form_data
import superset.models.core as models
from superset.models.sql_lab import Query
from superset.models.user_attributes import UserAttribute
from superset.sql_parse import SupersetQuery
from superset.utils import (
merge_extra_filters, merge_request_params, QueryStatus,
Expand Down Expand Up @@ -2701,6 +2702,15 @@ def welcome(self):
if not g.user or not g.user.get_id():
return redirect(appbuilder.get_url_for_login)

welcome_dashboard_id = (
db.session
.query(UserAttribute.welcome_dashboard_id)
.filter_by(user_id=g.user.get_id())
.scalar()
)
if welcome_dashboard_id:
return self.dashboard(str(welcome_dashboard_id))

payload = {
'user': bootstrap_user_data(),
'common': self.common_bootsrap_payload(),
Expand Down
37 changes: 34 additions & 3 deletions tests/utils_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,14 +635,45 @@ def test_convert_legacy_filters_into_adhoc_having_filters(self):
self.assertEquals(form_data, expected)

@patch('superset.utils.to_adhoc', mock_to_adhoc)
def test_convert_legacy_filters_into_adhoc_existing(self):
def test_convert_legacy_filters_into_adhoc_present_and_empty(self):
form_data = {
'adhoc_filters': [],
'where': 'a = 1',
}
expected = {
'adhoc_filters': [
{
'clause': 'WHERE',
'expressionType': 'SQL',
'sqlExpression': 'a = 1',
},
],
}
convert_legacy_filters_into_adhoc(form_data)
self.assertEquals(form_data, expected)

@patch('superset.utils.to_adhoc', mock_to_adhoc)
def test_convert_legacy_filters_into_adhoc_present_and_nonempty(self):
form_data = {
'adhoc_filters': [
{
'clause': 'WHERE',
'expressionType': 'SQL',
'sqlExpression': 'a = 1',
},
],
'filters': [{'col': 'a', 'op': 'in', 'val': 'someval'}],
'having': 'COUNT(1) = 1',
'having_filters': [{'col': 'COUNT(1)', 'op': '==', 'val': 1}],
'where': 'a = 1',
}
expected = {'adhoc_filters': []}
expected = {
'adhoc_filters': [
{
'clause': 'WHERE',
'expressionType': 'SQL',
'sqlExpression': 'a = 1',
},
],
}
convert_legacy_filters_into_adhoc(form_data)
self.assertEquals(form_data, expected)

0 comments on commit 42bdef3

Please sign in to comment.