Add "Published" feature to dashboards (#4725)
* Allow users to publish dashboards * Rework publish dashboards feature - The eye next to the title has been replaced with a [draft] badge - Published status is now toggled in the Header Action Dropdown - CRUD list shows published status * Fix linter errors * Update javascript tests * Add tests and change DashboardFilter Add some tests to make sure the published status is rendered and Make it so that users cannot see dashboards that are published if they don't have access to any of the slices within * Fix some linter errors * Remove commas from core.py * Fix some failing tests * More linter errors I introduced * Fix more linter errors I introduced * update alembic migration * Update design of publish dash feature * Upgrade migration version * Secure publish endpoint * Remove bad quotes * Give publish span its own style * fix publish rendering * Add new test for publish feature * Update migration * update slug in test * Update migration * Address reviwer comments * Fix linter errors * Add licenses * Remove fetchPublished(), use bootstrap data * Update migration * Update croniter to existing version * Fix linter errors * Upgrade DB Revisions * Fix flake8 linter error * Set all dashboards to published on migration * Migration proper line spacing * Fix migration to work with postgres * UPDATE statement works with postgresql and sqlite hopefully * Update wording to kick off travis
This commit is contained in:
parent
8d81c3049a
commit
97ffb762d0
|
|
@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
|
|||
import Header from '../../../../src/dashboard/components/Header';
|
||||
import EditableTitle from '../../../../src/components/EditableTitle';
|
||||
import FaveStar from '../../../../src/components/FaveStar';
|
||||
import PublishedStatus from '../../../../src/dashboard/components/PublishedStatus';
|
||||
import HeaderActionsDropdown from '../../../../src/dashboard/components/HeaderActionsDropdown';
|
||||
import Button from '../../../../src/components/Button';
|
||||
import UndoRedoKeylisteners from '../../../../src/dashboard/components/UndoRedoKeylisteners';
|
||||
|
|
@ -43,6 +44,8 @@ describe('Header', () => {
|
|||
fetchFaveStar: () => {},
|
||||
fetchCharts: () => {},
|
||||
saveFaveStar: () => {},
|
||||
savePublished: () => {},
|
||||
isPublished: () => {},
|
||||
startPeriodicRender: () => {},
|
||||
updateDashboardTitle: () => {},
|
||||
editMode: false,
|
||||
|
|
@ -78,6 +81,11 @@ describe('Header', () => {
|
|||
expect(wrapper.find(EditableTitle)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the PublishedStatus', () => {
|
||||
const wrapper = setup(overrideProps);
|
||||
expect(wrapper.find(PublishedStatus)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the FaveStar', () => {
|
||||
const wrapper = setup(overrideProps);
|
||||
expect(wrapper.find(FaveStar)).toHaveLength(1);
|
||||
|
|
@ -110,6 +118,11 @@ describe('Header', () => {
|
|||
expect(wrapper.find(EditableTitle)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the PublishedStatus', () => {
|
||||
const wrapper = setup(overrideProps);
|
||||
expect(wrapper.find(PublishedStatus)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the FaveStar', () => {
|
||||
const wrapper = setup(overrideProps);
|
||||
expect(wrapper.find(FaveStar)).toHaveLength(1);
|
||||
|
|
@ -147,6 +160,11 @@ describe('Header', () => {
|
|||
expect(wrapper.find(FaveStar)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the PublishedStatus', () => {
|
||||
const wrapper = setup(overrideProps);
|
||||
expect(wrapper.find(PublishedStatus)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render the HeaderActionsDropdown', () => {
|
||||
const wrapper = setup(overrideProps);
|
||||
expect(wrapper.find(HeaderActionsDropdown)).toHaveLength(1);
|
||||
|
|
|
|||
|
|
@ -99,6 +99,32 @@ export function saveFaveStar(id, isStarred) {
|
|||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_PUBLISHED = 'TOGGLE_PUBLISHED';
|
||||
export function togglePublished(isPublished) {
|
||||
return { type: TOGGLE_PUBLISHED, isPublished };
|
||||
}
|
||||
|
||||
export function savePublished(id, isPublished) {
|
||||
return function savePublishedThunk(dispatch) {
|
||||
return SupersetClient.post({
|
||||
endpoint: `/superset/dashboard/${id}/published/`,
|
||||
postPayload: { published: isPublished },
|
||||
})
|
||||
.then(() => {
|
||||
const nowPublished = isPublished ? 'published' : 'hidden';
|
||||
dispatch(addSuccessToast(t(`This dashboard is now ${nowPublished}`)));
|
||||
dispatch(togglePublished(isPublished));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
addDangerToast(
|
||||
t('You do not have permissions to edit this dashboard.'),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
|
||||
export function toggleExpandSlice(sliceId) {
|
||||
return { type: TOGGLE_EXPAND_SLICE, sliceId };
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import HeaderActionsDropdown from './HeaderActionsDropdown';
|
|||
import EditableTitle from '../../components/EditableTitle';
|
||||
import Button from '../../components/Button';
|
||||
import FaveStar from '../../components/FaveStar';
|
||||
import PublishedStatus from './PublishedStatus';
|
||||
import UndoRedoKeylisteners from './UndoRedoKeylisteners';
|
||||
|
||||
import { chartPropShape } from '../util/propShapes';
|
||||
|
|
@ -57,12 +58,14 @@ const propTypes = {
|
|||
colorNamespace: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
isPublished: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fetchFaveStar: PropTypes.func.isRequired,
|
||||
fetchCharts: PropTypes.func.isRequired,
|
||||
saveFaveStar: PropTypes.func.isRequired,
|
||||
savePublished: PropTypes.func.isRequired,
|
||||
startPeriodicRender: PropTypes.func.isRequired,
|
||||
updateDashboardTitle: PropTypes.func.isRequired,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
|
|
@ -272,6 +275,7 @@ class Header extends React.PureComponent {
|
|||
onSave,
|
||||
updateCss,
|
||||
editMode,
|
||||
isPublished,
|
||||
builderPaneType,
|
||||
dashboardInfo,
|
||||
hasUnsavedChanges,
|
||||
|
|
@ -293,6 +297,15 @@ class Header extends React.PureComponent {
|
|||
onSaveTitle={this.handleChangeText}
|
||||
showTooltip={false}
|
||||
/>
|
||||
<span className="publish">
|
||||
<PublishedStatus
|
||||
dashboardId={dashboardInfo.id}
|
||||
isPublished={isPublished}
|
||||
savePublished={this.props.savePublished}
|
||||
canEdit={userCanEdit}
|
||||
canSave={userCanSaveAs}
|
||||
/>
|
||||
</span>
|
||||
<span className="favstar">
|
||||
<FaveStar
|
||||
itemId={dashboardInfo.id}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import TooltipWrapper from '../../components/TooltipWrapper';
|
||||
|
||||
const propTypes = {
|
||||
dashboardId: PropTypes.number.isRequired,
|
||||
isPublished: PropTypes.bool.isRequired,
|
||||
savePublished: PropTypes.func.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const draftButtonTooltip = t(
|
||||
'This dashboard is not published, it will not show up in the list of dashboards. ' +
|
||||
'Click here to publish this dashboard.',
|
||||
);
|
||||
|
||||
const draftDivTooltip = t(
|
||||
'This dashboard is not published which means it will not show up in the list of dashboards.' +
|
||||
' Favorite it to see it there or access it by using the URL directly.',
|
||||
);
|
||||
|
||||
const publishedTooltip = t(
|
||||
'This dashboard is published. Click to make it a draft.',
|
||||
);
|
||||
|
||||
const divStyle = {
|
||||
border: '1px dotted black',
|
||||
backgroundColor: '#F9F9F9',
|
||||
padding: '3px 7px 3px 7px',
|
||||
fontFamily: 'Monospace',
|
||||
fontSize: '16px',
|
||||
};
|
||||
|
||||
export default class PublishedStatus extends React.Component {
|
||||
componentDidMount() {
|
||||
this.togglePublished = this.togglePublished.bind(this);
|
||||
}
|
||||
|
||||
togglePublished() {
|
||||
this.props.savePublished(this.props.dashboardId, !this.props.isPublished);
|
||||
}
|
||||
|
||||
render() {
|
||||
// Show everybody the draft badge
|
||||
if (!this.props.isPublished) {
|
||||
// if they can edit the dash, make the badge a button
|
||||
if (this.props.canEdit && this.props.canSave) {
|
||||
return (
|
||||
<TooltipWrapper
|
||||
label="Unpublished Dashboard"
|
||||
placement="bottom"
|
||||
tooltip={draftButtonTooltip}
|
||||
>
|
||||
<button
|
||||
style={divStyle}
|
||||
onClick={() => {
|
||||
this.togglePublished();
|
||||
}}
|
||||
>
|
||||
Draft
|
||||
</button>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TooltipWrapper
|
||||
label="Unpublished Dashboard"
|
||||
placement="bottom"
|
||||
tooltip={draftDivTooltip}
|
||||
>
|
||||
<div style={divStyle}>Draft</div>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Show the published badge for the owner of the dashboard to toggle
|
||||
else if (this.props.canEdit && this.props.canSave) {
|
||||
return (
|
||||
<TooltipWrapper
|
||||
label="Published Dashboard"
|
||||
placement="bottom"
|
||||
tooltip={publishedTooltip}
|
||||
>
|
||||
<button
|
||||
style={divStyle}
|
||||
onClick={() => {
|
||||
this.togglePublished();
|
||||
}}
|
||||
>
|
||||
Published
|
||||
</button>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't show anything if one doesn't own the dashboard and it is published
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PublishedStatus.propTypes = propTypes;
|
||||
|
|
@ -27,6 +27,7 @@ import {
|
|||
showBuilderPane,
|
||||
fetchFaveStar,
|
||||
saveFaveStar,
|
||||
savePublished,
|
||||
fetchCharts,
|
||||
startPeriodicRender,
|
||||
updateCss,
|
||||
|
|
@ -76,6 +77,7 @@ function mapStateToProps({
|
|||
charts,
|
||||
userId: dashboardInfo.userId,
|
||||
isStarred: !!dashboardState.isStarred,
|
||||
isPublished: !!dashboardState.isPublished,
|
||||
isLoading: isDashboardLoading(charts),
|
||||
hasUnsavedChanges: !!dashboardState.hasUnsavedChanges,
|
||||
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
|
||||
|
|
@ -96,6 +98,7 @@ function mapDispatchToProps(dispatch) {
|
|||
showBuilderPane,
|
||||
fetchFaveStar,
|
||||
saveFaveStar,
|
||||
savePublished,
|
||||
fetchCharts,
|
||||
startPeriodicRender,
|
||||
updateDashboardTitle,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
SHOW_BUILDER_PANE,
|
||||
TOGGLE_EXPAND_SLICE,
|
||||
TOGGLE_FAVE_STAR,
|
||||
TOGGLE_PUBLISHED,
|
||||
UPDATE_CSS,
|
||||
SET_REFRESH_FREQUENCY,
|
||||
} from '../actions/dashboardState';
|
||||
|
|
@ -71,6 +72,9 @@ export default function dashboardStateReducer(state = {}, action) {
|
|||
[TOGGLE_FAVE_STAR]() {
|
||||
return { ...state, isStarred: action.isStarred };
|
||||
},
|
||||
[TOGGLE_PUBLISHED]() {
|
||||
return { ...state, isPublished: action.isPublished };
|
||||
},
|
||||
[SET_EDIT_MODE]() {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ export default function(bootstrapData) {
|
|||
colorNamespace: dashboard.metadata.color_namespace,
|
||||
colorScheme: dashboard.metadata.color_scheme,
|
||||
editMode: dashboard.dash_edit_perm && editMode,
|
||||
isPublished: dashboard.published,
|
||||
builderPaneType:
|
||||
dashboard.dash_edit_perm && editMode
|
||||
? BUILDER_PANE_TYPE.ADD_COMPONENTS
|
||||
|
|
|
|||
|
|
@ -214,6 +214,11 @@ body {
|
|||
position: relative;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.publish {
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_gutter {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export const dashboardStatePropShape = PropTypes.shape({
|
|||
filters: PropTypes.object.isRequired,
|
||||
expandedSlices: PropTypes.object,
|
||||
editMode: PropTypes.bool,
|
||||
isPublished: PropTypes.bool.isRequired,
|
||||
builderPaneType: PropTypes.string.isRequired,
|
||||
colorNamespace: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Add published column to dashboards
|
||||
|
||||
Revision ID: d6ffdf31bdd4
|
||||
Revises: 45e7da7cfeba
|
||||
Create Date: 2018-03-30 14:00:44.929483
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d6ffdf31bdd4"
|
||||
down_revision = "d7c1a0d6f2da"
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table("dashboards") as batch_op:
|
||||
batch_op.add_column(sa.Column("published", sa.Boolean(), nullable=True))
|
||||
op.execute("UPDATE dashboards SET published='1'")
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("dashboards") as batch_op:
|
||||
batch_op.drop_column("published")
|
||||
|
|
@ -420,6 +420,7 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
|
|||
slug = Column(String(255), unique=True)
|
||||
slices = relationship("Slice", secondary=dashboard_slices, backref="dashboards")
|
||||
owners = relationship(security_manager.user_model, secondary=dashboard_user)
|
||||
published = Column(Boolean, default=False)
|
||||
|
||||
export_fields = (
|
||||
"dashboard_title",
|
||||
|
|
@ -484,6 +485,7 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
|
|||
"metadata": self.params_dict,
|
||||
"css": self.css,
|
||||
"dashboard_title": self.dashboard_title,
|
||||
"published": self.published,
|
||||
"slug": self.slug,
|
||||
"slices": [slc.data for slc in self.slices],
|
||||
"position_json": positions,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from flask_appbuilder import expose, SimpleFormView
|
|||
from flask_appbuilder.actions import action
|
||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask_appbuilder.security.decorators import has_access, has_access_api
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
from flask_babel import gettext as __
|
||||
from flask_babel import lazy_gettext as _
|
||||
import pandas as pd
|
||||
|
|
@ -90,6 +91,7 @@ from .base import (
|
|||
DeleteMixin,
|
||||
generate_download_headers,
|
||||
get_error_msg,
|
||||
get_user_roles,
|
||||
handle_api_exception,
|
||||
json_error_response,
|
||||
json_success,
|
||||
|
|
@ -209,36 +211,59 @@ class DatabaseFilter(SupersetFilter):
|
|||
|
||||
class DashboardFilter(SupersetFilter):
|
||||
"""
|
||||
List dashboards for which users have access to at least one slice or are owners.
|
||||
List dashboards with the following criteria:
|
||||
1. Those which the user owns
|
||||
2. Those which the user has favorited
|
||||
3. Those which have been published (if they have access to at least one slice)
|
||||
|
||||
If the user is an admin show them all dashboards.
|
||||
This means they do not get curation but can still sort by "published"
|
||||
if they wish to see those dashboards which are published first
|
||||
"""
|
||||
|
||||
def apply(self, query, func): # noqa
|
||||
if security_manager.all_datasource_access():
|
||||
return query
|
||||
Dash = models.Dashboard
|
||||
User = ab_models.User
|
||||
Slice = models.Slice # noqa
|
||||
Dash = models.Dashboard # noqa
|
||||
User = security_manager.user_model
|
||||
# TODO(bogdan): add `schema_access` support here
|
||||
Favorites = models.FavStar
|
||||
|
||||
user_roles = [role.name.lower() for role in list(self.get_user_roles())]
|
||||
if "admin" in user_roles:
|
||||
return query
|
||||
|
||||
datasource_perms = self.get_view_menus("datasource_access")
|
||||
slice_ids_qry = db.session.query(Slice.id).filter(
|
||||
Slice.perm.in_(datasource_perms)
|
||||
all_datasource_access = security_manager.all_datasource_access()
|
||||
published_dash_query = (
|
||||
db.session.query(Dash.id)
|
||||
.join(Dash.slices)
|
||||
.filter(
|
||||
and_(
|
||||
Dash.published == True, # noqa
|
||||
or_(Slice.perm.in_(datasource_perms), all_datasource_access),
|
||||
)
|
||||
)
|
||||
)
|
||||
owner_ids_qry = (
|
||||
|
||||
users_favorite_dash_query = db.session.query(Favorites.obj_id).filter(
|
||||
and_(
|
||||
Favorites.user_id == User.get_user_id(),
|
||||
Favorites.class_name == "Dashboard",
|
||||
)
|
||||
)
|
||||
owner_ids_query = (
|
||||
db.session.query(Dash.id)
|
||||
.join(Dash.owners)
|
||||
.filter(User.id == User.get_user_id())
|
||||
)
|
||||
|
||||
query = query.filter(
|
||||
or_(
|
||||
Dash.id.in_(
|
||||
db.session.query(Dash.id)
|
||||
.distinct()
|
||||
.join(Dash.slices)
|
||||
.filter(Slice.id.in_(slice_ids_qry))
|
||||
),
|
||||
Dash.id.in_(owner_ids_qry),
|
||||
Dash.id.in_(owner_ids_query),
|
||||
Dash.id.in_(published_dash_query),
|
||||
Dash.id.in_(users_favorite_dash_query),
|
||||
)
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
|
@ -759,8 +784,8 @@ class DashboardModelView(SupersetModelView, DeleteMixin): # noqa
|
|||
add_title = _("Add Dashboard")
|
||||
edit_title = _("Edit Dashboard")
|
||||
|
||||
list_columns = ["dashboard_link", "creator", "modified"]
|
||||
order_columns = ["modified"]
|
||||
list_columns = ["dashboard_link", "creator", "published", "modified"]
|
||||
order_columns = ["modified", "published"]
|
||||
edit_columns = [
|
||||
"dashboard_title",
|
||||
"slug",
|
||||
|
|
@ -768,9 +793,10 @@ class DashboardModelView(SupersetModelView, DeleteMixin): # noqa
|
|||
"position_json",
|
||||
"css",
|
||||
"json_metadata",
|
||||
"published",
|
||||
]
|
||||
show_columns = edit_columns + ["table_names", "charts"]
|
||||
search_columns = ("dashboard_title", "slug", "owners")
|
||||
search_columns = ("dashboard_title", "slug", "owners", "published")
|
||||
add_columns = edit_columns
|
||||
base_order = ("changed_on", "desc")
|
||||
description_columns = {
|
||||
|
|
@ -793,6 +819,10 @@ class DashboardModelView(SupersetModelView, DeleteMixin): # noqa
|
|||
"want to alter specific parameters."
|
||||
),
|
||||
"owners": _("Owners is a list of users who can alter the dashboard."),
|
||||
"published": _(
|
||||
"Determines whether or not this dashboard is "
|
||||
"visible in the list of all dashboards"
|
||||
),
|
||||
}
|
||||
base_filters = [["slice", DashboardFilter, lambda: []]]
|
||||
label_columns = {
|
||||
|
|
@ -2385,6 +2415,41 @@ class Superset(BaseSupersetView):
|
|||
session.commit()
|
||||
return json_success(json.dumps({"count": count}))
|
||||
|
||||
@api
|
||||
@has_access_api
|
||||
@expose("/dashboard/<dashboard_id>/published/", methods=("GET", "POST"))
|
||||
def publish(self, dashboard_id):
|
||||
"""Gets and toggles published status on dashboards"""
|
||||
session = db.session()
|
||||
Dashboard = models.Dashboard # noqa
|
||||
Role = ab_models.Role
|
||||
dash = (
|
||||
session.query(Dashboard).filter(Dashboard.id == dashboard_id).one_or_none()
|
||||
)
|
||||
admin_role = session.query(Role).filter(Role.name == "Admin").one_or_none()
|
||||
|
||||
if request.method == "GET":
|
||||
if dash:
|
||||
return json_success(json.dumps({"published": dash.published}))
|
||||
else:
|
||||
return json_error_response(
|
||||
"ERROR: cannot find dashboard {0}".format(dashboard_id), status=404
|
||||
)
|
||||
|
||||
else:
|
||||
edit_perm = is_owner(dash, g.user) or admin_role in get_user_roles()
|
||||
if not edit_perm:
|
||||
return json_error_response(
|
||||
'ERROR: "{0}" cannot alter dashboard "{1}"'.format(
|
||||
g.user.username, dash.dashboard_title
|
||||
),
|
||||
status=403,
|
||||
)
|
||||
|
||||
dash.published = str(request.form["published"]).lower() == "true"
|
||||
session.commit()
|
||||
return json_success(json.dumps({"published": dash.published}))
|
||||
|
||||
@has_access
|
||||
@expose("/dashboard/<dashboard_id>/")
|
||||
def dashboard(self, dashboard_id):
|
||||
|
|
|
|||
|
|
@ -301,6 +301,14 @@ class DashboardTests(SupersetTestCase):
|
|||
|
||||
def test_public_user_dashboard_access(self):
|
||||
table = db.session.query(SqlaTable).filter_by(table_name="birth_names").one()
|
||||
|
||||
# Make the births dash published so it can be seen
|
||||
births_dash = db.session.query(models.Dashboard).filter_by(slug="births").one()
|
||||
births_dash.published = True
|
||||
|
||||
db.session.merge(births_dash)
|
||||
db.session.commit()
|
||||
|
||||
# Try access before adding appropriate permissions.
|
||||
self.revoke_public_access_to_table(table)
|
||||
self.logout()
|
||||
|
|
@ -379,15 +387,115 @@ class DashboardTests(SupersetTestCase):
|
|||
resp = self.get_resp("/dashboard/list/")
|
||||
self.assertNotIn("/superset/dashboard/empty_dashboard/", resp)
|
||||
|
||||
dash = (
|
||||
db.session.query(models.Dashboard).filter_by(slug="empty_dashboard").first()
|
||||
def test_users_can_view_published_dashboard(self):
|
||||
table = db.session.query(SqlaTable).filter_by(table_name="energy_usage").one()
|
||||
# get a slice from the allowed table
|
||||
slice = (
|
||||
db.session.query(models.Slice).filter_by(slice_name="Energy Sankey").one()
|
||||
)
|
||||
dash.owners = [gamma_user]
|
||||
db.session.merge(dash)
|
||||
|
||||
self.grant_public_access_to_table(table)
|
||||
|
||||
# Create a published and hidden dashboard and add them to the database
|
||||
published_dash = models.Dashboard()
|
||||
published_dash.dashboard_title = "Published Dashboard"
|
||||
published_dash.slug = "published_dash"
|
||||
published_dash.slices = [slice]
|
||||
published_dash.published = True
|
||||
|
||||
hidden_dash = models.Dashboard()
|
||||
hidden_dash.dashboard_title = "Hidden Dashboard"
|
||||
hidden_dash.slug = "hidden_dash"
|
||||
hidden_dash.slices = [slice]
|
||||
hidden_dash.published = False
|
||||
|
||||
db.session.merge(published_dash)
|
||||
db.session.merge(hidden_dash)
|
||||
db.session.commit()
|
||||
|
||||
resp = self.get_resp("/dashboard/list/")
|
||||
self.assertIn("/superset/dashboard/empty_dashboard/", resp)
|
||||
self.assertNotIn("/superset/dashboard/hidden_dash/", resp)
|
||||
self.assertIn("/superset/dashboard/published_dash/", resp)
|
||||
|
||||
def test_users_can_view_own_dashboard(self):
|
||||
user = security_manager.find_user("gamma")
|
||||
|
||||
# Create one dashboard I own and another that I don't
|
||||
dash = models.Dashboard()
|
||||
dash.dashboard_title = "My Dashboard"
|
||||
dash.slug = "my_dash"
|
||||
dash.owners = [user]
|
||||
dash.slices = []
|
||||
|
||||
hidden_dash = models.Dashboard()
|
||||
hidden_dash.dashboard_title = "Not My Dashboard"
|
||||
hidden_dash.slug = "not_my_dash"
|
||||
hidden_dash.slices = []
|
||||
hidden_dash.owners = []
|
||||
|
||||
db.session.merge(dash)
|
||||
db.session.merge(hidden_dash)
|
||||
db.session.commit()
|
||||
|
||||
self.login(user.username)
|
||||
|
||||
resp = self.get_resp("/dashboard/list/")
|
||||
self.assertIn("/superset/dashboard/my_dash/", resp)
|
||||
self.assertNotIn("/superset/dashboard/not_my_dash/", resp)
|
||||
|
||||
def test_users_can_view_favorited_dashboards(self):
|
||||
user = security_manager.find_user("gamma")
|
||||
|
||||
favorite_dash = models.Dashboard()
|
||||
favorite_dash.dashboard_title = "My Favorite Dashboard"
|
||||
favorite_dash.slug = "my_favorite_dash"
|
||||
|
||||
regular_dash = models.Dashboard()
|
||||
regular_dash.dashboard_title = "A Plain Ol Dashboard"
|
||||
regular_dash.slug = "regular_dash"
|
||||
|
||||
db.session.merge(favorite_dash)
|
||||
db.session.merge(regular_dash)
|
||||
db.session.commit()
|
||||
|
||||
dash = (
|
||||
db.session.query(models.Dashboard)
|
||||
.filter_by(slug="my_favorite_dash")
|
||||
.first()
|
||||
)
|
||||
|
||||
favorites = models.FavStar()
|
||||
favorites.obj_id = dash.id
|
||||
favorites.class_name = "Dashboard"
|
||||
favorites.user_id = user.id
|
||||
|
||||
db.session.merge(favorites)
|
||||
db.session.commit()
|
||||
|
||||
self.login(user.username)
|
||||
|
||||
resp = self.get_resp("/dashboard/list/")
|
||||
self.assertIn("/superset/dashboard/my_favorite_dash/", resp)
|
||||
|
||||
def test_user_can_not_view_unpublished_dash(self):
|
||||
admin_user = security_manager.find_user("admin")
|
||||
gamma_user = security_manager.find_user("gamma")
|
||||
slug = "admin_owned_unpublished_dash"
|
||||
|
||||
# Create a dashboard owned by admin and unpublished
|
||||
dash = models.Dashboard()
|
||||
dash.dashboard_title = "My Dashboard"
|
||||
dash.slug = slug
|
||||
dash.owners = [admin_user]
|
||||
dash.slices = []
|
||||
dash.published = False
|
||||
db.session.merge(dash)
|
||||
db.session.commit()
|
||||
|
||||
# list dashboards as a gamma user
|
||||
self.login(gamma_user.username)
|
||||
resp = self.get_resp("/dashboard/list/")
|
||||
self.assertNotIn(f"/superset/dashboard/{slug}/", resp)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue