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//")