Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions caravel/migrations/versions/4fa88fe24e94_owners_many_to_many.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""owners_many_to_many

Revision ID: 4fa88fe24e94
Revises: b4456560d4f3
Create Date: 2016-04-15 17:58:33.842012

"""

# revision identifiers, used by Alembic.
revision = '4fa88fe24e94'
down_revision = 'b4456560d4f3'

from alembic import op
import sqlalchemy as sa


def upgrade():
op.create_table('dashboard_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('dashboard_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['dashboard_id'], [u'dashboards.id'], ),
sa.ForeignKeyConstraint(['user_id'], [u'ab_user.id'], ),
sa.PrimaryKeyConstraint('id'),
)
op.create_table('slice_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('slice_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['slice_id'], [u'slices.id'], ),
sa.ForeignKeyConstraint(['user_id'], [u'ab_user.id'], ),
sa.PrimaryKeyConstraint('id'),
)


def downgrade():
op.drop_table('slice_user')
op.drop_table('dashboard_user')
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Materializing permission

Revision ID: c3a8f8611885
Revises: 4fa88fe24e94
Create Date: 2016-04-25 08:54:04.303859

"""

# revision identifiers, used by Alembic.
revision = 'c3a8f8611885'
down_revision = '4fa88fe24e94'

from alembic import op
import sqlalchemy as sa
from caravel import db
from caravel import models


def upgrade():
bind = op.get_bind()
op.add_column('slices', sa.Column('perm', sa.String(length=2000), nullable=True))
session = db.Session(bind=bind)

for slc in session.query(models.Slice).all():
if slc.datasource:
slc.perm = slc.datasource.perm
session.merge(slc)
session.commit()
db.session.close()


def downgrade():
op.drop_column('slices', 'perm')
44 changes: 39 additions & 5 deletions caravel/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from flask.ext.appbuilder import Model
from flask.ext.appbuilder.models.mixins import AuditMixin
from pydruid.client import PyDruid
from flask.ext.appbuilder.models.decorators import renders
from pydruid.utils.filters import Dimension, Filter
from six import string_types
from sqlalchemy import (
Expand Down Expand Up @@ -66,15 +67,15 @@ def changed_by_fk(cls): # noqa
Integer, ForeignKey('ab_user.id'),
default=cls.get_user_id, onupdate=cls.get_user_id, nullable=True)

@property
def created_by_(self): # noqa
@renders('created_by')
def creator(self): # noqa
return '{}'.format(self.created_by or '')

@property # noqa
@renders('changed_by')
def changed_by_(self):
return '{}'.format(self.changed_by or '')

@property
@renders('changed_on')
def modified(self):
s = humanize.naturaltime(datetime.now() - self.changed_on)
return '<span class="no-wrap">{}</nobr>'.format(s)
Expand Down Expand Up @@ -110,6 +111,13 @@ class CssTemplate(Model, AuditMixinNullable):
css = Column(Text, default='')


slice_user = Table('slice_user', Model.metadata,
Column('id', Integer, primary_key=True),
Column('user_id', Integer, ForeignKey('ab_user.id')),
Column('slice_id', Integer, ForeignKey('slices.id'))
)


class Slice(Model, AuditMixinNullable):

"""A slice is essentially a report or a view on data"""
Expand All @@ -125,11 +133,13 @@ class Slice(Model, AuditMixinNullable):
params = Column(Text)
description = Column(Text)
cache_timeout = Column(Integer)
perm = Column(String(2000))

table = relationship(
'SqlaTable', foreign_keys=[table_id], backref='slices')
druid_datasource = relationship(
'DruidDatasource', foreign_keys=[druid_datasource_id], backref='slices')
owners = relationship("User", secondary=slice_user)

def __repr__(self):
return self.slice_name
Expand Down Expand Up @@ -211,13 +221,34 @@ def slice_link(self):
url=url, obj=self)


def set_perm(mapper, connection, target): # noqa
if target.table_id:
src_class = SqlaTable
id_ = target.table_id
elif target.druid_datasource_id:
src_class = DruidDatasource
id_ = target.druid_datasource_id
ds = db.session.query(src_class).filter_by(id=int(id_)).first()
target.perm = ds.perm

sqla.event.listen(Slice, 'before_insert', set_perm)
sqla.event.listen(Slice, 'before_update', set_perm)


dashboard_slices = Table(
'dashboard_slices', Model.metadata,
Column('id', Integer, primary_key=True),
Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
Column('slice_id', Integer, ForeignKey('slices.id')),
)

dashboard_user = Table(
'dashboard_user', Model.metadata,
Column('id', Integer, primary_key=True),
Column('user_id', Integer, ForeignKey('ab_user.id')),
Column('dashboard_id', Integer, ForeignKey('dashboards.id'))
)


class Dashboard(Model, AuditMixinNullable):

Expand All @@ -233,6 +264,7 @@ class Dashboard(Model, AuditMixinNullable):
slug = Column(String(255), unique=True)
slices = relationship(
'Slice', secondary=dashboard_slices, backref='dashboards')
owners = relationship("User", secondary=dashboard_user)

def __repr__(self):
return self.dashboard_title
Expand Down Expand Up @@ -1165,11 +1197,13 @@ def wrapper(*args, **kwargs):
user_id = g.user.id
d = request.args.to_dict()
d.update(kwargs)
slice_id = d.get('slice_id', 0)
slice_id = int(slice_id) if slice_id else 0
log = cls(
action=f.__name__,
json=json.dumps(d),
dashboard_id=d.get('dashboard_id') or None,
slice_id=d.get('slice_id') or None,
slice_id=slice_id,
user_id=user_id)
db.session.add(log)
db.session.commit()
Expand Down
74 changes: 51 additions & 23 deletions caravel/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
from flask.ext.appbuilder.actions import action
from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
from flask.ext.appbuilder.security.decorators import has_access
from flask_appbuilder.models.sqla.filters import BaseFilter
from pydruid.client import doublesum
from sqlalchemy import create_engine
from sqlalchemy import select, text
from sqlalchemy import create_engine, select, text
from sqlalchemy.sql.expression import TextAsFrom
from werkzeug.routing import BaseConverter
from wtforms.validators import ValidationError
Expand All @@ -32,6 +32,42 @@
log_this = models.Log.log_this


class CaravelFilter(BaseFilter):
def get_perms(self):
perms = []
for role in g.user.roles:
for perm_view in role.permissions:
if perm_view.permission.name == 'datasource_access':
perms.append(perm_view.view_menu.name)
return perms


class FilterSlice(CaravelFilter):
def apply(self, query, func): # noqa
if any([r.name in ('Admin', 'Alpha') for r in g.user.roles]):
return query
qry = query.filter(self.model.perm.in_(self.get_perms()))
print(qry)
return qry


class FilterDashboard(CaravelFilter):
def apply(self, query, func): # noqa
if any([r.name in ('Admin', 'Alpha') for r in g.user.roles]):
return query
Slice = models.Slice # noqa
slice_ids_qry = (
db.session
.query(Slice.id)
.filter(Slice.perm.in_(self.get_perms()))
)
return query.filter(
self.model.slices.any(
models.Slice.id.in_(slice_ids_qry)
)
)


def validate_json(form, field): # noqa
try:
json.loads(field.data)
Expand Down Expand Up @@ -136,8 +172,7 @@ class DruidMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa

class DatabaseView(CaravelModelView, DeleteMixin): # noqa
datamodel = SQLAInterface(models.Database)
list_columns = ['database_name', 'sql_link', 'created_by_', 'changed_on']
order_columns = utils.list_minus(list_columns, ['created_by_'])
list_columns = ['database_name', 'sql_link', 'creator', 'changed_on']
add_columns = [
'database_name', 'sqlalchemy_uri', 'cache_timeout', 'extra']
search_exclude_columns = ('password',)
Expand Down Expand Up @@ -183,7 +218,7 @@ class TableModelView(CaravelModelView, DeleteMixin): # noqa
datamodel = SQLAInterface(models.SqlaTable)
list_columns = [
'table_link', 'database', 'sql_link', 'is_featured',
'changed_by_', 'changed_on']
'changed_by_', 'changed_on', 'perm']
add_columns = [
'table_name', 'database', 'schema',
'default_endpoint', 'offset', 'cache_timeout']
Expand Down Expand Up @@ -246,21 +281,19 @@ class DruidClusterModelView(CaravelModelView, DeleteMixin): # noqa
category_icon='fa-database',)



class SliceModelView(CaravelModelView, DeleteMixin): # noqa
datamodel = SQLAInterface(models.Slice)
add_template = "caravel/add_slice.html"
can_add = False
label_columns = {
'created_by_': 'Creator',
'datasource_link': 'Datasource',
}
list_columns = [
'slice_link', 'viz_type',
'datasource_link', 'created_by_', 'modified']
order_columns = utils.list_minus(list_columns, ['created_by_', 'modified'])
'slice_link', 'viz_type', 'datasource_link', 'creator', 'modified']
edit_columns = [
'slice_name', 'description', 'viz_type', 'druid_datasource',
'table', 'dashboards', 'params', 'cache_timeout']
'table', 'owners', 'dashboards', 'params', 'cache_timeout']
base_order = ('changed_on', 'desc')
description_columns = {
'description': Markup(
Expand All @@ -269,6 +302,7 @@ class SliceModelView(CaravelModelView, DeleteMixin): # noqa
"<a href='https://daringfireball.net/projects/markdown/'>"
"markdown</a>"),
}
base_filters = [['id', FilterSlice, lambda: []]]

appbuilder.add_view(
SliceModelView,
Expand All @@ -281,10 +315,9 @@ class SliceModelView(CaravelModelView, DeleteMixin): # noqa
class SliceAsync(SliceModelView): # noqa
list_columns = [
'slice_link', 'viz_type',
'created_by_', 'modified', 'icons']
'creator', 'modified', 'icons']
label_columns = {
'icons': ' ',
'created_by_': 'Creator',
'viz_type': 'Type',
'slice_link': 'Slice',
}
Expand All @@ -294,13 +327,9 @@ class SliceAsync(SliceModelView): # noqa

class DashboardModelView(CaravelModelView, DeleteMixin): # noqa
datamodel = SQLAInterface(models.Dashboard)
label_columns = {
'created_by_': 'Creator',
}
list_columns = ['dashboard_link', 'created_by_', 'modified']
order_columns = utils.list_minus(list_columns, ['created_by_', 'modified'])
list_columns = ['dashboard_link', 'creator', 'modified']
edit_columns = [
'dashboard_title', 'slug', 'slices', 'position_json', 'css',
'dashboard_title', 'slug', 'slices', 'owners', 'position_json', 'css',
'json_metadata']
add_columns = edit_columns
base_order = ('changed_on', 'desc')
Expand All @@ -316,6 +345,7 @@ class DashboardModelView(CaravelModelView, DeleteMixin): # noqa
"visible"),
'slug': "To get a readable URL for your dashboard",
}
base_filters = [['slice', FilterDashboard, lambda: []]]

def pre_add(self, obj):
obj.slug = obj.slug.strip() or None
Expand All @@ -336,9 +366,8 @@ def pre_update(self, obj):


class DashboardModelViewAsync(DashboardModelView): # noqa
list_columns = ['dashboard_link', 'created_by_', 'modified']
list_columns = ['dashboard_link', 'creator', 'modified']
label_columns = {
'created_by_': 'Creator',
'dashboard_link': 'Dashboard',
}

Expand All @@ -362,11 +391,10 @@ class DruidDatasourceModelView(CaravelModelView, DeleteMixin): # noqa
datamodel = SQLAInterface(models.DruidDatasource)
list_columns = [
'datasource_link', 'cluster', 'owner',
'created_by_', 'created_on',
'creator', 'created_on',
'changed_by_', 'changed_on',
'offset']
related_views = [
DruidColumnInlineView, DruidMetricInlineView]
related_views = [DruidColumnInlineView, DruidMetricInlineView]
edit_columns = [
'datasource_name', 'cluster', 'description', 'owner',
'is_featured', 'is_hidden', 'default_endpoint', 'offset',
Expand Down
Loading