superset/tests/dashboard_tests.py
Tresdon Jones 97ffb762d0 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
2019-07-10 23:14:13 -07:00

503 lines
18 KiB
Python

# 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.
"""Unit tests for Superset"""
import json
import unittest
from flask import escape
from sqlalchemy import func
from superset import db, security_manager
from superset.connectors.sqla.models import SqlaTable
from superset.models import core as models
from .base_tests import SupersetTestCase
class DashboardTests(SupersetTestCase):
def __init__(self, *args, **kwargs):
super(DashboardTests, self).__init__(*args, **kwargs)
@classmethod
def setUpClass(cls):
pass
def setUp(self):
pass
def tearDown(self):
pass
def get_mock_positions(self, dash):
positions = {"DASHBOARD_VERSION_KEY": "v2"}
for i, slc in enumerate(dash.slices):
id = "DASHBOARD_CHART_TYPE-{}".format(i)
d = {
"type": "DASHBOARD_CHART_TYPE",
"id": id,
"children": [],
"meta": {"width": 4, "height": 50, "chartId": slc.id},
}
positions[id] = d
return positions
def test_dashboard(self):
self.login(username="admin")
urls = {}
for dash in db.session.query(models.Dashboard).all():
urls[dash.dashboard_title] = dash.url
for title, url in urls.items():
assert escape(title) in self.client.get(url).data.decode("utf-8")
def test_new_dashboard(self):
self.login(username="admin")
dash_count_before = db.session.query(func.count(models.Dashboard.id)).first()[0]
url = "/dashboard/new/"
resp = self.get_resp(url)
self.assertIn("[ untitled dashboard ]", resp)
dash_count_after = db.session.query(func.count(models.Dashboard.id)).first()[0]
self.assertEquals(dash_count_before + 1, dash_count_after)
def test_dashboard_modes(self):
self.login(username="admin")
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
url = dash.url
if dash.url.find("?") == -1:
url += "?"
else:
url += "&"
resp = self.get_resp(url + "edit=true&standalone=true")
self.assertIn("editMode": true", resp)
self.assertIn("standalone_mode": true", resp)
self.assertIn('<body class="standalone">', resp)
def test_save_dash(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
positions = self.get_mock_positions(dash)
data = {
"css": "",
"expanded_slices": {},
"positions": positions,
"dashboard_title": dash.dashboard_title,
}
url = "/superset/save_dash/{}/".format(dash.id)
resp = self.get_resp(url, data=dict(data=json.dumps(data)))
self.assertIn("SUCCESS", resp)
def test_save_dash_with_filter(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="world_health").first()
positions = self.get_mock_positions(dash)
filters = {str(dash.slices[0].id): {"region": ["North America"]}}
default_filters = json.dumps(filters)
data = {
"css": "",
"expanded_slices": {},
"positions": positions,
"dashboard_title": dash.dashboard_title,
"default_filters": default_filters,
}
url = "/superset/save_dash/{}/".format(dash.id)
resp = self.get_resp(url, data=dict(data=json.dumps(data)))
self.assertIn("SUCCESS", resp)
updatedDash = (
db.session.query(models.Dashboard).filter_by(slug="world_health").first()
)
new_url = updatedDash.url
self.assertIn("region", new_url)
resp = self.get_resp(new_url)
self.assertIn("North America", resp)
def test_save_dash_with_invalid_filters(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="world_health").first()
# add an invalid filter slice
positions = self.get_mock_positions(dash)
filters = {str(99999): {"region": ["North America"]}}
default_filters = json.dumps(filters)
data = {
"css": "",
"expanded_slices": {},
"positions": positions,
"dashboard_title": dash.dashboard_title,
"default_filters": default_filters,
}
url = "/superset/save_dash/{}/".format(dash.id)
resp = self.get_resp(url, data=dict(data=json.dumps(data)))
self.assertIn("SUCCESS", resp)
updatedDash = (
db.session.query(models.Dashboard).filter_by(slug="world_health").first()
)
new_url = updatedDash.url
self.assertNotIn("region", new_url)
def test_save_dash_with_dashboard_title(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
origin_title = dash.dashboard_title
positions = self.get_mock_positions(dash)
data = {
"css": "",
"expanded_slices": {},
"positions": positions,
"dashboard_title": "new title",
}
url = "/superset/save_dash/{}/".format(dash.id)
self.get_resp(url, data=dict(data=json.dumps(data)))
updatedDash = (
db.session.query(models.Dashboard).filter_by(slug="births").first()
)
self.assertEqual(updatedDash.dashboard_title, "new title")
# bring back dashboard original title
data["dashboard_title"] = origin_title
self.get_resp(url, data=dict(data=json.dumps(data)))
def test_save_dash_with_colors(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
positions = self.get_mock_positions(dash)
new_label_colors = {"data value": "random color"}
data = {
"css": "",
"expanded_slices": {},
"positions": positions,
"dashboard_title": dash.dashboard_title,
"color_namespace": "Color Namespace Test",
"color_scheme": "Color Scheme Test",
"label_colors": new_label_colors,
}
url = "/superset/save_dash/{}/".format(dash.id)
self.get_resp(url, data=dict(data=json.dumps(data)))
updatedDash = (
db.session.query(models.Dashboard).filter_by(slug="births").first()
)
self.assertIn("color_namespace", updatedDash.json_metadata)
self.assertIn("color_scheme", updatedDash.json_metadata)
self.assertIn("label_colors", updatedDash.json_metadata)
# bring back original dashboard
del data["color_namespace"]
del data["color_scheme"]
del data["label_colors"]
self.get_resp(url, data=dict(data=json.dumps(data)))
def test_copy_dash(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
positions = self.get_mock_positions(dash)
new_label_colors = {"data value": "random color"}
data = {
"css": "",
"duplicate_slices": False,
"expanded_slices": {},
"positions": positions,
"dashboard_title": "Copy Of Births",
"color_namespace": "Color Namespace Test",
"color_scheme": "Color Scheme Test",
"label_colors": new_label_colors,
}
# Save changes to Births dashboard and retrieve updated dash
dash_id = dash.id
url = "/superset/save_dash/{}/".format(dash_id)
self.client.post(url, data=dict(data=json.dumps(data)))
dash = db.session.query(models.Dashboard).filter_by(id=dash_id).first()
orig_json_data = dash.data
# Verify that copy matches original
url = "/superset/copy_dash/{}/".format(dash_id)
resp = self.get_json_resp(url, data=dict(data=json.dumps(data)))
self.assertEqual(resp["dashboard_title"], "Copy Of Births")
self.assertEqual(resp["position_json"], orig_json_data["position_json"])
self.assertEqual(resp["metadata"], orig_json_data["metadata"])
# check every attribute in each dashboard's slices list,
# exclude modified and changed_on attribute
for index, slc in enumerate(orig_json_data["slices"]):
for key in slc:
if key not in ["modified", "changed_on"]:
self.assertEqual(slc[key], resp["slices"][index][key])
def test_add_slices(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
new_slice = (
db.session.query(models.Slice)
.filter_by(slice_name="Energy Force Layout")
.first()
)
existing_slice = (
db.session.query(models.Slice).filter_by(slice_name="Name Cloud").first()
)
data = {
"slice_ids": [new_slice.data["slice_id"], existing_slice.data["slice_id"]]
}
url = "/superset/add_slices/{}/".format(dash.id)
resp = self.client.post(url, data=dict(data=json.dumps(data)))
assert "SLICES ADDED" in resp.data.decode("utf-8")
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
new_slice = (
db.session.query(models.Slice)
.filter_by(slice_name="Energy Force Layout")
.first()
)
assert new_slice in dash.slices
assert len(set(dash.slices)) == len(dash.slices)
# cleaning up
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
dash.slices = [o for o in dash.slices if o.slice_name != "Energy Force Layout"]
db.session.commit()
def test_remove_slices(self, username="admin"):
self.login(username=username)
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
origin_slices_length = len(dash.slices)
positions = self.get_mock_positions(dash)
# remove one chart
chart_keys = []
for key in positions.keys():
if key.startswith("DASHBOARD_CHART_TYPE"):
chart_keys.append(key)
positions.pop(chart_keys[0])
data = {
"css": "",
"expanded_slices": {},
"positions": positions,
"dashboard_title": dash.dashboard_title,
}
# save dash
dash_id = dash.id
url = "/superset/save_dash/{}/".format(dash_id)
self.client.post(url, data=dict(data=json.dumps(data)))
dash = db.session.query(models.Dashboard).filter_by(id=dash_id).first()
# verify slices data
data = dash.data
self.assertEqual(len(data["slices"]), origin_slices_length - 1)
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()
resp = self.get_resp("/chart/list/")
self.assertNotIn("birth_names</a>", resp)
resp = self.get_resp("/dashboard/list/")
self.assertNotIn("/superset/dashboard/births/", resp)
self.grant_public_access_to_table(table)
# Try access after adding appropriate permissions.
self.assertIn("birth_names", self.get_resp("/chart/list/"))
resp = self.get_resp("/dashboard/list/")
self.assertIn("/superset/dashboard/births/", resp)
self.assertIn("Births", self.get_resp("/superset/dashboard/births/"))
# Confirm that public doesn't have access to other datasets.
resp = self.get_resp("/chart/list/")
self.assertNotIn("wb_health_population</a>", resp)
resp = self.get_resp("/dashboard/list/")
self.assertNotIn("/superset/dashboard/world_health/", resp)
def test_dashboard_with_created_by_can_be_accessed_by_public_users(self):
self.logout()
table = db.session.query(SqlaTable).filter_by(table_name="birth_names").one()
self.grant_public_access_to_table(table)
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
dash.owners = [security_manager.find_user("admin")]
dash.created_by = security_manager.find_user("admin")
db.session.merge(dash)
db.session.commit()
assert "Births" in self.get_resp("/superset/dashboard/births/")
def test_only_owners_can_save(self):
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
dash.owners = []
db.session.merge(dash)
db.session.commit()
self.test_save_dash("admin")
self.logout()
self.assertRaises(Exception, self.test_save_dash, "alpha")
alpha = security_manager.find_user("alpha")
dash = db.session.query(models.Dashboard).filter_by(slug="births").first()
dash.owners = [alpha]
db.session.merge(dash)
db.session.commit()
self.test_save_dash("alpha")
def test_owners_can_view_empty_dashboard(self):
dash = (
db.session.query(models.Dashboard).filter_by(slug="empty_dashboard").first()
)
if not dash:
dash = models.Dashboard()
dash.dashboard_title = "Empty Dashboard"
dash.slug = "empty_dashboard"
else:
dash.slices = []
dash.owners = []
db.session.merge(dash)
db.session.commit()
gamma_user = security_manager.find_user("gamma")
self.login(gamma_user.username)
resp = self.get_resp("/dashboard/list/")
self.assertNotIn("/superset/dashboard/empty_dashboard/", resp)
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()
)
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.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__":
unittest.main()