diff --git a/caravel/assets/javascripts/explore.js b/caravel/assets/javascripts/explore.js index 2f7ded6b00fd2..c1fda16560fff 100644 --- a/caravel/assets/javascripts/explore.js +++ b/caravel/assets/javascripts/explore.js @@ -333,23 +333,6 @@ function initExploreView() { $("#having_panel #plus").click(function () { add_filter(undefined, "having"); }); - $("#btn_save").click(function () { - var slice_name = prompt("Name your slice!"); - if (slice_name !== "" && slice_name !== null) { - $("#slice_name").val(slice_name); - prepForm(); - $("#action").val("save"); - $("#query").submit(); - } - }); - $("#btn_overwrite").click(function () { - var flag = confirm("Overwrite slice [" + $("#slice_name").val() + "] !?"); - if (flag) { - $("#action").val("overwrite"); - prepForm(); - $("#query").submit(); - } - }); $(".query").click(function () { query(true); @@ -400,6 +383,78 @@ function initExploreView() { }); $(this).remove(); }); + + function prepSaveDialog() { + var setButtonsState = function () { + var add_to_dash = $("input[name=add_to_dash]:checked").val(); + if (add_to_dash === 'existing' || add_to_dash === 'new') { + $('.gotodash').removeAttr('disabled'); + } else { + $('.gotodash').prop('disabled', true); + } + }; + var url = '/dashboardmodelviewasync/api/read'; + url += '?_flt_0_owners=' + $('#userid').val(); + $.get(url, function (data) { + var choices = []; + for (var i=0; i< data.pks.length; i++) { + choices.push({ id: data.pks[i], text: data.result[i].dashboard_title }); + } + $('#save_to_dashboard_id').select2({ + data: choices, + dropdownAutoWidth: true + }).on("select2-selecting", function () { + $("#add_to_dash_existing").prop("checked", true); + setButtonsState(); + }); + }); + + $("input[name=add_to_dash]").change(setButtonsState); + $("input[name='new_dashboard_name']").on('focus', function () { + $("#add_to_new_dash").prop("checked", true); + setButtonsState(); + }); + $("input[name='new_slice_name']").on('focus', function () { + $("#save_as_new").prop("checked", true); + setButtonsState(); + }); + $('#btn_modal_save').click(function () { + var action = $('input[name=rdo_save]:checked').val(); + if (action === 'saveas') { + var slice_name = $('input[name=new_slice_name]').val(); + if (slice_name === '') { + showModal({ + title: "Error", + body: "You must pick a name for the new slice" + }); + return; + } + document.getElementById("slice_name").value = slice_name; + } + var add_to_dash = $('input[name=add_to_dash]:checked').val(); + if (add_to_dash === 'existing' && $('#save_to_dashboard_id').val() === '') { + showModal({ + title: "Error", + body: "You must pick an existing dashboard" + }); + return; + } else if (add_to_dash === 'new' && $('input[name=new_dashboard_name]').val() === '') { + showModal({ + title: "Error", + body: "Please enter a name for the new dashboard" + }); + return; + } + $('#action').val(action); + prepForm(); + $("#query").submit(); + }); + $('#btn_modal_save_goto_dash').click(function () { + document.getElementById("goto_dash").value = 'true'; + $('#btn_modal_save').click(); + }); + } + prepSaveDialog(); } $(document).ready(function () { diff --git a/caravel/assets/stylesheets/explore.css b/caravel/assets/stylesheets/explore.css index 1666f9fc4774a..8aa0ae21a1149 100644 --- a/caravel/assets/stylesheets/explore.css +++ b/caravel/assets/stylesheets/explore.css @@ -7,3 +7,12 @@ left: 0; top: 0; } + +.header hr { + margin-top: 10px; + margin-bottom: 10px; +} + +.navbar { + margin-bottom: 10px; +} diff --git a/caravel/migrations/versions/27ae655e4247_make_creator_owners.py b/caravel/migrations/versions/27ae655e4247_make_creator_owners.py new file mode 100644 index 0000000000000..71c627305dbc7 --- /dev/null +++ b/caravel/migrations/versions/27ae655e4247_make_creator_owners.py @@ -0,0 +1,31 @@ +"""Make creator owners + +Revision ID: 27ae655e4247 +Revises: d8bc074f7aad +Create Date: 2016-06-27 08:43:52.592242 + +""" + +# revision identifiers, used by Alembic. +revision = '27ae655e4247' +down_revision = 'd8bc074f7aad' + +from alembic import op +from caravel import db, models + + +def upgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + + objects = session.query(models.Slice).all() + objects += session.query(models.Dashboard).all() + for obj in objects: + if obj.created_by and obj.created_by not in obj.owners: + obj.owners.append(obj.created_by) + session.commit() + session.close() + + +def downgrade(): + pass diff --git a/caravel/migrations/versions/960c69cb1f5b_.py b/caravel/migrations/versions/960c69cb1f5b_.py index d304d539d6378..62ee976577d11 100644 --- a/caravel/migrations/versions/960c69cb1f5b_.py +++ b/caravel/migrations/versions/960c69cb1f5b_.py @@ -8,7 +8,7 @@ # revision identifiers, used by Alembic. revision = '960c69cb1f5b' -down_revision = 'd8bc074f7aad' +down_revision = '27ae655e4247' from alembic import op import sqlalchemy as sa diff --git a/caravel/templates/caravel/dashboard.html b/caravel/templates/caravel/dashboard.html index 861f1e318dc8d..12f5d443f93af 100644 --- a/caravel/templates/caravel/dashboard.html +++ b/caravel/templates/caravel/dashboard.html @@ -75,26 +75,28 @@

-
- - - - - - - - -
+ {% if dash_edit_perm %} +
+ + + + + + + + +
+ {% endif %}
diff --git a/caravel/templates/caravel/explore.html b/caravel/templates/caravel/explore.html index 0c01fc39ea950..7c63f83a7f98c 100644 --- a/caravel/templates/caravel/explore.html +++ b/caravel/templates/caravel/explore.html @@ -29,6 +29,20 @@
+ {% endblock %} diff --git a/caravel/views.py b/caravel/views.py index 57cb39819fbb8..9c4b1cbf10610 100644 --- a/caravel/views.py +++ b/caravel/views.py @@ -85,6 +85,8 @@ def check_ownership(obj, raise_if_false=True): model. It is meant to be used in the ModelView's pre_update hook in which raising will abort the update. """ + if not obj: + return False roles = (r.name for r in get_user_roles()) if 'Admin' in roles: return True @@ -96,7 +98,11 @@ def check_ownership(obj, raise_if_false=True): orig_obj.created_by and orig_obj.created_by.username == g.user.username): return True - if hasattr(orig_obj, 'owners') and g.user.username in owner_names: + if ( + hasattr(orig_obj, 'owners') and + g.user and + hasattr(g.user, 'username') and + g.user.username in owner_names): return True if raise_if_false: raise utils.CaravelSecurityException( @@ -239,7 +245,6 @@ class TableColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa appbuilder.add_view_no_menu(TableColumnInlineView) - class DruidColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa datamodel = SQLAInterface(models.DruidColumn) edit_columns = [ @@ -493,7 +498,6 @@ class DruidClusterModelView(CaravelModelView, DeleteMixin): # noqa category_icon='fa-database',) - class SliceModelView(CaravelModelView, DeleteMixin): # noqa datamodel = SQLAInterface(models.Slice) add_template = "caravel/add_slice.html" @@ -630,7 +634,7 @@ def pre_update(self, obj): class DashboardModelViewAsync(DashboardModelView): # noqa - list_columns = ['dashboard_link', 'creator', 'modified'] + list_columns = ['dashboard_link', 'creator', 'modified', 'dashboard_title'] label_columns = { 'dashboard_link': 'Dashboard', } @@ -758,6 +762,7 @@ class Caravel(BaseCaravelView): @expose("/datasource///") # Legacy url @log_this def explore(self, datasource_type, datasource_id): + error_redirect = '/slicemodelview/list/' datasource_class = models.SqlaTable \ if datasource_type == "table" else models.DruidDatasource @@ -771,9 +776,6 @@ def explore(self, datasource_type, datasource_id): datasource = datasource[0] if datasource else None slice_id = request.args.get("slice_id") slc = None - slice_add_perm = self.can_access('can_add', 'SliceModelView') - slice_edit_perm = self.can_access('can_edit', 'SliceModelView') - slice_download_perm = self.can_access('can_download', 'SliceModelView') if slice_id: slc = ( @@ -785,6 +787,10 @@ def explore(self, datasource_type, datasource_id): flash(__("The datasource seems to have been deleted"), "alert") return redirect(error_redirect) + slice_add_perm = self.can_access('can_add', 'SliceModelView') + slice_edit_perm = check_ownership(slc, raise_if_false=False) + slice_download_perm = self.can_access('can_download', 'SliceModelView') + all_datasource_access = self.can_access( 'all_datasource_access', 'all_datasource_access') datasource_access = self.can_access( @@ -794,7 +800,7 @@ def explore(self, datasource_type, datasource_id): return redirect(error_redirect) action = request.args.get('action') - if action in ('save', 'overwrite'): + if action in ('saveas', 'overwrite'): return self.save_or_overwrite_slice( request.args, slc, slice_add_perm, slice_edit_perm) @@ -840,11 +846,11 @@ def explore(self, datasource_type, datasource_id): template = "caravel/standalone.html" else: template = "caravel/explore.html" - resp = self.render_template( template, viz=obj, slice=slc, datasources=datasources, can_add=slice_add_perm, can_edit=slice_edit_perm, - can_download=slice_download_perm) + can_download=slice_download_perm, + userid=g.user.get_id() if g.user else '') try: pass except Exception as e: @@ -856,8 +862,9 @@ def explore(self, datasource_type, datasource_id): mimetype="application/json") return resp - def save_or_overwrite_slice(self, args, slc, slice_add_perm, slice_edit_perm): - """save or overwrite a slice""" + def save_or_overwrite_slice( + self, args, slc, slice_add_perm, slice_edit_perm): + """Save or overwrite a slice""" slice_name = args.get('slice_name') action = args.get('action') @@ -865,6 +872,7 @@ def save_or_overwrite_slice(self, args, slc, slice_add_perm, slice_edit_perm): d = args.to_dict(flat=False) del d['action'] del d['previous_viz_type'] + as_list = ('metrics', 'groupby', 'columns', 'all_columns', 'mapbox_label') for k in d: v = d.get(k) @@ -880,8 +888,8 @@ def save_or_overwrite_slice(self, args, slc, slice_add_perm, slice_edit_perm): elif datasource_type == 'table': table_id = args.get('datasource_id') - if action == "save": - slc = models.Slice() + if action in ('saveas'): + slc = models.Slice(owners=[g.user] if g.user else []) slc.params = json.dumps(d, indent=4, sort_keys=True) slc.datasource_name = args.get('datasource_name') @@ -891,12 +899,43 @@ def save_or_overwrite_slice(self, args, slc, slice_add_perm, slice_edit_perm): slc.datasource_type = datasource_type slc.slice_name = slice_name - if action == 'save' and slice_add_perm: + if action in ('saveas') and slice_add_perm: self.save_slice(slc) elif action == 'overwrite' and slice_edit_perm: self.overwrite_slice(slc) - return redirect(slc.slice_url) + # Adding slice to a dashboard if requested + dash = None + if request.args.get('add_to_dash') == 'existing': + dash = ( + db.session.query(models.Dashoard) + .filter_by(id=int(request.args.get('save_to_dashboard_id'))) + .one() + ) + flash( + "Slice [{}] was added to dashboard [{}]".format( + slc.slice_name, + dash.dashboard_title), + "info") + elif request.args.get('add_to_dash') == 'new': + dash = models.Dashoard( + dashboard_title=request.args.get('new_dashboard_name'), + owners=[g.user] if g.user else []) + flash( + "Dashboard [{}] just got created and slice [{}] was added " + "to it".format( + dash.dashboard_title, + slc.slice_name), + "info") + + if dash and slc not in dash.slices: + dash.slices.append(slc) + db.session.commit() + + if request.args.get('goto_dash') == 'true': + return redirect(dash.url) + else: + return redirect(slc.slice_url) def save_slice(self, slc): session = db.session() @@ -981,7 +1020,11 @@ def testconn(self): """Tests a sqla connection""" try: uri = request.json.get('uri') - connect_args = request.json.get('extras', {}).get('engine_params', {}).get('connect_args', {}) + connect_args = ( + request.json + .get('extras', {}) + .get('engine_params', {}) + .get('connect_args', {})) engine = create_engine(uri, connect_args=connect_args) engine.connect() return json.dumps(engine.table_names(), indent=4) @@ -1052,7 +1095,7 @@ def dashboard(**kwargs): # noqa "caravel/dashboard.html", dashboard=dash, templates=templates, dash_save_perm=self.can_access('can_save_dash', 'Caravel'), - dash_edit_perm=self.can_access('can_edit', 'DashboardModelView')) + dash_edit_perm=check_ownership(dash, raise_if_false=False)) @has_access @expose("/sql//")