diff --git a/superset/config.py b/superset/config.py index ca9fcbdb6a..a5e4f2988c 100644 --- a/superset/config.py +++ b/superset/config.py @@ -412,6 +412,9 @@ HIVE_POLL_INTERVAL = 5 # 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 diff --git a/superset/migrations/versions/0c5070e96b57_add_user_attributes_table.py b/superset/migrations/versions/0c5070e96b57_add_user_attributes_table.py new file mode 100644 index 0000000000..69eba1b62f --- /dev/null +++ b/superset/migrations/versions/0c5070e96b57_add_user_attributes_table.py @@ -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') diff --git a/superset/models/__init__.py b/superset/models/__init__.py index 18df0e6088..2084aee0c9 100644 --- a/superset/models/__init__.py +++ b/superset/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import core # noqa from . import sql_lab # noqa +from . import user_attributes # noqa diff --git a/superset/models/core.py b/superset/models/core.py index 51e11b18f3..f3b18f8d31 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -17,6 +17,7 @@ import textwrap 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 @@ -28,7 +29,7 @@ from sqlalchemy import ( ) 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 @@ -39,6 +40,7 @@ from superset import app, db, db_engine_specs, security_manager, utils from superset.connectors.connector_registry import ConnectorRegistry from superset.legacy import update_time_range from superset.models.helpers import AuditMixinNullable, ImportMixin, set_perm +from superset.models.user_attributes import UserAttribute from superset.viz import viz_types install_aliases() from urllib import parse # noqa @@ -59,6 +61,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""" diff --git a/superset/models/user_attributes.py b/superset/models/user_attributes.py new file mode 100644 index 0000000000..faf41274dc --- /dev/null +++ b/superset/models/user_attributes.py @@ -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') diff --git a/superset/views/core.py b/superset/views/core.py index ab0f686d08..e6f29039e7 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -45,6 +45,7 @@ from superset.jinja_context import get_template_processor from superset.legacy import cast_form_data, update_time_range 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, @@ -2648,6 +2649,15 @@ class Superset(BaseSupersetView): 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(),